Protocol
Fees and relayers

Relayers

Why relayers exist, what a relayer sees, what it can and cannot do to your transaction, how it is paid, and the policy Umbra's own relayer applies.

A relayer submits your transaction to Solana for you and pays its SOL fee, so the wallet that lands a private transfer or a withdrawal is the relayer's, not yours. A relayer cannot change what the transaction does. It can refuse to submit it.

Why they exist

A private transfer spends notes and creates notes without moving any public token balance. If you submitted it from your own wallet, your wallet address would sit on every one of your private transactions, and anyone could count them and time them. A relayer breaks that link: the chain shows only the relayer as the signer and fee payer.

A value transaction names its submitter when it is built: the context digest binds the submitter's address, so only that wallet can land it. Any wallet can be the submitter, your own included, so a relayer is a privacy and convenience service, not a gatekeeper. If one does not send, you can build the transaction again for another relayer or send it yourself.

What a relayer sees

The relayer receives the finished transaction: its public payload, its zero-knowledge proof and the combined attester signature. From that it learns the transaction's public values (deposit and withdrawal addresses, tokens and amounts, nullifiers, the tip) and the moment you asked for it to be sent. It also sees the network address you sent it from. It never sees who owns the notes or how much moves between them, because those are inside the proof.

What a relayer cannot change

The context digest is a Keccak-256 hash of the payload's public part and every account address the instruction names: 39 addresses, from the submitter and the trees to the pool token accounts and each lane's depositor and destination. The attesters sign that digest, and the proof commits to it. A relayer that changed a recipient, an amount or any of those accounts would produce a different digest, so the combined attester signature and the proof would no longer verify.

Only what the relayer fills in stays outside the digest: the nullifier-freshness proof, which it makes itself against the nullifier root current when it submits, and the sequence number that names that root. The shielded pool checks that proof on its own. See nullifier freshness.

What a relayer can do is decide whether, and when, to send. It can hold a transaction back until its proof is too old to be accepted.

How a relayer is paid

A transaction can name a tip for its submitter, in one token. The tip's amount and token are public inputs to the proof. The program credits the tip to an accrual account derived from the submitter's address and the token, and the relayer collects its accrued tips later.

The accrual account's address is in the digest too, and when a tip is paid the program checks that the account belongs to the submitter that signed. The digest binds the submitter itself, tip or no tip, so a relayer that copied a transaction and sent it itself would fail the combined attester signature and the proof.

Umbra's relayer

Umbra runs a relayer service. The policy below is what that service does, and other relayers need not share it. The chart shows the path a relayed transaction takes through it.

sequenceDiagram
participant U as Your device
participant R as Relayer
participant N as Nullifier service
participant S as Solana
U->>R: finished transaction
R->>R: policy checks
R->>S: read accounts the transaction names
R->>S: simulate with a placeholder freshness proof
R->>N: non-membership witnesses
R->>R: nullifier-freshness proof
R->>S: simulate, then send
R-->>U: outcome

It checks its own policy first

The relayer refuses, before reading the chain, any transaction that:

  • deposits. A deposit needs the depositor's own signature over a blockhash the relayer would choose, so the relayer cannot carry it. You submit deposits yourself.
  • spends a program-owned note. The service is configured to refuse these.
  • names another submitter. The transaction must name the relayer's own submitter address for that pool in the submitter slot.
  • moves a token outside its list. Every token in the transaction's token table must be on the relayer's list of relayable tokens.
  • pays no tip, or too little. By default it refuses a zero tip because the relayer still pays the Solana fee. Sponsorship can allow a zero tip; the chosen submitter remains bound and screened. A paid tip must be in a token the relayer accepts as payment, meet that token's published minimum, and be credited to the accrual account the relayer has set up for that token.
  • is out of date. The transaction's execution slot must not be ahead of the live slot and must leave enough of the proof-age window to land.

It also recomputes the context digest from the payload and accounts and refuses the transaction if the digest differs from the one the client proved against.

It checks the transaction against the chain

