cd /news/artificial-intelligence/aztec-experiments Β· home β€Ί topics β€Ί artificial-intelligence β€Ί article
[ARTICLE Β· art-65795] src=github.com β†— pub= topic=artificial-intelligence verified=true sentiment=Β· neutral

Aztec Experiments

An anonymous billboard on Aztec v5 mainnet allows users to deposit ETH on L1, post anonymous messages on L2, and withdraw ETH back to L1. The system includes a censorship mechanism where a censor can flag posts as immoral, and an automated censorship bot uses a local LLM to evaluate posts against an on-chain moderation policy. The project features partial formal verification with 70 proven theorems in Lean 4 / Verity.

read19 min views1 publishedJul 20, 2026
Aztec Experiments
Image: source

An anonymous billboard on Aztec v5 mainnet. Users deposit ETH on L1, post messages anonymously on L2, and withdraw their ETH back to L1. Posts are fully anonymous β€” no sender address appears in public call data, and there is no link to the L1 deposit.

Features:

Censorship mechanism: A censor (set at deploy time) can flag posts as "immoral". Flagged posts are hidden from the default feed and impose a time-lock penalty on the poster's next post. The censor can transfer rights to another address.Automated censorship bot: A local-LLM daemon (censor-daemon/

) watches the billboard, evaluates posts against the on-chain moderation policy, and automatically flags violations.Moderation policy: The censor can set a text moderation policy on-chain (up to 1488 bytes), shown in the UI and CLI. The daemon reads it as the source of truth.Partial formal verification: Security properties (rate limits, censorship screening, privacy, deposit safety) are formally verified in Lean 4 / Verity β€” 70 proven theorems, zerosorry

s. Seefv/.

L1 (Ethereum)                     L2 (Aztec)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ BillboardPortal  β”‚              β”‚ Billboard            β”‚
β”‚                  β”‚              β”‚                      β”‚
β”‚ deposit(ETH)     │──msg──▢      β”‚ claim_deposit()      β”‚
β”‚  records deposit β”‚              β”‚  consumes L1β†’L2 msg  β”‚
β”‚  sends L1β†’L2 msg β”‚              β”‚  creates DepositNote β”‚
β”‚                  β”‚              β”‚                      β”‚
β”‚ withdraw(ETH)    │◀──msg──      β”‚ post()               β”‚
β”‚  consumes Outbox β”‚              β”‚  consumes note       β”‚
β”‚  sends ETH       β”‚              β”‚  stores message      β”‚
β”‚                  β”‚              β”‚  creates new note    β”‚
β”‚                  β”‚              β”‚                      β”‚
β”‚                  β”‚              β”‚ withdraw()           β”‚
β”‚                  β”‚              β”‚  consumes note       β”‚
β”‚                  β”‚              β”‚  sends L2β†’L1 msg     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Deposits: L1 deposit is public (EOA + amount visible on Ethereum)** Posts**: Fully anonymous β€” no sender address in public call data, no link to L1 deposit** Withdrawals**: Reveals depositor's L1 address (inherent β€” ETH must go somewhere)** Anonymity set**: All depositors who haven't withdrawn yet

Posts are rate-limited by a time-lock stored in a private note. The cooldown is inversely proportional to the deposit amount:

cooldown = base_cooldown * min_deposit / amount

For example with base_cooldown=3600, min_deposit=0.001

:

  • 0.001 ETH β†’ 3600s cooldown (1 post/hour)
  • 0.005 ETH β†’ 720s cooldown (5 posts/hour)
  • 0.01 ETH β†’ 360s cooldown (10 posts/hour)

Each post()

advances next_allowed_time

by one cooldown (or more if flagged posts are screened). Posting multiple times in quick succession is possible if enough time has elapsed since the last post β€” the save-up rule (max_save_up

) allows accumulating up to max_save_up

posts of credit during dormancy, then spending them in quick succession. The deposit→withdraw loop does not help: re-depositing creates a new note with a fresh claim-time lock, identical to simply waiting.

