Skip to main content

Root Updater

The root updater service (ghost-root-updater, port 3001) is the critical link between on-chain commitment events and the Merkle tree state used by the Ghost Protocol. It listens for Committed events emitted by the CommitmentTree contract, maintains a local incremental Poseidon Merkle tree, computes the new root after each insertion, and submits the updated root on-chain via the updateRoot() function.

Role in the Protocol

When a user commits tokens through the Ghost Protocol, the CommitRevealVault contract forwards the commitment hash to the CommitmentTree contract, which emits a Committed event containing the commitment value and its leaf index. However, the on-chain contract does not compute the Merkle root itself — Poseidon hashing is too expensive to perform on-chain for an entire tree path. Instead, the root updater performs this computation off-chain and submits the result.

User commits tokens


CommitRevealVault.commit()


CommitmentTree.insertCommitment()


emit Committed(commitment, leafIndex)


Root Updater detects event


Insert into local Merkle tree


Compute new Poseidon root


CommitmentTree.updateRoot(newRoot)

The on-chain updateRoot() function is permissioned — only the designated operator address can call it. This ensures that arbitrary parties cannot submit incorrect roots.

Incremental Poseidon Merkle Tree

The root updater maintains a full incremental Merkle tree in memory. The tree uses Poseidon2 (PoseidonT3) for internal node hashing, matching the circuit's expectations:

  • Depth: Matches the on-chain CommitmentTree depth (typically 20 levels, supporting up to 2^20 = 1,048,576 commitments).
  • Hash function: Poseidon2 over the BN254 scalar field.
  • Zero values: Each level has a precomputed "zero" hash. An empty leaf is 0, and each subsequent level's zero is Poseidon2(zero[level-1], zero[level-1]).
  • Insertion: New commitments are inserted at the next available leaf index. The path from the leaf to the root is recomputed.

Current State

As of the last recorded snapshot:

  • Commitment count: 131
  • Last synced block: ~883,495
  • Tree utilization: 131 / 1,048,576 (0.01%)

Event Listening

The service connects to the Specter chain RPC and subscribes to Committed events on the CommitmentTree contract address. It processes events in order of block number and log index to maintain consistency.

On startup, the service performs a historical sync:

  1. Query all past Committed events from the contract's deployment block to the current block.
  2. Insert each commitment into the local tree in order.
  3. Verify the locally computed root matches the on-chain root.
  4. Begin listening for new events in real time.

This historical sync ensures the service can recover from restarts without data loss.

Debounce Batching

When multiple commitments arrive in rapid succession (e.g., several commits in the same block), the root updater debounces root submissions to avoid redundant on-chain transactions:

  1. A new Committed event arrives and is inserted into the local tree.
  2. A debounce timer starts (configurable, typically 2–5 seconds).
  3. If additional events arrive before the timer expires, they are inserted and the timer resets.
  4. When the timer expires, a single updateRoot() transaction is submitted with the final root that incorporates all batched commitments.

This reduces gas costs and avoids transaction nonce conflicts when multiple commitments land in consecutive blocks.

Commitment Cache

The service maintains a persistent cache of all processed commitments, keyed by leaf index. This cache serves multiple purposes:

  • Deduplication: If the same event is delivered twice (e.g., due to RPC reorg or reconnection), the cache prevents double-insertion.
  • Fast restart: On restart, the cache is loaded before the historical sync begins, reducing the number of events that need to be reprocessed.
  • Diagnostic queries: The health endpoint can report the total commitment count and the most recently inserted commitment.

The cache is written to disk atomically to prevent corruption on unexpected shutdown.

Keystore Support

The operator private key used for updateRoot() transactions can be provided in multiple ways:

  • Environment variable: OPERATOR_PRIVATE_KEY set directly in the PM2 environment.
  • Encrypted keystore: A JSON keystore file (Ethereum V3 format) decrypted at startup with a password from an environment variable or stdin prompt.

The keystore approach is preferred for production, as it avoids storing the raw private key in plaintext environment files.

Health Endpoint

The service exposes a health check at GET /health on port 3001:

{
"status": "healthy",
"commitmentCount": 131,
"lastSyncedBlock": 883495,
"merkleRoot": "0x...",
"uptime": 864000,
"pendingBatch": 0
}
FieldDescription
status"healthy" if the service is running and synced, "syncing" during historical replay
commitmentCountTotal number of commitments inserted into the local tree
lastSyncedBlockThe most recent block number processed
merkleRootThe current locally computed Merkle root
uptimeService uptime in seconds
pendingBatchNumber of commitments in the current debounce batch awaiting submission

Error Handling

  • RPC disconnection: The service implements exponential backoff reconnection. During disconnection, no events are missed — the historical sync on reconnection catches up.
  • Transaction failure: If updateRoot() reverts (e.g., due to gas estimation failure or nonce collision), the service retries with increased gas and an updated nonce.
  • Reorgs: If a chain reorganization invalidates previously processed events, the service detects the inconsistency during the next sync cycle and rebuilds the tree from the fork point.

Configuration

Key configuration parameters:

ParameterDefaultDescription
RPC_URLSpecter chain RPC endpoint
COMMITMENT_TREE_ADDRESSAddress of the CommitmentTree contract
OPERATOR_PRIVATE_KEYPrivate key for updateRoot() transactions
DEBOUNCE_MS3000Debounce interval for batching root updates
START_BLOCK0Block number to begin historical sync from
PORT3001HTTP server port for health endpoint