Skip to main content

How the Relay Flow Works

A gasless private transaction travels through three stages before reaching finality. There is no relay service or intermediary: the user generates the proof, submits the unsigned extrinsic directly, and the block author (an Aura validator) includes it and receives the embedded fee.


Stage overview


Stage 1 — User wallet: proof generation

The user's wallet (or SDK) generates the ZK proof off-chain. The fee is included as a public input signal at this point — it becomes part of the arithmetic constraints that the circuit enforces.

1
Select a fee

The wallet chooses a fee value that meets or exceeds MinRelayFee. This is a planck-denominated value deducted from the note's total amount.

2
Generate the Groth16 proof

The wallet calls the circuit's WASM binary with the note's private inputs and the fee as a public input. The resulting proof is only valid for those exact public signals — including the fee value.

3
Encode calldata

The wallet ABI-encodes the proof and all public signals into EVM calldata for the ShieldedPool precompile selector (unshield or privateTransfer).

The circuit-level constraints that enforce the fee:

// Unshield circuit:
note_value === amount + fee

// Transfer circuit (2 inputs, 2 outputs):
input_values[0] + input_values[1] === output_values[0] + output_values[1] + fee

Both circuits also apply Num2Bits(128) to fee, preventing field-overflow attacks.


Stage 2 — Direct submission as unsigned extrinsic

The wallet submits the extrinsic directly to the Substrate node. No relay service, no HTTP intermediary. The extrinsic uses ensure_none — it carries no signing account.

The SDK encodes the proof and all public signals into the extrinsic call data and sends it via the standard Substrate RPC (author_submitExtrinsic or the SCALE-encoded equivalent).

Fee immutability

The fee is a public signal inside the ZK proof. It cannot be modified after proof generation — any alteration causes on-chain verification to fail. The node accepts the extrinsic as-is.


Stage 3 — pallet-shielded-pool: execution

pallet-shielded-pool receives the unsigned extrinsic and processes it:

A
Pre-verification checks

Asset registered, Merkle root is in HistoricRoots, nullifiers not yet spent, fee ≥ MinRelayFee. These run before the expensive proof verification.

B
ZK proof verification

pallet-zk-verifier verifies the Groth16 proof against the active verification key for the circuit. Public signals include [merkle_root, nullifier(s), amount/commitments, asset_id, fee, relayer_h160].

C
State changes

Nullifiers are marked spent, output commitments are inserted into the Merkle tree, and (for unshield) tokens are transferred to the recipient's public account.

D
Fee attribution

The pallet identifies the current block author's AccountId and calls T::Relayer::accumulate_relay_fee(block_author, asset_id, fee). This increments PendingRelayerFees[AccountId][asset_id]. The fee tokens remain physically inside pool_account_id — only the accounting counter changes.


What gets written to chain

After a successful unshield relay:

Storage itemChange
NullifiersSpent nullifier added
PoolBalance[asset_id]Decremented by amount (fee stays in pool)
PendingRelayerFees[AccountId][asset_id]Incremented by fee
Recipient's public balanceIncremented by amount

After a successful private_transfer relay:

Storage itemChange
NullifiersBoth input nullifiers added
MerkleLeavesTwo new commitments inserted
MerkleRootUpdated
PendingRelayerFees[AccountId][asset_id]Incremented by fee

Security properties

Fee is cryptographically bound to the proof

No one can modify the fee after the proof is generated — the altered extrinsic would fail on-chain verification.

Validators cannot spend the user's funds

The validator includes the transaction but does not control the private note. Spending requires knowledge of the note's spending key, which never leaves the user's wallet.

The user's identity is not revealed by submission

The extrinsic is unsigned — there is no from-address. The proof's private inputs (sender identity, note contents) remain hidden inside the ZK circuit.