A censor (set at deployment time) can flag posts as "immoral" via declare_immoral(post_index, response)

. Flagged posts are:

Hidden from the billboard feed by default (user app shows a "View censored posts" link with confirmation dialog)Flag-penalized: when a flagged post is screened during a subsequentpost()

call, the next post's time lock is extended bycooldown * (K-1)

per flag (K set at deploy time). This is equivalent to making K-1 dummy posts at the same time: screening 1 flagged post costs K total cooldowns (1 base + (K-1) penalty).

Screening uses constant-cost Merkle proofs instead of chain walking. Each post()

call screens 0–2 older posts (the "child" and "grandchild" in the post chain) via assert_note_existed_by

. A post can only be screened after the censor window (default 3600s) has passed since it was created β€” guaranteeing the censor time to flag it first.

Withdrawal requires all real posts to be screened (last_screened_index >= last_real_post_index

). Users make dummy posts (which advance screening without storing content) to screen their last real post before withdrawing. If no real posts exist, only the initial claim-time lock must expire.

The post chain uses a cryptographic link mechanism: each post stores a prev_link

field computed as poseidon2_hash_with_separator([inner_note_hash, link_secret], DOM_SEP__POST_LINK)

, where link_secret

is derived from the poster's nullifier hiding key. This preserves anonymity β€” no public link between posts is revealed.

The censor can transfer censorship rights to another address via transfer_censor(new_address)

. The deployer has no post-deployment control over the censor.

The censor can set a text moderation policy on-chain via set_moderation_policy(fields, len)

(up to 1488 bytes, stored as 48 Field values). The policy is readable via get_moderation_policy()

and is displayed in the user app, censor app, and CLI output. The automated censor daemon reads the on-chain policy as the source of truth for moderation decisions.

See SECURITY_PROPERTIES.md for the full security analysis with line-by-line code references.

The censor-daemon/

directory contains a self-contained daemon that watches the Billboard contract for new posts, runs them through a local LLM (llama.cpp), and automatically flags any that violate the on-chain moderation policy.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  daemon.mjs │────▢│  llama-server │────▢│  Local LLM      β”‚
β”‚  (orchestr) β”‚     β”‚  (port 5090)  β”‚     β”‚  (Qwen3.5-2B)   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”‚ subprocess (black-box)
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  cli.mjs  list --json        β”‚  β†’ posts[], censorWindow, policy
β”‚  cli.mjs  declare-immoral    β”‚  β†’ flags post on-chain
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

From any state, running node daemon.mjs

will:

Download + compile llama.cpp(if not already present)** Download the model**(~1.4 GB Q4_K_M GGUF, Qwen3.5-2B by default)** Start llama-server**β€” local OpenAI-compatible API on127.0.0.1:5090

Read contract configβ€” fetches the on-chain moderation policy, censor window, and max save-up** Poll the billboard**β€” shells out tocli.mjs list --json

to read all postsModerate each postβ€” sends the post + policy to the LLM, asks "VIOLATION or OK?"** Flag violations**β€” shells out tocli.mjs declare-immoral

to flag the post on-chain

The daemon is a thin orchestrator β€” it never touches the Aztec SDK directly. All on-chain operations go through the existing user CLI as black-box subprocess calls. It is censor-window-aware: it prioritizes posts closest to expiring (oldest first), warns on posts with <5 minutes left, and warns on posts already past the censor window.

Policy resolution order: on-chain policy (from get_moderation_policy()

) β†’ local policy.txt

β†’ hardcoded default.

node censor-daemon/daemon.mjs --dry-run --once

node censor-daemon/daemon.mjs --poll-interval 30

Tests: 23 unit tests (test_moderation.mjs

) + 14 integration tests (test_daemon.mjs

) with mock infrastructure β€” no network or llama.cpp required. Run bash censor-daemon/run_tests.sh

.

See censor-daemon/README.md for full details.

Before interacting with L2, you need Fee Juice to pay transaction fees. This app handles the full flow:

