Relayer
Off-chain infrastructure for tree management
Overview#
The relayer is the off-chain service that manages the Poseidon Merkle tree, processes deposit events, serves Merkle proofs, and submits root updates via multisig. It bridges the gap between asynchronous deposits and the on-chain root state.
Relayer Architecture
The relayer is permissionless and run independently. Anyone can run a relayer instance by configuring oracle keypairs, RPC endpoint, and denominations. The multisig (2-of-3) prevents any single party from unilaterally updating the Merkle root.
Components#
The relayer consists of three modules:
| Module | File | Description |
|---|---|---|
| DepositRelayer class | src/index.ts | Core logic: event processing, tree management, root updates |
| Express server | src/server.ts | HTTP endpoints for proof serving, counters, health |
| PoseidonMerkleTree | src/tree.ts | In-memory Merkle tree implementation |
DepositRelayer#
class DepositRelayer {
constructor(
connection: Connection,
program: Program,
admin: Keypair,
oracles: Keypair[],
tree: PoseidonMerkleTree,
denomination: bigint,
threshold: number, // default: 2
isToken?: boolean, // default: false
mint?: PublicKey,
);
}
Key Methods#
| Method | Description |
|---|---|
| processDepositEvent(event) | Inserts commitment into tree. Triggers pushRootUpdate() if pendingLeaves >= threshold |
| pushRootUpdate() | Builds and submits multisig update_root transaction with 2 oracle signers |
| parseDepositEventsFromLogs(logs) | Parses DepositEvent from raw program logs |
Express Server#
Endpoints#
| Endpoint | Method | Description |
|---|---|---|
| /health | GET | Returns { ok, leaves, multisig, relayers } |
| /proof | GET | ?commitment=0x...&denomination=... — Returns Merkle proof for a commitment. If not yet in tree, blocks up to 30s waiting for the onLogs callback to resolve the pending promise. Returns 408 on timeout. |
| /next-counter | GET | ?key=0x...&denomination=... — Atomically increments and returns a per-(key, denomination) counter. |
Event Subscription#
connection.onLogs(PROGRAM_ID, (logs) => {
for (const event of relayer.parseDepositEventsFromLogs(logs)) {
relayer.processDepositEvent(event);
// Resolve any pending /proof requests for this commitment
}
}, "confirmed");
Configuration#
Environment variables:
| Variable | Default | Description |
|---|---|---|
| RPC_URL | http://127.0.0.1:8899 | Solana RPC endpoint |
| ADMIN_KEYPAIR | — | Path to admin keypair JSON |
| ORACLE1_KEYPAIR | — | Path to oracle 1 keypair JSON |
| ORACLE2_KEYPAIR | — | Path to oracle 2 keypair JSON |
| ORACLE3_KEYPAIR | — | Path to oracle 3 keypair JSON |
| DENOMINATIONS | 0.1,1,10 | Comma-separated denominations |
| RELAYER_THRESHOLD | 1 | Root update threshold |
| PORT | 3001 | HTTP server port |
| COUNTERS_FILE | ./relayer-counters.json | Counter persistence path |
Root Update Flow#
1. Shield transaction confirms
2. onLogs callback fires
3. parseDepositEventsFromLogs extracts DepositEvent
4. Commitment inserted into tree
5. If pendingLeaves >= threshold:
a. Compute new root from tree
b. Build update_root instruction
c. Add 2 oracle keypairs as signers in remaining_accounts
d. Sign with admin + 2 oracles
e. Submit transaction
Pending Deposits
If threshold > 1, the root is not updated immediately after each deposit. This means the on-chain root may lag behind the relayer's tree. Withdrawals during this window use the previous root. The frontend polls /proof which waits up to 30s for the deposit to be processed by the relayer.
PDA Derivation#
multisigPda(programId) → ["multisig"]
mixerStatePda(denomination, programId) → ["mixer", denomination LE bytes]
State Persistence#
- Oracle keypairs: JSON files specified via environment variables
- Admin keypair: Auto-generated or loaded from
relayer-admin.json - Counters: Persisted to
relayer-counters.json(per-key, per-denomination)
Running a Relayer#
# Set up environment
export ORACLE1_KEYPAIR=~/.config/solana/o1.json
export ORACLE2_KEYPAIR=~/.config/solana/o2.json
export ORACLE3_KEYPAIR=~/.config/solana/o3.json
export RPC_URL=http://127.0.0.1:8899
# Start
cd packages/relayer
npm run start
Dev Server#
The dev.sh script automates full local development:
./dev.sh
This handles: keypair generation, solana-test-validator, anchor deploy, relayer startup, and frontend launch.