A Node.js TypeScript service that periodically synchronizes the ORBS L3 network committee to multiple EVM chains by collecting signatures from committee nodes and submitting them via smart contract transactions.
This service monitors the ORBS L3 network committee and automatically syncs committee changes to configured EVM chains. The flow is:
- Committee change detection: Fetches the current committee from ORBS L3 via
getCurrentCommittee. When the committee changes, it announces a new nonce (N+1). - Signature collection: Calls
getSignedCommittee?nonce=Non each committee node via the lambda, collects all guardian signatures for that nonce, and stores them in PostgreSQL. - Chain sync: For each configured EVM chain, reads the contract's
nonce()state. If the contract is behind the latest stored nonce, submitssync(newCommittee, newConfig, sigs)for each missing nonce sequentially.
The nonce is the version of the current committee state. Each committee change increments the nonce (N, N+1, N+2, …). The backend:
- Stores signed committees per nonce in PostgreSQL (committee payload + guardian signatures)
- Reads each target contract's
nonce()to see how far behind it is - Submits one
sync()transaction per missing nonce, in order, until the contract is up to date
The lambda getSignedCommittee?nonce=N returns signatures for a specific nonce; the signed payload includes the nonce.
┌─────────────────┐
│ ORBS L3 Network│
│ (orbs-client) │
└────────┬────────┘
│
│ Fetch committee
│
┌────────▼─────────────────────────────┐
│ Committee Sync Backend Service │
│ │
│ ┌──────────────────────────────┐ │
│ │ Periodic Check Loop │ │
│ │ (CHECK_INTERVAL seconds) │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────┐ │
│ │ Committee Change Detection │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────┐ │
│ │ Collect Signatures │ │
│ │ (from all committee nodes) │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────┐ │
│ │ Submit to EVM Chains │ │
│ │ (sync() per missing nonce) │ │
│ └───────────────────────────────┘ │
└────────┬─────────────────────────────┘
│
├──────────────┬──────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Chain 1 │ │ Chain 2 │ │ Chain N │
│ (EVM) │ │ (EVM) │ │ (EVM) │
└─────────┘ └─────────┘ └─────────┘
- Nonce-based sync: Each committee version has a nonce; signed committees are stored per nonce in PostgreSQL
- Periodic monitoring: Configurable interval for checking committee changes and syncing chains
- Multi-chain support: Syncs committee to multiple EVM chains; each chain's contract nonce is read and updated independently
- Signature collection: Aggregates signatures from all committee nodes via
getSignedCommittee?nonce=N - Dynamic configuration: Reloads
chain.jsonon each iteration - Status API: Express server providing real-time status and activity logs
- Error tracking: Comprehensive error logging and reporting
Create a .env file in the project root:
# ORBS Network Configuration
SEED_IP=13.112.58.64 # ORBS L3 seed node IP address
# Sync Configuration
CHECK_INTERVAL=300 # Interval in seconds between committee checks
# EVM Chain Configuration
PRIVATE_KEY=0x... # Private key for signing transactions (without 0x prefix is also accepted)
# Express Server Configuration
PORT=3000 # Port for status API server (default: 3000)
# Database (PostgreSQL)
DB_HOST=localhost # Database host (default: localhost)
DB_PORT=5432 # Database port (default: 5432)
DB_USER=postgres # Database user (default: postgres)
DB_PASSWORD= # Database password
DB_NAME=committee_sync # Database name (default: committee_sync)Create a chain.json file in the project root with the following format:
[
["https://mainnet.infura.io/v3/YOUR_PROJECT_ID", "0x1234567890123456789012345678901234567890"],
["https://polygon-rpc.com", "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"]
]Each entry is an array [rpcUrl, contractAddress] where:
- First element: RPC URL string for the EVM chain
- Second element: Contract address string for the committee-sync contract
Note: The chain.json file is reloaded on every iteration, allowing dynamic chain configuration updates without restarting the service.
The contract exposes nonce() (view) and sync(). The abi.json file must include:
function nonce() external view returns (uint256);
function sync(address[] memory newCommittee, CommitteeSyncConfig.Config[] memory newConfig, bytes[] memory sigs) external;nonce: Current committee version on the contractnewCommittee: Array of committee member addressesnewConfig: Per-member config (structure depends onCommitteeSyncConfig.Config)sigs: Array of hex-encoded signatures corresponding to each committee member
The service uses PostgreSQL to store signed committees per nonce. Create the database and ensure the schema is applied. On first run, migrations in migrations/001_schema.sql are applied automatically.
Schema:
committee_nonces: One row per nonce with committee payload (nonce, committee_hash, committee_json, created_at)committee_signatures: One row per guardian signature (nonce, guardian_address, signature, created_at)
The project includes a Docker setup for local development:
./db/run.shThis starts PostgreSQL in a container with data persisted in db/data/. Credentials are in db/docker.env (user: postgres, password: postgres, database: committee_sync).
Ensure your .env includes:
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=committee_sync
To stop: docker stop committee-sync-db. See db/README.md for more options.
Prerequisites: This project requires the orbs-client package to be available as a sibling directory at ../git/orbs-network/orbs-client. The package is not yet published to npm.
# Ensure orbs-client is available as a sibling directory
# Expected structure:
# ../git/orbs-network/
# ├── orbs-client/
# └── committee-sync-backend/
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run the service
npm startNote: The orbs-client package is imported as a local dependency from the sibling folder. Make sure both projects are cloned in the same parent directory.
# Run with TypeScript compiler in watch mode
npm run dev
# Run tests
npm test# Build the project
npm run build
# Start the service
npm startReturns the current status of the service including activity history, sync statistics, and errors.
Response Format:
{
"status": "running",
"startTime": "2024-01-01T00:00:00.000Z",
"uptime": 3600,
"currentCommittee": {
"members": ["0x...", "0x..."],
"lastUpdated": "2024-01-01T00:05:00.000Z"
},
"syncStats": [
{
"rpcUrl": "https://mainnet.infura.io/v3/YOUR_PROJECT_ID",
"contractAddress": "0x1234567890123456789012345678901234567890",
"totalSyncs": 5,
"lastSync": "2024-01-01T00:05:00.000Z",
"lastSyncStatus": "success"
},
{
"rpcUrl": "https://polygon-rpc.com",
"contractAddress": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"totalSyncs": 3,
"lastSync": "2024-01-01T00:04:30.000Z",
"lastSyncStatus": "success"
}
],
"activity": [
{
"timestamp": "2024-01-01T00:05:00.000Z",
"type": "committee_sync",
"rpcUrl": "https://mainnet.infura.io/v3/YOUR_PROJECT_ID",
"contractAddress": "0x1234567890123456789012345678901234567890",
"status": "success",
"details": "Committee synced successfully"
}
],
"errors": [
{
"timestamp": "2024-01-01T00:03:00.000Z",
"type": "signature_collection",
"message": "Failed to collect signature from node 0x...",
"node": "0x..."
}
]
}Response Fields:
status: Current service status ("running"|"error")startTime: ISO timestamp when the service starteduptime: Service uptime in secondscurrentCommittee: Current committee informationmembers: Array of committee member addresseslastUpdated: ISO timestamp of last committee update
syncStats: Array of per-chain synchronization statisticsrpcUrl: RPC URL for the chaincontractAddress: Contract address for the committee-sync contracttotalSyncs: Total number of successful syncs to this chainlastSync: ISO timestamp of the last sync attemptlastSyncStatus: Status of the last sync ("success"|"error")
activity: Array of recent activities (last N entries)timestamp: ISO timestamp of the activitytype: Activity type ("committee_sync","signature_collection","error", etc.)rpcUrl: RPC URL for the chain (if applicable)contractAddress: Contract address for the chain (if applicable)status: Activity status ("success"|"error")details: Human-readable description
errors: Array of recent errors (last N entries)timestamp: ISO timestamp of the errortype: Error type ("signature_collection","transaction","committee_fetch", etc.)message: Error messagenode: Optional node identifier if error is node-specific
- Load Configuration: Reload
chain.jsonfile - Fetch Committee: Use
@orbs-network/orbs-clientto get current committee from ORBS L3 network:- Get committee nodes using
client.getNodes({ committeeOnly: true }) - Call
{LAMBDA_SCRIPT_BASE_URL}/getCurrentCommitteeon a committee node - Parse the response to extract the current committee data
- Get committee nodes using
- Committee Change: If committee has changed:
- Compute new nonce = (latest stored nonce in DB) + 1
- Call
{LAMBDA_SCRIPT_BASE_URL}/getSignedCommittee?nonce=Non each committee node - Store committee payload and signatures in PostgreSQL
- Chain Sync: For each chain in
chain.json:- Read contract
nonce()via RPC - If contract nonce < latest stored nonce, load each missing nonce from DB
- For each missing nonce, call
sync(newCommittee, newConfig, sigs)sequentially
- Read contract
- Update Status: Record activity and update status endpoint data
- Wait: Sleep for
CHECK_INTERVALseconds before next iteration
- Committee Fetch Errors: Logged and reported in status endpoint, service continues
- Signature Collection Errors: Individual node failures are logged, service attempts to collect from remaining nodes
- Transaction Errors: Per-chain errors are logged separately, other chains continue processing
- Configuration Errors: Invalid
chain.jsonformat causes error log, service continues with previous configuration
@orbs-network/orbs-client: ORBS network client for committee data (imported from sibling folder../git/orbs-network/orbs-client)ethers: Ethereum library for EVM chain interactionsexpress: Web server for status APIpg: PostgreSQL client for storing signed committeesdotenv: Environment variable managementtypescript: TypeScript compiler
Local Dependency Setup:
The orbs-client package must be available as a sibling directory. In package.json, it should be configured as:
{
"dependencies": {
"@orbs-network/orbs-client": "file:../orbs-client"
}
}orbs-network/
├── orbs-client/ # Sibling dependency (required)
│ └── ...
└── committee-sync-backend/
├── src/
│ ├── index.ts # Main entry point
│ ├── config/
│ │ └── config.ts # Configuration loading
│ ├── orbs/
│ │ └── committee.ts # ORBS committee fetching logic
│ ├── signatures/
│ │ └── collector.ts # Signature collection logic
│ ├── evm/
│ │ └── sync.ts # EVM chain sync logic
│ ├── server/
│ │ └── status.ts # Express status server
│ └── types/
│ └── index.ts # TypeScript type definitions
├── chain.json # Chain configuration (user-provided)
├── abi.json # Contract ABI (user-provided)
├── .env # Environment variables (user-provided)
├── package.json
├── tsconfig.json
└── README.md
Important: The orbs-client package must be available as a sibling directory. Both projects should be cloned in the same parent directory (../git/orbs-network/).
The project uses TypeScript with strict type checking. See tsconfig.json for configuration details.
- Private Key Management: Never commit
.envfile or private keys to version control - RPC Endpoints: Use secure RPC endpoints (HTTPS) in production
- Contract Verification: Verify contract addresses before deployment
- Error Logging: Avoid logging sensitive information (private keys, full transaction data)
- Check that all required environment variables are set
- Verify
chain.jsonfile exists and is valid JSON - Ensure
SEED_IPis reachable
- Verify ORBS network connectivity
- Check
SEED_IPis correct and accessible - Review error logs in status endpoint
- Verify
PRIVATE_KEYhas sufficient balance for gas fees - Check RPC endpoints are accessible
- Verify contract addresses are correct
- Review transaction errors in status endpoint
MIT