Swap ETH→AZTEC(optional) — If you don't already have AZTEC tokens, the app swaps ETH for AZTEC via Uniswap V3 on L1.** Deposit to L2**— Approves the FeeJuicePortal and deposits AZTEC tokens, sending an L1→L2 message.** Scan / Recover**— Scans L1DepositToAztecPublic

events backwards to find your deposit, derives the claim secret from your ETH wallet signature, and checks nullifier consumption + checkpoint status.Claim on L2β€” Consumes the L1β†’L2 message to mint Fee Juice on L2.

Also supports an auto

action that runs deposit β†’ wait β†’ claim in one go.

Deploys and links the billboard contracts. The deployment has a circular dependency (L1 portal needs L2 address, L2 contract needs L1 portal address), broken by making the L2 constructor take no arguments and setting the portal later.

Deploy L2β€” Deploys the Noir Billboard contract using universal deploy (address depends only on salt + artifact, not deployer wallet).Deploy L1 Portalβ€” Deploys the Solidity portal via CREATE2 (deterministic from L2 address + rollup + version).** Link Portal**β€” Callsupdate_portal()

on L2 to store the L1 portal address.Cross-Checkβ€” Verifies L1β†’L2 and L2β†’L1 references match.

The main user-facing app. Full flow: deposit β†’ claim β†’ post β†’ withdraw β†’ claim on L1.

Deposit ETHβ€” Deposits ETH into the L1 portal (sends an L1β†’L2 message).** Claim on L2**β€” Consumes the L1β†’L2 message to create a private DepositNote.** Post**β€” Posts an anonymous message. Each post extends the time lock by one cooldown. The billboard feed auto-refreshes every 5 seconds.Withdrawβ€” Consumes the DepositNote and sends an L2β†’L1 message.** Claim on L1**β€” After the epoch proof (~40 min on mainnet), consumes the Outbox message to claim ETH on L1.

The billboard feed hides censored posts by default. A "View censored posts" link at the bottom shows how many posts are hidden; clicking it opens a confirmation dialog ("These posts have been flagged as immoral by the censor. Are you sure you want to see them?"). Confirming reveals the censored posts with grey styling and a "Hide censored posts" toggle to hide them again.

The auto

action runs the full flow end-to-end autonomously.

A dedicated app for the censor to manage censorship. No ETH wallet needed β€” the censor only operates on L2.

Load censor walletβ€” Load the censor's Aztec wallet (the one whose address was passed toinit

at deploy time, or the one that received rights viatransfer_censor

).Flag postsβ€” Enter a post index and optional response text, then flag it as immoral. Flagged posts are hidden from the user billboard by default. When a flagged post is later screened during a subsequent post, the poster's next post time lock is extended by (K-1) extra cooldowns per flag.Transfer rightsβ€” Transfer censorship authority to another Aztec address.** View billboard**β€” The censor sees all posts including flagged ones (with censor response messages).

The censor is set at init()

time only β€” the deployer cannot change it after deployment. Only the censor can transfer rights to another address.

