Skip to main content

ADR: Relayer-Assisted Merge

ADR — Relayer Merge as an Alternative to IMT for Concealing Stake Count

StatusProposed

Problem

Current L3/staking has very limited mixing capabilities if implemented as regular shielded pool without some additional exotic logic, as seen in the following usage scenario: a merchant receives payments from many clients via L3 stakes. When the merchant withdraws, the number of nullifiers consumed reveals how many clients they have. The system needs to hide this count while allowing efficient bulk withdrawal.

Motivating example: A merchant with 100 clients receives 100 individual stakes. At withdrawal time, the merchant wants to unstake all funds without revealing that they had ~100 separate deposits.

Current solution

Use an Indexed Merkle Tree (IMT) to conceal the number of nullifiers. The unstake circuit proves non-membership and insertion against the IMT root. Only old_root → new_root is visible on-chain — the number of nullifiers is hidden inside the root transition.

Problems with IMT:

  1. Limited batch size. Works up to MAX_N nullifiers per TX (currently 8). Above that, the merchant needs multiple transactions, leaking a rough stake count (e.g., 13 TXs × 8 = ~100 clients). Recursive proving could lift this limit but adds significant client-side complexity.
  2. Catastrophic state loss. The relayer is the sole maintainer of the nullifier tree. If it loses the database, there is no recovery path — nullifiers are private and never appear on-chain. All staked funds become permanently locked. The existing fullResync recovery path is broken (it parses ciphertext fields as nullifiers) and there is no easy way to fix it.
  3. Serialization bottleneck. The contract enforces old_nullifier_root == current_root. Only one unstake TX can land per block. Every concurrent proof is invalidated and must be regenerated (~28s proving time wasted). The relayer must either serialize requests (throughput bottleneck) or let users race (wasted work).

Proposed solution

Replace the IMT with a more classical shielded pool with relayer-assisted note merge pattern, using a flat on-chain nullifier set (mapping(bytes32 => bool)) instead of a Merkle tree, allowing contract to perform the spent check directly (O(1) lookup) — no tree witnesses, no circuit involvement for non-membership proofs.

Note structure change

Add the holder's public key (PK) and an encrypted balance to the stake commitment:

// Current:  hash2(amount, blinding)
// Proposed: hash(R_x, R_y, S_x, S_y, blinding, PK_x, PK_y)
// where (R, S) = ElGamal(amount, PK) — encrypted to holder's PK
  • PK binding means the note is no longer a bearer instrument. Only the PK owner (who knows SK) can unstake.
  • Encrypted balance means the relayer handles ciphertexts opaquely — it never learns the amounts. ElGamal on Grumpkin supports additive homomorphism natively.

Three operations

Stake (client → note): Client encrypts amount to merchant's PK, creates commitment, deducts from L2 balance. Circuit cost: similar to current — ElGamal encryption already present. Client shares the full note contents with the relayer: (ciphertext (R, S), blinding, leafIndex, PK). The relayer needs all of this to verify notes against the stake tree and perform merges, but critically does not learn the plaintext amount (the ciphertext is opaque without SK). If the ciphertext is emitted in the on-chain Staked event, the client only needs to share (blinding, leafIndex) out-of-band — the relayer reads the rest from chain data.

Merge (relayer, no SK required): Consumes up to MAX_N notes with the same PK, outputs one note with the same PK and homomorphically aggregated ciphertext. Requires only knowledge of note contents (ciphertext, blinding, leafIndex), not SK. The merge circuit:

  • Verifies each input commitment against the stake tree
  • Computes nullifiers and publishes them (flat mapping, no IMT)
  • Performs ElGamal homomorphic addition: R_out = ΣR_i, S_out = ΣS_i — this is ~5-10 constraints per EC point addition on Grumpkin, trivially cheap inside a BN254 circuit
  • Outputs a new commitment with the aggregated ciphertext and same PK

Unstake (merchant, requires SK): Consumes the single merged note. Merchant decrypts with SK, proves correct decryption, credits L2 balance. One proof, one nullifier — indistinguishable from any other single-note unstake.

Relayer workflow

  1. Clients pay by staking notes encrypted to the merchant's PK
  2. Relayer learns note metadata (ciphertext, blinding, leafIndex) — but not plaintext amounts
  3. Relayer periodically merges notes in the background (merchant doesn't need to be online)
  4. When the merchant wants to withdraw, they query the relayer for their current merged note and unstake it
  5. Alternatively: with each merge, the relayer publishes the new note data on-chain (encrypted to the merchant's PK), removing the need for the merchant to query the relayer at all

Nullifier handling

Nullifiers are published on-chain and checked against a simple mapping(bytes32 => bool). No IMT, no tree witnesses, no relayer state dependency. The merge TX reveals the number of notes being merged, but this is a relayer operation — it doesn't reveal whose notes are being merged or the amounts. The merchant's final unstake always consumes exactly 1 note.

Tradeoffs

ConcernIMT (current)Relayer merge (proposed)
Nullifier count privacyHidden in root transitionMerge count visible, but only to relayer operation level; final unstake always = 1
Relayer state lossCatastrophic — all funds lockedRecoverable — nullifiers on-chain, notes reconstructable
Serialization / race conditionOne TX per blockFully parallelizable
Relayer learns amountsYes (plaintext preimages)No (encrypted ciphertexts only)
Relayer can stealYes (bearer instrument)No (PK-bound, needs SK)
Relayer can block withdrawalYes (controls witnesses)Only if note data not published on-chain; solvable by publishing encrypted notes
Relayer knows note ownershipYes (serves per-user witnesses)Yes (knows which notes share a PK)
Circuit complexity (merge)N/ACheap — EC point addition on Grumpkin is ~70-140 constraints for 8 notes
Circuit complexity (unstake)Expensive — IMT non-membership + insertion proofsCheap — single note, no tree proofs
Client proving time~28s (IMT proofs dominate)~2-5s (no IMT)
On-chain cost2 field elements (roots)MAX_N nullifiers × 32 bytes; on L2 (Base) this is < $0.01

Possible Extensions

Hiding Merges Among Regular Transfers

It is possible to design a single circuit used for both merges and private transfers (within pools, between peers), making merges indistinguishable from transfers. The idea is as follows:

  1. The circuit always takes two input notes and produces two output notes. Either an input or an output note may be a dummy — slightly wasteful, but acceptable.
  2. Input notes must share the same PK. Spending rights are authorized by checking whether an appropriate signature (signed with SK) is provided:
    • If a signature is provided, the output notes may have an arbitrary receiver_PK.
    • If no signature is provided (replaced by a dummy), then receiver_PK must match the PK from the input notes (this is the merge operation). It can also be enforced that the second output note must be a dummy in this case.

For regular transfers, the user signs as normal. For merges, no signature is required — but only the merge operation is then permitted.