The relayer then reads every account the transaction names and refuses it if the program would. It checks the guardian cluster, the screener and the fee enclave; that the commitment and identity roots the transaction names are still recent and the zone-signer root is the live one, holding the transaction for a retry while the zone registry waits for a rebuild; that the trees have room; the token accounts and token programs; the fee policy; and that the pool token accounts, and for SOL the lamport account above its rent reserve, hold enough to pay out. This is so the relayer does not pay to send a transaction that will fail.

Last, the relayer has the program check the transaction as far as it can before it spends anything on it. It simulates each withdrawal on its own with a placeholder freshness proof: a well-formed proof of an unrelated statement. The program verifies the proof, the freshness proof and the combined attester signature together, in one pairing, so no simulation can show that the signature or the proof holds on its own. A simulation that fails only at that pairing shows that everything before it passed: the accounts, the roots, and that both proofs and the signature are well formed. The relayer refuses a transaction whose proof or attester signature the program rejects as malformed before it makes a freshness proof for it. A well-formed forgery costs it one freshness proof and is refused when the relayer simulates the finished transaction, and the program stays the only judge of both.

A withdrawal can pay an associated token account that does not exist yet. The client names the owner, and the relayer creates the account in the same Solana transaction, but only if it is exactly that owner's associated token account for the token. Each account it creates raises the minimum tip by a per-token creation fee the relayer publishes.

It completes and sends the transaction

The relayer asks an off-chain copy of the nullifier tree for non-membership witnesses for the transaction's nullifiers, checks that they prove absence under the copy's current root, and makes the nullifier-freshness proof against that root. The pool accepts any nullifier root still in its recent history, so the copy's root can be newer than the one the relayer read when it checked the transaction; where the two name the same point in that history they must agree. A nullifier that is already in the tree means the notes are spent, and the relayer refuses the transaction. It then builds the Solana transaction and simulates it to size its compute budget: it refuses a transaction the simulation says would use more than 1,350,000 compute units, and otherwise sets the limit to 1.2 times the simulated amount plus 50,000, at most 1,400,000. It signs the transaction as submitter and fee payer, and refuses to sign a priority fee above its highest bid at that compute limit. It then simulates the transaction again and sends it. Until the transaction lands or its proof expires, the relayer sends the same signed transaction again.

Several withdrawals in one transaction

One relayed transaction can carry up to two withdrawals and, after them, instructions the client builds and signs itself. The relayer admits a short list and refuses anything else: the associated token account creations it builds for the withdrawals, the withdrawals themselves, and the two instructions that register a new key, the user registry's bind_platform_wallet and the pool's register. They always run in that order, so a client instruction can use what the withdrawals paid out.

Each withdrawal is checked as it would be alone, and then the withdrawals are checked together. Two withdrawals that each fit a pool token account can still overdraw it, so their payouts are added up per pool token account before the relayer accepts them.

Client instructions add signatures and compute the relayer pays for, so a fee-paying relay pays a per-token surcharge for each one on top of the minimum tip; the relayer publishes it beside the minimum, and takes no client instructions paid in a token it publishes none for.

The relayer's key signs only as the fee payer and for the account creations it built. A client-built instruction may not name the relayer's address in any role. A program can move SOL out of an account only through an instruction that names that account as a signer, so no client instruction can reach the relayer's balance.

Signing in two steps

The client's keys sign the whole transaction, including the blockhash and the nullifier-freshness proof the relayer adds, so a transaction with client instructions cannot be signed before the relayer has built it. It takes two requests. The relayer answers the first with the message to sign once it has built it, and sends the transaction after the client posts its signatures. Each signature must come from one of the keys the client named and must verify over that message.

A relay waiting for signatures has already cost the relayer its nullifier-freshness proofs, so the relayer bounds the wait. It holds such a relay for a configured number of slots and then expires it, which frees its notes for another attempt, and it refuses a new one while too many are waiting.

If the relayer refuses

Use another relayer, or submit the transaction yourself and accept that your wallet appears on it as the signer.

Last updated

On this page