aztec/
β”œβ”€β”€ README.md                        ← this file
β”œβ”€β”€ SECURITY_PROPERTIES.md           ← security analysis with code references
β”œβ”€β”€ wallet.json                      ← Aztec wallet (secret key + salt)
β”œβ”€β”€ eth_wallet.json                  ← ETH wallet (private key)
β”œβ”€β”€ package.json                     ← ethers + fake-indexeddb (for CLIs)
β”‚
β”œβ”€β”€ billboard/                       ← Contracts
β”‚   β”œβ”€β”€ Nargo.toml                   ← Noir workspace
β”‚   β”œβ”€β”€ billboard_contract/          ← L2 contract (Noir)
β”‚   β”‚   β”œβ”€β”€ Nargo.toml
β”‚   β”‚   └── src/
β”‚   β”‚       β”œβ”€β”€ main.nr              ← Billboard contract
β”‚   β”‚       └── lib.nr               ← Helpers: msg hashes, cooldown computation
β”‚   β”œβ”€β”€ portal/                      ← L1 contract (Solidity / Foundry)
β”‚   β”‚   β”œβ”€β”€ foundry.toml
β”‚   β”‚   └── src/
β”‚   β”‚       └── BillboardPortal.sol
β”‚   └── target/                      ← Compiled Noir artifacts (gitignored)
β”‚
β”œβ”€β”€ shared/                          ← Shared modules (used by all apps)
β”‚   β”œβ”€β”€ styles.css                   ← Dark theme CSS
β”‚   β”œβ”€β”€ helpers.js                   ← UI helpers: logging, pagination, extractors
β”‚   β”œβ”€β”€ wallet-buttons.js            ← Common wallet /generation UI module
β”‚   β”œβ”€β”€ app-env.js                   ← Common browser env builders (buildEnv, buildConfig, makeCallEngine)
β”‚   β”œβ”€β”€ aztec-lib.js                 ← PXE/CRS/wallet infrastructure + Fee Juice constants
β”‚   β”œβ”€β”€ aztec_bundle.js              ← Aztec PXE bundle (WASM, ~58MB)
β”‚   β”œβ”€β”€ ethers.min.js                ← ethers v6 (for L1 interactions)
β”‚   β”œβ”€β”€ poseidon2.js                 ← Pure-JS Poseidon2 (verified against bb.js)
β”‚   └── rpc-config.json              ← Aztec node URL + API key
β”‚
β”œβ”€β”€ apps/                            ← Web apps + CLIs
β”‚   β”œβ”€β”€ build.mjs                    ← Build script (combines modules β†’ single-file HTML)
β”‚   β”œβ”€β”€ serve.py                     ← Dev server with COOP/COEP headers for multi-threaded WASM
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ fee-juice/               ← Fee Juice app
β”‚   β”‚   β”‚   β”œβ”€β”€ template.html
β”‚   β”‚   β”‚   β”œβ”€β”€ app.js               ← Thin wrapper (UI β†’ engine)
β”‚   β”‚   β”‚   β”œβ”€β”€ engine.js            ← Fee Juice flow logic (swap, deposit, scan, claim)
β”‚   β”‚   β”‚   β”œβ”€β”€ cli.mjs              ← CLI tool (same engine, Node.js)
β”‚   β”‚   β”‚   └── pxe-cache.cjs        ← IndexedDB dump/restore for CLI PXE caching
β”‚   β”‚   └── billboard/
β”‚   β”‚       β”œβ”€β”€ billboard_artifact.json   ← Compiled L2 contract artifact
β”‚   β”‚       β”œβ”€β”€ portal_bytecode.txt       ← Compiled L1 portal bytecode
β”‚   β”‚       β”œβ”€β”€ build_artifact.mjs        ← Artifact build script (VKs, bytecode)
β”‚   β”‚       β”œβ”€β”€ build_vks.sh              ← VK computation helper
β”‚   β”‚       β”œβ”€β”€ deploy/                   ← Deploy app
β”‚   β”‚       β”‚   β”œβ”€β”€ template.html
β”‚   β”‚       β”‚   β”œβ”€β”€ app.js               ← Thin wrapper
β”‚   β”‚       β”‚   β”œβ”€β”€ engine.js            ← Deploy flow (L2 + L1 + link + cross-check)
β”‚   β”‚       β”‚   β”œβ”€β”€ cli.mjs              ← CLI tool
β”‚   β”‚       β”‚   β”œβ”€β”€ gen_eth_wallet.mjs   ← ETH wallet generator
β”‚   β”‚       β”‚   β”œβ”€β”€ gen_censor_wallet.mjs← Censor Aztec+ETH wallet generator
β”‚   β”‚       β”‚   β”œβ”€β”€ gen_user_wallet.mjs  ← User Aztec+ETH wallet generator
β”‚   β”‚       β”‚   β”œβ”€β”€ billboard_artifact.json
β”‚   β”‚       β”‚   └── portal_bytecode.txt
β”‚   β”‚       β”œβ”€β”€ censor/                   ← Censor app
β”‚   β”‚       β”‚   β”œβ”€β”€ template.html
β”‚   β”‚       β”‚   β”œβ”€β”€ app.js               ← Flag posts, transfer rights, view all posts
β”‚   β”‚       β”‚   β”œβ”€β”€ engine.js            ← Symlink β†’ user/engine.js (shared engine)
β”‚   β”‚       β”‚   └── pxe-cache.cjs        ← Symlink β†’ user/pxe-cache.cjs
β”‚   β”‚       └── user/                     ← User app
β”‚   β”‚           β”œβ”€β”€ template.html
β”‚   β”‚           β”œβ”€β”€ app.js               ← Thin wrapper (UI β†’ engine, live feed, censored post hiding)
β”‚   β”‚           β”œβ”€β”€ engine.js            ← User flow (deposit, claim, post, withdraw, claim-l1, declare-immoral, transfer-censor)
β”‚   β”‚           β”œβ”€β”€ cli.mjs              ← CLI tool
β”‚   β”‚           └── pxe-cache.cjs        ← IndexedDB dump/restore for CLI PXE caching
β”‚   β”œβ”€β”€ dist/                        ← Built single-file apps
β”‚   β”‚   β”œβ”€β”€ fee-juice.html
β”‚   β”‚   β”œβ”€β”€ deploy.html
β”‚   β”‚   β”œβ”€β”€ user.html
β”‚   β”‚   β”œβ”€β”€ censor.html
β”‚   β”‚   β”œβ”€β”€ aztec_bundle.js          ← Copy of the PXE bundle
β”‚   β”‚   └── crs/                     ← CRS files for proving
β”‚
β”œβ”€β”€ censor-daemon/                  ← Automated censorship bot (local LLM)
β”‚   β”œβ”€β”€ daemon.mjs                  ← Orchestrator: llama.cpp setup, polling, flagging
β”‚   β”œβ”€β”€ moderation.mjs              ← LLM moderation logic (prompt, verdict parsing)
β”‚   β”œβ”€β”€ policy.txt                  ← Default moderation policy (fallback)
β”‚   β”œβ”€β”€ test_moderation.mjs         ← Unit tests for moderation parsing (23 tests)
β”‚   β”œβ”€β”€ test_daemon.mjs             ← Integration tests with mock infra (14 tests)
β”‚   β”œβ”€β”€ run_tests.sh                ← Test runner script
β”‚   └── README.md                   ← Full daemon documentation
β”‚
β”œβ”€β”€ fv/                             ← Formal verification (Lean 4 + Verity)
β”‚   β”œβ”€β”€ PLAN.md                     ← Verification plan and status
β”‚   β”œβ”€β”€ billboard_aztec_lean/       ← Lean model of L2 Noir contract (51 theorems)
β”‚   β”‚   └── BillboardAztec.lean
β”‚   β”œβ”€β”€ billboard_portal_verity/    ← Verity port of L1 Solidity portal (12 theorems)
β”‚   β”‚   β”œβ”€β”€ BillboardPortal.lean
β”‚   β”‚   β”œβ”€β”€ Spec.lean, Invariants.lean
β”‚   β”‚   └── Proofs/Basic.lean
β”‚   β”œβ”€β”€ bridge_model/               ← Lean model of L1↔L2 bridge (7 theorems)
β”‚   β”‚   └── Bridge.lean
β”‚   └── notes/
β”‚       └── SECURITY_PROPERTIES_FORMAL.md ← Theorem-by-theorem mapping (P1-P18, I1-I12)
β”‚
β”œβ”€β”€ .pxe-cache/                     ← CLI PXE cache (IndexedDB dumps, gitignored)
β”‚
└── node_modules/                    ← ethers, fake-indexeddb

