protocol

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.

i

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.