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
CommitmentTreedepth (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 isPoseidon2(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:
- Query all past
Committedevents from the contract's deployment block to the current block. - Insert each commitment into the local tree in order.
- Verify the locally computed root matches the on-chain root.
- 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:
- A new
Committedevent arrives and is inserted into the local tree. - A debounce timer starts (configurable, typically 2–5 seconds).
- If additional events arrive before the timer expires, they are inserted and the timer resets.
- 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_KEYset 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
}
| Field | Description |
|---|---|
status | "healthy" if the service is running and synced, "syncing" during historical replay |
commitmentCount | Total number of commitments inserted into the local tree |
lastSyncedBlock | The most recent block number processed |
merkleRoot | The current locally computed Merkle root |
uptime | Service uptime in seconds |
pendingBatch | Number 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:
| Parameter | Default | Description |
|---|---|---|
RPC_URL | — | Specter chain RPC endpoint |
COMMITMENT_TREE_ADDRESS | — | Address of the CommitmentTree contract |
OPERATOR_PRIVATE_KEY | — | Private key for updateRoot() transactions |
DEBOUNCE_MS | 3000 | Debounce interval for batching root updates |
START_BLOCK | 0 | Block number to begin historical sync from |
PORT | 3001 | HTTP server port for health endpoint |