Each app follows the same pattern:

β€” All business logic. Takes anengine.js

env

object ({ aztec, ethers, log, initCRS, createStore, getEthSigner, portalBytecode, artifact }

) and aconfig

object. Returns{ ok, state, handles }

wherehandles

gives the web UI access to live PXE/contract objects for polling.β€” Thin UI wrapper. Buildsapp.js

env

/config

from UI state, calls the engine with the appropriate action, routeslog

callbacks to page-specific status divs.β€” CLI tool. Same engine, Node.js environment. Polyfills IndexedDB withcli.mjs

fake-indexeddb

, loads the bundle, runs the engine.

Both the web UI and CLI share the same engine, so any flow that is autonomous in the CLI is also autonomous in the web UI.

Fee Juice: status

, scan

, deposit

, claim

, auto

User: status

, deposit

, claim

, post

, list

, withdraw

, claim-l1

, declare-immoral

, transfer-censor

, auto

Deploy: status

, deploy

The auto

action runs the full flow end-to-end: deposit β†’ wait for L2 ingest β†’ claim β†’ post (if message provided) β†’ withdraw β†’ claim-l1.

Censor daemon: daemon.mjs

is not an engine action but a standalone orchestrator that shells out to cli.mjs list --json

and cli.mjs declare-immoral

