Relayers
Private operations — unshield and private_transfer — are submitted as
unsigned Substrate extrinsics. There is no signing account, so there is no
public address to correlate with the private operation.
The fee is not a gas payment. It is a value committed inside the ZK proof as a public input and deducted from the user's note, which is what lets someone with no public balance spend privately.
There is no relayer to be
Every active validator relays. Including a fee-bearing unsigned extrinsic is ordinary Aura block authorship — there is no relay service, no endpoint to run, and no extra process on the node.
What a validator does need is an EVM address registered on-chain, so the chain knows where to credit the fee. That is one signed extrinsic, done once — see Relay Setup and Rewards.
A validator without one still authors blocks and still relays. The fee simply goes to the block author by the fallback below — which, often enough, is somebody else.
Who gets credited
The chain takes the fee recipient from the dispatch origin, never from
calldata. That is the only place it cannot be forged: a relayer field would be
an unauthenticated claim, and anyone could take a propagated proof, resubmit it
naming themselves, and collect a fee they never paid for.
Three shapes, all either authenticated or deliberately anonymous:
| Origin | Credited | Why |
|---|---|---|
Relayed(addr) | that address | EVM precompile — addr signed the transaction |
Signed(who) | who's registered address | the signature proves who submitted |
None | the block author | unsigned; nobody is named |
The third row is the common case, and the second has a wrinkle worth knowing:
A signed submitter with no registered address resolves to "nobody" rather than failing, and the fee falls back to the block author. Relaying is deliberately not gated on registration — failing here would reject a user's transaction over someone else's misconfiguration.
Watching where fees go
Two events exist specifically so that attribution is auditable:
| Event | Fires when | Why you care |
|---|---|---|
RelayFeeDiverted { requested, credited, asset_id, amount } | the call named an address that resolves to no registered relayer | This is where your missing fees went. requested is the address that was named, credited is who actually received it |
SelfRelayedFee { author, asset_id, amount } | an authenticated relayer resolved and turned out to be the block author | Legitimate on its own — with N authors in rotation it happens about 1/N of the time |
SelfRelayedFee is published because it is the only on-chain trace of the one
attack origin-based attribution does not prevent: an author can ignore its own
pool ordering and include a copy of another node's submission pointed at itself.
A single event proves nothing. A rate sustained well above 1/N across many
sessions does, and the response is off-chain — validatorSet.removeValidator.
The plain fallback — no relayer named, fee to the author — does not emit
SelfRelayedFee. It happens on every unrelayed call and carries no claim about
who relayed anything, so reporting it would bury the signal.
Where this lives
| Component | Responsibility |
|---|---|
pallet-shielded-pool | verifies the proof, moves the tokens, attributes the fee |
pallet-relayer | the address registry and the pending-fee ledger |
pallet-zk-verifier | holds the verification keys the proof is checked against |
ShieldedPoolPrecompile | the EVM entry point — see precompile reference |
Next Steps
- Gasless Fees — how the circuit forces the fee to be honest
- Fee Lifecycle — where fees accumulate, and how they are claimed
- Relay Setup and Rewards — register an address and collect