# Introducing the New L1 Native Privacy Layer

**URL:** https://forum.bnbchain.org/t/introducing-the-new-l1-native-privacy-layer/5481
**Category:** Privacy
**Created:** [July 28, 2026, 1:30pm UTC](https://forum.bnbchain.org/t/introducing-the-new-l1-native-privacy-layer/5481 "2026-07-28T13:30:16Z")
**Posts on this page:** 3
**Page:** 1

<div class="post-metadata">

### Author: ![galaio](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.bnbchain.org/galaio/32/580_2.png) [@galaio](https://forum.bnbchain.org/u/galaio)
#### Post date: [July 28, 2026, 1:30pm UTC](https://forum.bnbchain.org/t/introducing-the-new-l1-native-privacy-layer/5481/1 "2026-07-28T13:30:16Z")

</div>

New L1 embeds a native shielded pool inside the L1 protocol: a note/nullifier accounting model, dual Groth16/BN254 proofs, and a Poseidon2 commitment tree, together with the genesis-predeployed, non-upgradeable system contract `ShieldedPool(0x5000)`, the Poseidon2 precompile `0x67`, and the native privacy transaction type `0x77`. This article starts from the user-facing scenarios and the end-to-end flow, then dissects the implementation, presents measured metrics, and closes with the forward path: PQ migration, recursive proofs, a compliance hook, and ERC20 support.

## 01 · Usage: how the privacy layer looks

Privacy in New L1 is not a bolt-on mixer but a protocol-native “second accounting layer”: once funds are deposited into the pool they exist as encrypted notes; in-pool transfers hide amount, asset, and both counterparties completely; only withdraw returns to the public world. Throughout, the user’s only backup is a single 32-byte seed.

### 1.1 Participants and the three core operations

There are four kinds of participants: **users** (hold the seed and the notes), **relayers** (pay gas, hide the user’s EOA), **validators / block producers** (in Direct mode they pack `0x77` directly and collect the public gasFee), and the genesis-predeployed **system contract** itself. Everything today is built around three primitives:

- `shield` · **privDeposit**

An EOA calls the system contract directly, transfers BNB in the clear, and mints one private note inside the pool. The entrance is necessarily public — “I am a pool user” is the inherent cost of entering a shielded pool.

- `private` · **transfer / atomicCall**

In-pool 2-in-3-out private transfer: amount, asset, and both counterparties are hidden. The atomicCall variant can unshield → call public DeFi → reshield within a single atomic transaction.

- `unshield` · **withdraw**

Nullify a note and pay out publicly to any EOA. Submitting through a relayer or `0x77` hides “who initiated the withdrawal”. Withdraw can never be disabled by governance.

### 1.2 Key hierarchy: one seed, three role-separated keys

The user only backs up a single 32-byte seed (`seed`); all three keys are derived from it. The derivation deliberately places the “ **key that spends** ” and the “ **key that reads** ” in two mathematically disjoint worlds: `ownerNullifierKey` (the spend key, abbreviated **onk** below) is a scalar on BN254 — spending requires Poseidon2 hashing inside a zero-knowledge circuit, and the circuit only understands that field; `viewKey` (the viewing key) is a scalar on secp256k1 — it only decrypts incoming ciphertexts off-chain and never enters a circuit.

The two keys live in different fields and serve different purposes, and leaking one does not yield the other; this also avoids the roughly 1.5 million constraints that emulating secp256k1 arithmetic inside a circuit would cost. Abbreviations used in this report: **onk** = ownerNullifierKey (spend key), **onkHash** = ownerNullifierKeyHash (spend key hash, registered on-chain), **nss** = noteSecretSeed (note blinding seed), **seedHash** = noteSecretSeedHash (blinding seed hash).

```plaintext

# the only secret that must be backed up

seed (32B)

├─ ownerNullifierKey = keccak256("newl1.shield.onk" ‖ seed) mod r # BN254: spending + nullifier derivation

│ └─ ownerNullifierKeyHash = poseidon(OWNER_NK_HASH, onk) # registered on-chain, globally unique, immutable

├─ noteSecretSeed = keccak256("newl1.shield.nss" ‖ seed) mod r # BN254: derives the blinding factor of output notes

│ └─ noteSecretSeedHash = poseidon(NOTE_SECRET, nss) # registered on-chain, circuit anchor only

└─ viewKey = normalizeEvenY(sha256("newl1.shield.view" ‖ seed)) # secp256k1

 └─ viewKeyPub = x-only(viewKey·G) # 32B X coordinate = privacy address

```

| Key | Field / curve | Role | On-chain form | In circuit? |
| --- | --- | --- | --- | --- |
| ownerNullifierKey | BN254 scalar field r | spend authorization + nullifier derivation | hash only (registry) | ✅ pool proof |
| noteSecretSeed | BN254 scalar field r | derives the noteSecret of each output note | hash only (anchor) | ✅ anchoring assertion |
| viewKey | secp256k1 (mod n) | scanning + ECIES decryption of note ciphertext | public key is the privacy address | ❌ off-chain only |
| EOA key | secp256k1 | EIP-712 signature authorization (auth proof) | poseidon(pubkey) commitment | ✅ auth circuit |

#### View key in detail: the cryptographic boundary of read-only access

`viewKey` is the only “read-only key” in the system, and its power is deliberately narrowed to one thing: **decrypting note ciphertexts addressed to you**. Understanding its four boundaries is understanding New L1’s visibility model:

- `what it can do` · **Discover and read incoming funds**

Trial-decrypt each `outputNoteData` with `viewKey` via ECDH — if the AEAD tag verifies, the note is yours, and the plaintext yields `(amount, tokenField, onkHash, noteSecret, memo)`. Handing viewKey to an auditor or accountant grants an **incoming-only read view** : they can see every amount you received but obtain no spending power.

- `what it cannot do` · **Spend · full outgoing view · linkage**

Spending requires `ownerNullifierKey` (independently derived; viewKey cannot yield it). The outgoing side is only visible when the change note is also encrypted to yourself (normal wallets do this, but that is a wallet convention, not a protocol guarantee). viewKey also cannot link any two on-chain notes to the same person — blinding lives in `noteSecret`, not in the viewing layer.

- **PQ exposure** : ECDH is the harvest-now-decrypt-later surface. The leading version byte of `outputNoteData` is the switch for upgrading to ML-KEM hybrid encapsulation — a purely off-chain change that does not touch the circuits.

### 1.3 EOA address ↔ privacy address: an on-chain registry

The privacy address is simply `viewKeyPub`, paired with an **on-chain privacy registry** : the sender performs one on-chain lookup keyed by the recipient’s EOA address and obtains everything needed to encrypt and to construct the commitment, with no off-chain address-distribution protocol required.

```solidity

// registry inside ShieldedPool (rule M-A)

mapping(address => PrivacyInfo) registry;

struct PrivacyInfo {

uint256 ownerNullifierKeyHash; // globally unique, immutable after first registration (prevents nullifier-domain collisions / orphan notes)

uint256 noteSecretSeedHash; // rotatable

uint256 authDataCommitment; // poseidon(EOA pubkey limbs), rotatable (used by EOA-reuse authorization)

uint8 scheme; // 0=ecdsa (today: the spend-key channel), 1=pq (reserved), rotatable

bytes32 viewKeyPub; // the privacy address itself, rotatable

}

```

### 1.4 Privacy registration (registerPrivacyInfo)

Registration is keyed by `msg.sender` (no registration on someone else’s behalf). It is the user’s first step into the privacy system and the only explicit binding between an EOA and a privacy identity:

1. **keygen** — `newl1-shield keygen` derives the three keys from the seed and prints the `PrivacyInfo` tuple (ownerNullifierKeyHash, noteSecretSeedHash, scheme, viewKeyPub).

2. **register** — the EOA sends `registerPrivacyInfo(ownerNullifierKeyHash, noteSecretSeedHash, scheme, viewKeyPub)`; the contract locks ownerNullifierKeyHash (onkHash) as globally unique and emits `PrivacyInfoSet`.

3. **address distribution** — from then on anyone who knows your EOA address can send you a private transfer: the sender looks the registry up on-chain, so you never have to provide any out-of-band information.

### 1.5 Deposit: public inbound funds → private note

```solidity

function privDeposit(uint256 ownerCommitment, uint8 scheme,

                 uint256 tokenField, bytes outputNoteData) payable

// native coin only for now: require(tokenField == 0 && msg.value > 0 && !depositPaused)

// noteBody = poseidon(NOTE_BODY, ownerCommitment, msg.value, 0, isOpen=0, scheme)

// enqueue into the deferred queue; totalShielded += msg.value; emit NoteQueued / Deposit

```

What the chain shows: `from` = the user’s EOA (public), `to` = the system contract, amount public. When depositing **to someone else** , the caller builds `ownerCommitment` from that party’s registry record and attaches the ECIES-encrypted `outputNoteData`; when depositing to yourself, you use your own material. Under deferred insertion a deposit only enqueues, and the measured user cost is **151,951 gas**.

### 1.6 Transfer: 2-in-3-out fully private transfer

A transfer consumes 2 inputs (when fewer than two real notes are available, **phantom inputs** fill the slots, hiding the real input count) and produces 3 notes — the payment, the change, and the **private fee note** paid to the relayer. Value conservation is enforced inside the circuit: `Σinputs = out0 + out1 + out2(fee) + publicAmountOut`, and in the transfer case `publicAmountOut ≡ 0`, so no amount appears on-chain at all.

 ![fig-1](https://us1.discourse-cdn.com/flex020/uploads/binancesmartchain1/original/2X/c/cac83ae434e8664ae3a961918dd14b2134fb05e9.png)

_fig 1 · 2-in-3-out: fixed arity plus phantom inputs hide the real input count; feeAmount is a private witness bound through the intent digest so a relayer cannot tamper with it_

### 1.7 Withdraw: the public exit

A withdrawal goes through the same `transact` entrypoint (`operationKind=1`): the circuit proves possession of `ownerNullifierKey` and nullifies the nullifier, the contract pays `publicAmountOut` to `publicRecipient`, and `totalShielded` decreases. The recipient address and the amount are necessarily public (funds really do leave), but submitting via a relayer or `0x77` hides “who initiated the withdrawal” — on-chain one only sees the pool paying a fresh address.

### 1.8 Native privacy transaction type 0x77 and the three submission modes

`0x77` (ShieldedTx) is a native EIP-2718 transaction type with **no from, no nonce, no gasPrice, and no outer signature** — authorization is carried entirely by the embedded proofs, so any validator or builder can pack it directly. It coexists with the existing relayer path, giving three submission modes:

| Mode | Status | Fees and exposure surface |
| --- | --- | --- |
| **① Direct** (0x77, implemented) | implemented · verified on devnet | The user broadcasts via `eth_sendRawTransaction`; the public `gasFee` is unshielded to `block.coinbase` through value conservation (the slot-2 fee note is left empty). The user **needs no BNB and exposes no EOA** , and this is also the censorship-resistance fallback. Native BNB transfers only (op=0). |
| **② Relayer** | implemented · main path | The relayer wraps the proofs in an ordinary signed transaction calling `transact()`, pays the BNB gas, and is compensated by the slot-2 private fee note (`gasFee=0`). Before packing, the relayer must decrypt the slot-2 ciphertext itself to confirm the fee is really payable to it. |
| **③ Paymaster** (ERC-4337) | planned | The user signs a UserOperation, a bundler submits it, and a paymaster pays and can perform complex token conversion — the bridge into the AA wallet ecosystem. |

### 1.9 End-to-end: Alice → Bob

The following is the full lifecycle that `e2e/cases/privacy-flow.sh` verifies on a real devnet (proofs are generated against live on-chain tree state, not fixed fixtures):

 ![fig-2](https://us1.discourse-cdn.com/flex020/uploads/binancesmartchain1/original/2X/6/6432e416f70fa6cb6aec18d88b76550069561d7e.png)

_fig 2 · End-to-end sequence: grey = public actions · gold = private/proof actions · purple = events/read-only; six stages cover keys → registration → deposit → transfer → discovery → withdrawal_

1. **Both sides keygen + register** — Alice and Bob each derive their keys and register `PrivacyInfo`; from then on they only need each other’s EOA address.

2. **Alice deposits 10 BNB** — `privDeposit` is a public inbound transfer; the note commitment is enqueued and the `NoteQueued` event carries the ciphertext immediately.

3. **Batch insertion (drain)** — validators issue the `deferBatchInsert` system call (triggered automatically every 50 blocks ≈ 10s; anyone may also trigger it earlier by paying) to insert queued notes into the commitment tree in batch, advancing one new root. The notes become spendable at that point.

4. **Alice → Bob private transfer** — Alice looks up Bob’s registry entry on-chain → encrypts out[0] to his `viewKeyPub` with ECIES → generates the pool proof locally (bound to the intent digest) → submits via a relayer or broadcasts `0x77` directly. On-chain, only two nullifiers and three new commitments are visible.

5. **Bob scans and discovers the note** — Bob trial-decrypts each newly emitted `outputNoteData` with `viewKey`; a passing AEAD tag means the note is his and simultaneously yields `(amount, noteSecret, onkHash)`. **Discovery is immediate** (the ciphertext is visible with the transact event); only spendability waits for the drain.

6. **Bob withdraws to a fresh EOA** — Bob derives the nullifier to nullify the note and withdraws to an address entirely unrelated to his history; the test asserts the balance arrived, the nullifier is burned, and a double-spend replay is rejected.

## 02 · Implementation: how contracts, circuits, and the node interlock

New L1’s circuits are **fully in-house** (circom 2.2.3 + snarkjs, Groth16/BN254); The solution has an independent domain-tag namespace `newl1.shielded.*`, an independent VK, and an independent ceremony. Bit-level agreement between contracts and circuits is frozen by shared test vectors gated in CI (`make verify-shielded-set`).

### 2.1 Note / nullifier: a five-level hash derivation chain

```plaintext

// all Poseidon2 t=4 with New L1 domain tags (PIN-3: tag = keccak256("newl1.shielded.") mod r)

ownerNullifierKeyHash = poseidon(OWNER_NK_HASH, ownerNullifierKey)

ownerCommitment = poseidon(OWNER_COMMITMENT, onkHash, noteSecret, authDataCommitment) // arity 4: last slot binds the EOA authorization commitment

noteBody = poseidon(NOTE_BODY, ownerCommitment, amount, tokenField, isOpen, scheme)

noteCommitment = poseidon(NOTE_COMMITMENT, noteBody, leafIndex)

nullifier = poseidon(NULLIFIER, noteCommitment, ownerNullifierKey)

```

Three design points are worth noting. **① `isOpen` and `scheme` are carved into the leaf** — open notes (public amount/asset) share one tree and one Merkle-proof path with standard notes, and the authorization scheme is frozen per note so rotation does not affect old notes. **② `noteSecret` is derived from the sender’s seed** (the decision after the audit fix): `poseidon(NOTE_SECRET, senderSeed, intentReplayId, outIndex)`; the recipient obtains it by ECDH decryption rather than recomputation, and `noteSecretSeedHash` is only a circuit anchor. **③ The nullifier binds `leafIndex`** (via noteCommitment), so two notes with the same amount and same owner still nullify to different values.

### 2.2 Note tree and deferred batched insertion

The commitment tree is a depth-32 incremental Merkle tree (nodes are `poseidon([l,r])`, the zero ladder is `z[i+1]=poseidon(z[i],z[i])`, and the empty root is the fixed PIN-6 constant written into genesis storage). Early synchronous insertion loaded every transact with 99 Poseidon2 permutations (~3.05M gas, 79% of the total); it is now **two-phase** :

```plaintext

User transact (block B):

verify both proofs · burn nullifiers · consume replayId · check the ciphertext-hash binding

→ the 3 output commitments are only enqueued (cheap SSTORE; queue index == future leafIndex)

System deferBatchInsert (block H, H % 50 == 0, validator post-execution system call):

FIFO batch insertion · shared upper Merkle path · each drain advances the root exactly once · paid from system gas

← permissionless: anyone may also call it early and pay (trustless; inserts only already-verified commitments; no-op on an empty queue)

```

The safety boundary is clear. **Double-spend protection is not deferred** (nullifiers are burned inside transact, immediately). The queue is ordinary contract storage, so on a reorg it rolls back and replays together with the transactions, and drain is deterministic on rebuild (pinned by the `shielded_reorg.rs` unit test). Equivalence between the batch algorithm and per-note append is proven by differential testing. Batching reduces the cost of K notes from `K×33` permutations to about `2K+32`.

### 2.3 Recent root history: a 64-slot ring buffer

A spend proof proves membership against _some historical root_, while the tree keeps advancing — so the contract maintains a `ROOT_HISTORY=64` ring buffer of roots, and `isKnownRoot` accepts any root inside that window. Since each drain records exactly one root, the window is 64 drains × 50 blocks × 200ms ≈ **3,200 blocks / 640 seconds** — more than 10 minutes of submission headroom after a proof is generated. The revocation semantics of an old authorization key are anchored here too: after rotation, the old key expires naturally as its root slides out of the window.

### 2.4 zkProof · pool proof (29 public signals)

The pool circuit establishes accounting correctness and spend authorization in one shot: 2-in-3-out conservation, dual nullifier derivation (real and phantom indistinguishable, `nf0≠nf1`), Merkle membership, and full-field binding to `transactionIntentDigest`. The `operationKind` constraint matrix enforces at the R1CS level that the three operations have mutually exclusive public surfaces (its absence would be a privilege-escalation bug):

| Public signal (PIN-4 order) | op=0 transfer | op=1 withdraw | op=2 atomicCall |
| --- | --- | --- | --- |
| publicAmountOut | == 0 | ≠ 0 | ≠ 0 |
| publicRecipient | == 0 | ≠ 0 | == 0 (a CREATE2 temporary address is used) |
| callTarget / callDataHash / reshield\* | all 0 | all 0 | callTarget ≥ 0x10000, etc. |
| gasFee (index 28, 0x77-only) | _all ops:_ the public fee unshielded to coinbase; always 0 on the relayer path; counted in conservation and folded into the intent digest | | |
| scheme | _all ops:_ must equal the scheme inside the spent leaf (prevents cross-scheme privilege escalation); all amounts pass Num2Bits(248) to prevent field wraparound | | |

`transactionIntentDigest = poseidon(INTENT, all public fields + feeAmount + reshieldNoteSecret + replayId + validUntil + chainId, …)` is recomputed inside the circuit and constrained to be equal — if a relayer tampers with any field (recipient, fee, calldata hash, ciphertext hash) the proof becomes invalid. Replay protection is threefold: `intentReplayId` (one-shot), `executionChainId` (cross-chain), `validUntilSeconds` (expiry).

### 2.5 zkProof · auth proof (the authorization side of the split proof)

The authorization design went through one explicit trade-off. The minimal reliable model is “ **authorization is the spend key** ” — the pool proof proves knowledge of onk and locks the intent, with no separate signature (the Zcash/Tornado lineage). On top of that it was upgraded to **EOA-reuse authorization** : the user adds no new key and signs EIP-712 with their existing Ethereum account key, which the circuit verifies:

```plaintext

// auth_ecdsa.circom — only 2 public signals: [blindedAuthCommitment, transactionIntentDigest]

1. digest712 = keccak256(0x1901 ‖ DOMAIN_SEPARATOR ‖ hashStruct(tid)) // keccak recomputed in-circuit, closing off a free-choice msghash

2. ECDSAVerifyNoPubkeyCheck(64,4)(r, s, digest712, pubkey) === 1 // circom-ecdsa

3. authDataCommitment = poseidon(AUTH_DATA, xHi, xLo, yHi, yLo)

4. blindedAuthCommitment = poseidon(BLINDED, authDataCommitment, blindingFactor) // fresh blinding per transaction → method-level unlinkability

```

**Owner-binding is the core of PIN-5** : folding `authDataCommitment` into `ownerCommitment` (arity 3→4) lets the pool proof establish “this note committed to this EOA” and the auth proof establish “this EOA signed this digest”, with the shared `blindedAuthCommitment` tying the two together — otherwise all you proved is that “somebody signed something”. The cost is that the pool circuit changed and a new ceremony is required. Wallet-side changes are zero: any standard wallet supporting `eth_signTypedData_v4` works (domain: `NewL1ShieldAuth/1`, verifyingContract = `0x5003`).

### 2.6 Note delivery: ECIES-secp256k1 ciphertext delivery

```plaintext

outputNoteData = version(1B) ‖ ephPubCompressed(33B) ‖ ChaCha20-Poly1305(key, plaintext)

plaintext = { amount, tokenField, ownerNullifierKeyHash, noteSecret, scheme, memo? }

shared = (ephScalar · viewKeyPub).x # sender side; = (viewKey · ephPub).x on the recipient side

okm = HKDF-SHA256(ikm=shared, salt=ephPub, info="newl1.shield.note.ecies.v2", 44)

key = okm[0..32], nonce = okm[32..44]

```

The recipient **trial-decrypts** every ciphertext — a passing AEAD tag means it is theirs (the tag _is_ the discovery oracle) — then recomputes the commitment to confirm membership in the tree. Integrity is doubly enforced: `outNoteDataHash0..2` are folded into the intent digest (circuit side), and `transact` additionally re-checks `keccak256(ciphertext) mod r` slot by slot (contract side, with empty slots submitting `keccak("")` for uniformity). A relayer therefore cannot swap ciphertexts to mount a “delivery DoS” (leaving the recipient permanently unable to discover their note). secp256k1 was chosen over X25519 because it is the same curve family as EVM wallets and because viewKey never enters a circuit, so the in-circuit secp256k1 constraint trap does not apply.

### 2.7 Poseidon2 and the 0x67 precompile

- `parameters (PIN-1)` · **Horizen BN254-t4 standard parameter set**

t=4, RF=8 full rounds + RP=56 partial rounds, length-tagged sponge (capacity initialised to `N<<64`, rate 3). The four implementations — circom, the JS mirror, Solidity, and the Rust precompile — agree bit for bit, pinned by 9/9 cross-implementation vector tests. The 14 domain tags do not collide across contexts.

- `precompile (0x67)` · **27,200 → 1,500 gas per permutation**

`crates/evm/src/precompiles/poseidon2.rs` exposes the bare t=4 permutation, active from genesis (same pattern as `0x66` BLS). `Poseidon2Sponge` keeps the sponge wrapper and staticcalls once per permutation — after deleting the inlined Solidity round loops, the deployed `ShieldedPool` bytecode halved from 23,090 to 11,924 bytes.

#### Predeploy layout (genesis, non-upgradeable, no admin)

| Address | Contents | Notes |
| --- | --- | --- |
| 0x5000 | **ShieldedPool** | The pool itself. It embeds the snarkjs-exported `PoolGroth16Verifier` by **inheritance** (the combined artifact is GPL-3.0) and compiles in the Poseidon2 hash library; genesis also writes the tree’s initial storage (32 levels of zero-subtree hashes, the seed root, `nextLeafIndex=0`) — so this is not a pure code predeploy |
| 0x5003 | **Auth verifier** | A separate staticcall dispatched by `scheme`; deploying it separately makes a future pq channel **additive** (swap in another verifier address and the pool hot path stays untouched) |
| 0x67 | **Poseidon2 precompile** | A consensus-layer change (all full nodes agree), Rust implementation |
| 0x1007 | GovHub (existing) | The only privileged caller, and only for `setDepositPaused`; withdraw/transfer/atomicCall can never be paused |

### 2.8 Native transaction type 0x77: the node side

The node side mirrors the existing native AA type `0x76` and its `NewL1TxEnvelope` mechanism exactly: `NEWL1_SHIELDED_TX_TYPE = 0x77` sits in New L1’s reserved high range, avoiding collisions with future Ethereum standard types (0x00–0x0f). The pipeline is `eth_sendRawTransaction` (EIP-2718 decoding) → mempool admission (`PooledKind::Shielded`, proof verified once and memoised in a `OnceLock`) → miner packing → executor `shielded_transact` (synthesised sender + `transact` calldata + in-EVM Groth16 verification) → receipt type `0x77`. The key divergences from AA: no account nonce (replay protection is nullifier + replayId), no balance deduction from the caller (the fee _is_ the public gasFee), and authorization is dual proofs rather than ecrecover.

### 2.9 Gas and fee mechanics

| Path | Fee form | Conservation equation |
| --- | --- | --- |
| **Relayer (②)** | A private fee note (slot 2); feeAmount is a private witness folded into the digest; the relayer only pays after decrypting and verifying it | Σin = out0 + out1 + feeNote |
| **0x77 Direct (①)** | The public `gasFee` (pub[28]) is paid by the pool directly via `_payNative(block.coinbase)`; the proof does not bind the coinbase address (the packer is unknown at proving time); the slot-2 fee note is set to a dummy | Σin = out0 + out1 + gasFee |
| **Underlying fee model** | _(applies chain-wide)_ New L1 charges **prepaid, by declared gas limit** (under asynchronous execution, blocks are packed against declared gas — what is sold is declared space): `gas_used == gas_limit`, no refund of unused gas, EIP-3529 refunds void; the base fee is burned and tips are collected to validators via SYSTEM\_ADDRESS. Wallets must set gas\_limit precisely | |

### 2.10 The responsibility boundary of a privacy relayer

A relayer is a pure execution agent, cryptographically stripped of every avenue for misbehaviour: it cannot change the recipient, amount, or ciphertexts (digest binding); it cannot replay (replayId); and it cannot point the fee note at someone else and then trick another relayer into packing the transaction (the circuit does not enforce slot-2 ownership, but the decrypt-and-verify step before packing means a proof that pays someone else finds no sponsor). On devnet, any funded EOA can act as a relayer (`transact` has no msg.sender authorization).

### 2.11 Open notes + atomic EVM calls

This is New L1’s largest capability increment over EIP-8182: completing _unshield → public DeFi call → reshield_ inside a single atomic transaction, instead of splitting the intermediate state into several linkable transactions. The difficulty is that the DeFi output amount is unknown at proving time — the solution is the **open note** (`isOpen=1`, a leaf with public amount/asset) plus contract-side delta measurement:

 ![fig-3](https://us1.discourse-cdn.com/flex020/uploads/binancesmartchain1/original/2X/7/72808455b3a2c171fe98cdcef2348fcc5db43af6.png)

_fig 3 · Open note + atomic EVM call: gold = steps in the private domain · white = steps in the public domain; when the open note from ⑥ is later spent it converts into a fully private standard note_

1. **Verification + accounting** — verify both proofs (conservation: `Σin = private outputs + publicAmountOut + fee`), run the known-root / nullifier / replay checks, burn the nullifiers, and enqueue the 3 private outputs.

2. **CREATE2 temporary forwarder** — derive a one-shot address salted with `transactionIntentDigest` (replayId guarantees uniqueness and prevents address-reuse residue) and inject `publicAmountOut` in native coin — the caller is isolated, and the pool itself is never the msg.sender of an external call.

3. **The proof-bound external call** — execute the calldata against `callTarget`, requiring `keccak256(callData) mod r == callDataHash` (already bound by the proof); `callTarget ≥ 0x10000` bluntly rejects the pool, the verifiers, system contracts, and precompiles.

4. **Delta measurement + full sweep-back** — snapshot `bal_before` in the same frame, then after the call `measured = bal_after − bal_before` ( **absolute balances are forbidden** , to prevent an external top-up from forging an open-note amount); the temporary address’s whole balance is swept back into the pool, and any force-fed surplus accrues to the protocol.

5. **Reshield the open note** — build `noteBody = poseidon(NOTE_BODY, reshieldOwnerCommitment, measured, token, isOpen=1, scheme)` and enqueue it (if `measured==0`, no leaf is inserted); `reshieldNoteSecret` comes from the initiator’s seed and is folded into the digest, so a relayer cannot rewrite ownership. The custody invariant is asserted alongside: `totalShielded += measured − publicAmountOut`, and no payment may overdraw.

6. **Failure semantics = whole-transaction revert** — if the external call reverts, the entire transaction reverts (no nullifier consumed, no leaf inserted, no replayId burned) — refusing the “refund the unshield into a reshield” path and closing off the ownerless-blinding-factor problem. All entrypoints share one reentrancy guard slot.

### 2.12 Wallet toolchain and note-discovery optimisation (newl1-shield)

The protocol deliberately provides no Merkle-path RPC — wallets rebuild all state from the event stream and nodes stay stateless services. The reference implementation is the `newl1-shield` CLI (JS proving toolchain + Rust wrapper):

| Command | Responsibility |
| --- | --- |
| keygen / address / register | Derive the three keys from the seed, print the privacy address (viewKeyPub), register PrivacyInfo on-chain |
| synctree / scan | Replay `NoteQueued(seq, outputNoteData)` events to rebuild the commitment tree (seq == leafIndex, i.e. the queue index is the leaf position); trial-decrypt with viewKey to discover your own notes and persist a local note store |
| prove-spend | Build the witness against **live on-chain tree state** (real Merkle paths) → generate the pool + auth proofs with snarkjs; `--ond` requires the ciphertext to be fixed at proving time (its hash enters the digest) |
| wallet / status / inspect-tx | Local balance with the pending/spendable split; on-chain root / totalShielded / queue depth; decode the 29 public signals of any privacy transaction |

**Scanning optimisation** : a view tag (the leading bytes of the ephemeral public key) provides a cheap pre-filter, and full AEAD decryption runs only on hits; incremental scans look only at new commitments. The event design guarantees the ciphertext is visible from the moment of enqueueing — discovery never waits for a drain.

---

<div class="post-metadata">

### Author: ![galaio](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.bnbchain.org/galaio/32/580_2.png) [@galaio](https://forum.bnbchain.org/u/galaio)
#### Post date: [July 28, 2026, 1:38pm UTC](https://forum.bnbchain.org/t/introducing-the-new-l1-native-privacy-layer/5481/2 "2026-07-28T13:38:51Z")

</div>

## 03 · Key metrics: measurements and capacity projections

All gas figures below are Foundry `gasleft` measurements in-repo or live results from the 4-validator devnet; proving times and TPS are projections derived from constraint counts and chain parameters, with the assumptions stated item by item.

### 3.1 User-side operation gas (before vs. after deferred insertion)

| Operation | Synchronous insertion (old) | Deferred insertion (current, measured) | Notes |
| --- | --- | --- | --- |
| **privDeposit** (shield) | ~3,450,000 | **151,951** | Enqueue + event only; ~22.7× reduction |
| **transfer** (transact op=0) | ~3,820,000 | **775,859** | Approaching the dual-Groth16 verification floor (~810k); ~4.9× reduction |
| **withdraw** (transact op=1) | ~3,850,000 | **843,000** | Includes real ECDSA auth verification; the two Groth16 pairing checks account for ~810k (96%) |
| **atomicCall** (transact op=2) | — | **~1,010,000** | The withdraw baseline + CREATE2 forwarder + target call + reshield enqueue (measured against a mock DEX) |
| pool `verifyProof` (isolated) | — | **402,506** | 29-signal Groth16 verification; auth verification accounts for roughly the other half of on-chain verification gas |

In the synchronous-insertion era, 79% of transact gas (~3.05M, 99 Poseidon2 permutations) went to on-chain Merkle maintenance rather than ZK verification (10%) — deferred insertion hands that whole share over to the system.

### 3.2 System-side batch insertion (drain) cost

| Scenario | Gas | Notes |
| --- | --- | --- |
| deferBatchInsert · one at a time (Phase A) | 4,018,915 / note | A loop over `_insert`; a correctness baseline only |
| Batch · 3 notes (A.2) | ~1,350,000 | Shared upper path |
| Batch · 256 notes (A.2, Solidity Poseidon) | 17,844,180 | = 89% of the 20M system gas hard cap (the basis for DRAIN\_CHUNK\_CAP=256) |
| Marginal cost / note · Solidity Poseidon | 57,300 | Fitted as ≈ 3.17M fixed + 57.3k/note |
| Marginal cost / note · **0x67 precompile** | **10,670** | ~5.4× lower; a single permutation drops from 27.2k to 1,500 gas |

### 3.3 ZK proving metrics (measured: snarkjs/WASM and rapidsnark)

| Metric | pool | auth (ECDSA-in-circuit) |
| --- | --- | --- |
| Constraint count | 70,355 | 2,121,764 (~30×) |
| Proof size (fixed for Groth16) | 256 B | 256 B (512 B total per transact) |
| Witness generation (WASM, measured) | 0.13 s | 147.6 s |
| Groth16 prove · snarkjs (measured) | 3.10 s | 75.0 s |
| Groth16 prove · **rapidsnark** (measured) | 0.46 s (6.7×) | 10.17 s (7.4×) |
| Witness generation · native C++ (estimated) | ~0.02 s | ~8–15 s |
| On-chain verification gas | 402,506 | ~400k (~810k combined, i.e. 96% of withdraw’s 843k) |
| Total proving time per transaction | Total | Composition |
| -– | -– | -– |
| snarkjs + WASM (today’s dev toolchain, measured) | ~226 s (~3.8 min) | 147.6s WASM witgen + 75s prove, ~98% of it in the auth circuit |
| rapidsnark + WASM witgen (measured) | ~158 s | Proving is already 7× faster; WASM witness generation becomes the new bottleneck |
| **rapidsnark + native witgen** (estimated) | **~20–25 s** | The realistic shape of a production toolchain; with further parallelism/GPU, ~5–20 s per transaction is plausible |

On-chain verification is constant time (one pairing equation plus an MSM over the public signals; milliseconds natively on a node). The dev ceremony’s zkey is forgeable but **does not affect** gas, proof size, or constraint count — a mainnet ceremony only replaces the trust root, and proving time varies only with the prover implementation. Roadmap comparison: a Honk auth circuit is ~2M gas with a 16–24 KB proof (no per-circuit ceremony); wrapping Honk→Groth16 compresses it back to ~260 B / ~270k gas.

### 3.4 Throughput (TPS) projection with the current optimisations (deferred batching + the 0x67 precompile)

Chain parameters: block gas limit **150M** , block interval **200ms** (5 blocks/s). With deferred insertion and the Poseidon2 precompile both live, chain-level privacy throughput is bounded by user transaction gas:

| Basis | Derivation | TPS |
| --- | --- | --- |
| withdraw theoretical ceiling (100% privacy blocks) | 150M ÷ 843k ≈ 178 tx/block × 5 | ~890 |
| atomicCall theoretical ceiling | 150M ÷ 1.01M ≈ 148 tx/block × 5 | ~740 |
| **Including amortised drain insertion cost** (~+400k amortised per output note inserted) | effective ~1.25M/tx → 150M ÷ 1.25M × 5 | **~600** |

### 3.5 Latency and windows

- `note discovery` · **Immediate**

The ciphertext is visible in the same block as the transact event; the recipient can decrypt the amount and secret right away.

- `note spendability` · **~5s average / ~10s worst case**

Waits for the next 50-block drain boundary to enter the tree; when chunked under congestion, one further cycle.

- `proof submission window` · **≈ 640s**

ROOT\_HISTORY(64) × 50 blocks × 200ms — over 10 minutes of headroom after a proof is generated.

## 04 · Future: PQ, recursion, a compliance hook, and scenario evolution

Every “reserved seam” in the current implementation exists so that later capabilities can be **added additively, with zero note-state migration** : tokenField is reserved for ERC20, scheme for PQ, complianceHook for compliance, and open notes for DeFi composability.

### 4.1 Post-quantum (PQ) migration path

PQ is not a rebuild from scratch but incremental progress along three seams that are already in place:

1. **The authorization channel: scheme=1 is already carved into the leaf** — `scheme ∈ {ecdsa=0, pq=1}` is bound inside the note leaf and the circuit enforces `publicScheme == note.scheme`, so shipping PQ only requires deploying a new auth verifier (the staticcall address dispatched by scheme); existing notes and the pool hot path do not move at all. The concrete algorithm, encapsulation, and proof system are left to later design (candidate directions align with the BNB Chain mainnet PQ roadmap: Falcon-512 for its small on-chain signature size, ML-DSA as the conservative option; on the ZK side, the circuit-friendliness of hash-based signatures can be evaluated).

2. **Key rotation semantics: an escape hatch prepared for “Q-day”** — the registry design already embeds the migration mechanism: `onkHash` is immutable, which keeps the nullifier domain stable, while `authScheme` is rotatable — a user can actively switch authorization from ecdsa to pq, the old authorization expires naturally as its root slides out of the 64-slot history window (≈640s), and old notes remain spendable (scheme is frozen with the leaf). This shares the same “multi-signature-scheme dispatch” engineering as New L1’s native AA (`0x76` already supports secp256k1 / P256 / WebAuthn plus keychain authorization rotation).

3. **PQ-ifying note-delivery encryption** — the ECDH in today’s ECIES-over-secp256k1 is the Shor-vulnerable surface (record the ciphertext, decrypt it later, and historical amounts and secrets leak). The leading version byte of `outputNoteData` is exactly the upgrade switch reserved for this: `v3` can switch to ML-KEM (or a hybrid X25519+ML-KEM KEM) encapsulation — a purely off-chain change that touches neither the circuits nor the ceremony, requiring only a new KEM public-key field in wallets and the registry. This is the highest benefit/cost step of the three and the one that can ship first (“harvest-now-decrypt-later” is the most realistic quantum threat to a privacy pool).

### 4.2 ZK recursion: client proof + zkVM aggregation

The target architecture is two-layer proving: every user produces one proof for their own transaction on the client, and a **zkVM aggregates a batch of user proofs into a single proof that the chain verifies once**. The client-side proof system changes across two phases; the aggregation layer keeps the same shape in both. Until that layer lands, near-term prover work stands as measured before (rapidsnark plus native witness generation, ~20–25 s per transaction).

- `phase 1 · pre-quantum` · **Client Groth16 → zkVM aggregation → one on-chain verification**

The client keeps today’s Groth16/BN254 pool + auth proofs (256 B each), so wallets and the proving toolchain are untouched. An aggregator collects the proofs of N transactions and, inside a zkVM, verifies all N against the frozen verification keys and binds each proof to its own public signals. The zkVM’s STARK is then wrapped into a single Groth16/PLONK proof, and the chain performs one pairing check plus N sets of public inputs, through a new `transactBatch` entrypoint — the per-transaction `transact` path stays as it is. On gas: the ~810k of dual-pairing verification per transaction collapses into one fixed cost per batch, but the N nullifiers, commitments, recipients, and ciphertext hashes still have to reach the chain, so **calldata and storage do not amortise — only verification does**.

The natural aggregator is the existing relayer (mode ②): it already collects transactions, already pays gas, and is already compensated by the slot-2 private fee note, so no new economic actor has to be introduced.

- `phase 2 · post-quantum` · **Client STARK → zkVM aggregation → one on-chain verification**

The client side moves off Groth16 to a hash-based STARK (a zkVM guest program, or a Plonky3-class prover), which removes both the per-circuit trusted setup and the BN254 discrete-log assumption from the client. Proof size grows from 256 B to tens or hundreds of KB, which is acceptable precisely because a user proof travels to the aggregator off-chain and never enters a block. Aggregation then becomes recursive STARK verification — the same pipeline with the inner verifier swapped.

### 4.3 Compliance hook and full governance (planned)

The key design ruling is to **abandon “splitting the anonymity set by asset”** : tokens are hidden by default and the anonymity set is not fragmented per asset; compliance is instead enforced by a **revocable compliance hook** at three chokepoints — shield, unshield, and transfer. The `_complianceHook()` no-op seam is already in place, so integration requires zero migration:

| Mechanism | Design |
| --- | --- |
| Per-asset compliance verifier | `mapping(token ⇒ verifier)`, set by governance, pluggable, and **setting it to 0 revokes with immediate effect** (loosening a restriction needs no timelock) — a credible-neutrality guarantee in engineering terms; `complianceNullifier` prevents proof reuse |
| Priority | **OFAC non-membership proofs first** (the highest priority in regulatory dialogue), then zkKYC and source-of-funds proofs |
| Guard the entrance, never the exit | Compliance checks apply only at deposit / reshield; **withdraw is always open and governance can never touch funds already in the pool** — the engineering answer to how Tornado Cash ended |
| Governance switches | 11/21 validator multisig, public on-chain: `globalDepositEnabled` (emergency, no timelock) / `globalShieldEnabled` (24h) / `tokenDepositEnabled[t]` (24h); during a timelock window users can see the pending change and have time to unshield out of the way |

### 4.4 ERC20 / BEP-20 extension

`tokenField` has been inside the note commitment since day one (currently pinned to 0 = native coin), so ERC20 support requires migrating no note state: the deposit side switches to `transferFrom` plus per-asset compliance checks, the token stays hidden for in-pool transfers (conservation already forces all real inputs and outputs to share one tokenField), and the token is exposed in only two places — open notes and the native-transfer-specific transaction type. One boundary to note: **`0x77` with its public gasFee is native-BNB only** — private ERC20 transfers go through the relayer’s private fee note or the paymaster path, so that a fee asset does not tear the anonymity set open.

### 4.5 Future scenarios

- **Privacy demo (full devnet flow)**

`e2e/run.sh privacy-flow`: keygen → register → shield → private transfer → scan/discover → unshield, generating proofs against live on-chain tree state; `shielded-native.sh` validates the whole `0x77` pipeline (decode → mempool → packing → execution → coinbase receives the gasFee → state roots agree across full nodes). This is the minimum demonstrable product form.

- **Privacy transfer**

Everyday private payments — payroll, B2B settlement, personal transfers — with amount, asset, and both parties hidden; the recipient needs only an EOA address, funds are re-spendable within 10 seconds, and fees can be sponsored by a relayer for “send and receive with zero BNB on hand”.

- **Privacy swap**

atomicCall wired straight into an AMM: unshield → swap on PancakeSwap → atomically reshield the output back into the pool as an open note, which becomes fully private when next spent. Externally one sees only “the pool made a swap”, not who made it — trading strategy and position adjustments no longer run in the clear. MEV/sandwich protection and a callTarget allowlist are audit surfaces to settle before mainnet.

- **Privacy loan**

The same atomicCall primitive composed with lending protocols: unshield a private position into a Venus-style market as collateral → borrow and take the asset back into the pool as an open note; position health stays public and liquidatable while the position’s owner stays hidden. Further, zk credit/compliance proofs can be layered on (reusing the compliance hook) toward private credit lending.

---

<div class="post-metadata">

### Author: ![cryptomanxxx](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.bnbchain.org/cryptomanxxx/32/5240_2.png) [@cryptomanxxx](https://forum.bnbchain.org/u/cryptomanxxx)
#### Post date: [August 22, 2026, 8:48pm UTC](https://forum.bnbchain.org/t/introducing-the-new-l1-native-privacy-layer/5481/3 "2026-08-22T20:48:39Z")

</div>

The privacy layer for the new L1 AI agent BNB blockchain sounds cool but I am more interested in if the new L1 AI agent BNB blockchain will have a common yield layer?