as subprocesses. See censor-daemon/README.md.

cd billboard && aztec compile

cd apps/src/billboard && node build_artifact.mjs
cp billboard_artifact.json deploy/billboard_artifact.json

cd billboard/portal && forge build --use 0.8.33

cd apps && node build.mjs
cd fv/verity
export PATH="$HOME/.elan/bin:$PATH"
lake build   # 2125 jobs, zero sorrys

The L2 contract's Nargo.toml

must depend on aztec-nr

tag v5.0.0

β€” the same version the aztec_bundle.js

was built from. Version mismatches cause standard contract addresses (HandshakeRegistry, AuthRegistry, etc.) baked into the ACIR to differ from those registered in PXE, causing "contract is not registered" errors.

Each recompilation changes the contract class ID (public bytecode commitment), so always use a salt that was never used before when deploying.

node apps/src/fee-juice/cli.mjs --gen-all                          # Generate new wallets
node apps/src/fee-juice/cli.mjs --status --aztec-wallet wallet.json --eth-wallet eth_wallet.json
node apps/src/fee-juice/cli.mjs --scan --aztec-wallet wallet.json --eth-wallet eth_wallet.json
node apps/src/fee-juice/cli.mjs --eth-for-swap 0.005 --aztec-wallet wallet.json --eth-wallet eth_wallet.json

node apps/src/billboard/user/cli.mjs status   --contract-salt 2028 --aztec-wallet wallets/user_aztec_wallet.json --eth-wallet wallets/user_eth_wallet.json
node apps/src/billboard/user/cli.mjs deposit  --contract-salt 2028 --amount 0.002 --aztec-wallet wallets/user_aztec_wallet.json --eth-wallet wallets/user_eth_wallet.json
node apps/src/billboard/user/cli.mjs post     --contract-salt 2028 --msg "hello world" --aztec-wallet wallets/user_aztec_wallet.json --eth-wallet wallets/user_eth_wallet.json \
  --censor 0x0035abfebdafd10697b8a9a5de2792715602ff077787ca6cfefdab632547189f --k-multiplier 4
node apps/src/billboard/user/cli.mjs list     --contract-salt 2028 --aztec-wallet wallets/user_aztec_wallet.json --eth-wallet wallets/user_eth_wallet.json \
  --censor 0x0035abfebdafd10697b8a9a5de2792715602ff077787ca6cfefdab632547189f --k-multiplier 4
node apps/src/billboard/user/cli.mjs withdraw --contract-salt 2028 --aztec-wallet wallets/user_aztec_wallet.json --eth-wallet wallets/user_eth_wallet.json \
  --censor 0x0035abfebdafd10697b8a9a5de2792715602ff077787ca6cfefdab632547189f --k-multiplier 4

node apps/src/billboard/user/cli.mjs declare-immoral --contract-salt 2028 --post-index 0 --censor-response "spam" \
  --censor 0x0035abfebdafd10697b8a9a5de2792715602ff077787ca6cfefdab632547189f --k-multiplier 4 \
  --censor-wallet wallets/censor_aztec_wallet.json
node apps/src/billboard/user/cli.mjs transfer-censor --contract-salt 2028 --new-censor 0x... \
  --censor 0x0035abfebdafd10697b8a9a5de2792715602ff077787ca6cfefdab632547189f --k-multiplier 4 \
  --censor-wallet wallets/censor_aztec_wallet.json
python3 apps/serve.py [port]  # default: 5000 (project convention)

Sets COOP/COEP headers for SharedArrayBuffer

(multi-threaded WASM) and serves from apps/dist/

. Local CRS files in apps/dist/crs/

are loaded automatically.

The L2 contract is deployed with universalDeploy: true

and publicKeys: deriveKeys(Fr.ZERO).publicKeys

, so the contract address depends only on the salt + artifact β€” not the deployer's wallet. This means the deploy and user apps always compute the same address.

The v5 bundle uses the initializerless Schnorr account contract for self-deployment. The regular SchnorrAccountContract stores its signing key in a private note created by the constructor, but during self-deployment (deploy + claim fee juice in one tx), the entrypoint runs before the constructor, so get_note()

fails. The initializerless variant stores the signing key in a PXE capsule and verifies against immutables_hash

.

The web apps run the full PXE stack entirely in the browser:

IndexedDB store for PXE state (data directory includes wallet address for isolation)CRS files loaded from local files first, falling back to CDNProving viabb.js

WASM (multi-threaded when COOP/COEP headers are set)No backend serverβ€” all RPC goes directly to the Aztec node

Both CLIs dump/restore the in-memory IndexedDB state to ~/.pxe-cache/

between runs, saving ~2-4s on PXE sync. Cache files are keyed by account address.

The shared wallet-buttons.js

module provides a common wallet UI across all three apps with:

ETH wallet: Load from JSON, Generate random, or connect browser wallet** Aztec wallet**: Load from JSON, Generate random, or derive from ETH wallet (sign a message, use signature as secret key)- Color-coded button states and auto-advancement when all wallets are loaded

nargo/** aztecCLI (must match the v5 bundle version) forge**(Foundry, for compiling the Solidity portal)** Node.js**β‰₯ 24- A browser wallet for L1 transactions (in the web UI) Lean 4 / elan(only for building formal verification proofs infv/

)

The fv/

directory contains a partial formal verification of the billboard's security properties using Verity (Lean 4 framework for verified smart contracts) and a standalone Lean model of the L2 contract.

Build status: 2125 Lean jobs, zero sorrys, 70 proven theorems/lemmas.

Component File Theorems Axioms Status
L2 model (Lean)
fv/billboard_aztec_lean/BillboardAztec.lean
51 5 Constant-cost screening model: I1–I12, deposit safety, privacy
L1 portal (Verity)
fv/billboard_portal_verity/
12 1 Solidity port rewritten as verity_contract , guard proofs + conservation
Bridge model (Lean)
fv/bridge_model/Bridge.lean
7 2 Inbox/Outbox no-double-consume, content-hash agreement

L2 model (51 theorems, models the Noir contract's constant-cost screening design from LATEST_CHANGE.md):

I1β€” Constant-cost screening: eachpost()

screens at most 2 posts (zero axioms)I3β€” Censor window guarantee: posts only screened aftercensor_window

expires (zero axioms)I4β€” Monotonic advance:last_screened

never goes backwards (zero axioms)I5β€” Flag penalty: each flag adds exactlycooldown Γ— (K-1)

(zero axioms)I6β€” Save-up rule bounds burst posting (zero axioms)** I7**β€” Withdrawal requires full screening (zero axioms)** I10**β€” Chain link unforgeability (usesposeidon2_injective

axiom)Master safety theorem(deposit_safety_master

): withdraw ≀T_stop + ⌈N/2βŒ‰ Γ— C Γ— (1+(K-1)Γ—2)

β€” no permanent lockoutMaster privacy theorem(post_unlinkability_master

): two posts by same user are indistinguishable from two by different users (constructive proof, zero axioms)Censor governance: only censor can flag/transfer/set-policy (proven)

L1 portal (7 Verity proofs + 5 stubs): P2 (no double deposit), P3 (withdraw zeroes balance), P4 (no withdraw without deposit), P14 (u128 overflow), conservation of totalDeposited

.

Bridge: no-double-consume for Inbox and Outbox, content-hash agreement axioms (P1/P16).

8 axioms total, all at cryptographic/platform trust boundaries:

poseidon2_injective

β€” hash injectivity (cryptographic assumption)nhk_app_opacity

β€” link_secret not derivable from public data (cryptographic)nullifier_unlinkability

β€” nullifiers don't reveal owner (Aztec platform property)deposit/withdraw_hash_agreement

β€” L1 and L2 agree on content hashes (bridge boundary)externalCallStubBool_true

β€” Verity test stub for external calls returnstrue

See fv/notes/SECURITY_PROPERTIES_FORMAL.md for the full theorem-by-theorem mapping, and fv/PLAN.md for the verification plan.

Strengthen end-to-end FV theoremsβ€” The bridge soundness theorems (P1, P5, P16) and several L2 invariants (I2, I8, I9, I11, I12) are currently stated asTrue := by trivial

. Strengthen these to full proofs connecting L1 Verity + L2 Lean + bridge model. - Replace in-app JSON Ethereum wallet withβ€” The current "Load from JSON" ETH wallet option requires users to manually export and load a private key JSON file. Replace this with OpenLV integration for a more secure, user-friendly wallet connection.OpenLV - End-to-end browser test of censor appβ€” The censor web app (censor.html

) has been built but not yet tested in a browser. The CLIdeclare-immoral

andtransfer-censor

actions have been fully tested on mainnet (salt 2028). - End-to-end browser test of censored post hidingβ€” The "View censored posts" confirmation dialog and grey styling have been implemented in the user app but not yet tested in a browser against live mainnet data.

Add access control guard toβ€” Currently anyone can call it first with a bogus address, bricking the contract (griefing DoS, not theft). Should be deployer-only or called in the same L2 tx batch as deploy. Seeupdate_portal()

SECURITY_PROPERTIES.mdΒ§5. - Fixβ€” Theoretically wraps after ~134M posts, overwriting post 0. Economically unreachable today but should be fixed for correctness.post_count

/post_data

key type from u32 to u64/Field - Fixβ€” Over-counts across deposit/withdraw cycles (never decremented). Not security-relevant (withdrawals readtotalDeposited

accounting driftdeposits[msg.sender]

, not the total) but cosmetically wrong.

Surface L1 withdrawal claim in the web UIβ€” Theclaim-l1

action exists in the engine/CLI but the web UI doesn't expose it as a distinct page with Outbox proof input. - Add "view on explorer" links for confirmed L2 transactions (e.g. aztecscan.com). - Show estimated wait time for epoch proofs in the UI during claim-l1 (~40 min on mainnet). - Suppress verbose PXE/bundle console output in the browser β€” Partially done, but pino logger output still leaks through to the console.

Add transient RPC retry logic to web appsβ€” The CLI has this via aTRANSIENT_RE

regex; the web apps may not. - Handle multiple deposits to different Aztec addresses from the same ETH walletβ€” Fee-juice scan filters byto

address, but the UX could be clearer about which deposit is being claimed.

Withdraw deposits on old test deploymentsβ€” Salts 2025, 2026, 2027, 2028 have deposits that can be withdrawn to recover ETH.

── more in #artificial-intelligence 4 stories Β· sorted by recency
── more on @aztec 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/aztec-experiments] indexed:0 read:19min 2026-07-20 Β· β€”