Operator Guide¶
Who is this for?
Node operators, contract owners, security reviewers, and DevOps engineers responsible for managing validator onboarding on the Centurion network.
1. Overview¶
The Seat Manager operates the DepositContractCTN allowlist — a gatekeeper that controls which validator pubkeys may deposit on the Centurion chain.
A seat is a tracked (pubkey, withdrawal_credentials) pair. Each seat has an intentHash = keccak256(pubkey || wc || amountGwei || authorizedDepositor || ownershipEpoch) that the contract uses to allow or deny deposits.
| Capability | Description |
|---|---|
| BYO-BLS onboarding | External operators bring their own BLS key — foundation deploys vault |
| Per-validator vaults | Deploy a WithdrawalVault for each seat |
| PostgreSQL-backed state | Prisma ORM over PostgreSQL 18 |
| Session-based auth + RBAC | ADMIN > OPERATOR > VIEWER role hierarchy |
| Fastify REST API | Full CRUD + seat lifecycle actions |
| Next.js admin frontend | Dashboard, seat/operator/user management |
| Real-time watchers | EL/CL watchers observe deposits and activation |
| Evidence bundles | Tamper-evident JSON files for every admin action |
| Cross-check preflight | Dangerous actions verified across multiple endpoints |
| OpenTelemetry tracing | Optional distributed tracing export |
2. Architecture¶
graph TB
subgraph Frontend["Admin Frontend (Next.js 15)"]
UI[Dashboard / Seats / Operators / Users]
end
subgraph API["Fastify API Server"]
Auth[Session Auth + RBAC]
Routes[REST Routes + Zod Validation]
Idem[Idempotency Middleware]
end
subgraph CLI["CLI Commands"]
CD[Command Dispatcher]
PE[Preflight Engine]
EW[Evidence Writer]
end
subgraph Store["PostgreSQL + Prisma"]
DB[(PostgreSQL 18)]
end
subgraph Watchers["Background Watchers"]
ELW[EL Watcher - 12s poll]
CLW[CL Watcher - 24s poll]
end
UI --> API
API --> Store
CLI --> Store
Watchers --> Store
CLI -->|read-only RPC| NODES[EL / CL Nodes]
Watchers -->|read-only RPC| NODES
CLI -->|signed txs| DC[Deposit Contract]
API -->|signed txs| DC
Trust Boundaries¶
| Component | Trust Level | Signs Txs? |
|---|---|---|
| Admin Frontend | Client-side, session-authenticated | No |
| Fastify API | Server-side, RBAC-gated | Yes (via CLI commands) |
| Preflight Engine | Read-only RPC | No |
| Evidence Writer | Local file I/O | No |
| PostgreSQL Store | Server-side | No |
| Signer | Privileged — holds owner key | Yes |
3. Installation & Setup¶
Prerequisites¶
| Requirement | Details |
|---|---|
| Node.js | Version 22+ |
| PostgreSQL | Version 18+ |
| npm | Workspaces support |
| deposit-cli | For BLS key generation (auto-discovered in centurion-networks) |
Build & Verify¶
Database Setup¶
export DATABASE_URL="postgresql://user:password@localhost:5432/seat_manager?schema=public"
npx prisma migrate dev
4. Configuration¶
4.1 Config File¶
seat-manager.config.json in the repo root:
{
"centurionInfraRoot": "/path/to/centurion-infra",
"centurionNetworksRoot": "/path/to/centurion-networks",
"endpoints": [
{
"label": "node-1",
"host": "<node-ip>",
"rpcPort": 8545,
"elClient": "geth",
"clClient": "lighthouse",
"sshUser": "<ssh-user>"
},
{
"label": "node-2",
"host": "<node-ip>",
"rpcPort": 8545,
"elClient": "nethermind",
"clClient": "prysm",
"sshUser": "<ssh-user>"
}
]
}
Endpoints can be defined directly in the config file or discovered from centurion-infra/deployments/mainnet/deploy-node.sh.
4.2 Environment Variables¶
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
ADMIN_TOKEN |
For API | Static admin bearer token |
SEAT_MANAGER_OWNER_PRIVATE_KEY |
For --send |
Contract owner key (0x-prefixed hex) |
SEAT_MANAGER_INTENTS_MNEMONIC |
For BLS keygen | BLS mnemonic — passed to deposit-cli via stdin, never as CLI arg |
SEAT_MANAGER_INTENTS_KEYSTORE_PASSWORD |
For BLS keygen | Keystore password — passed via stdin |
SEAT_MANAGER_VAULT_DEPLOYER_PRIVATE_KEY |
For vault deploy | Falls back to owner key |
SEAT_MANAGER_DEPOSITOR_PRIVATE_KEY |
For deposits | Falls back to owner key |
SESSION_HMAC_SECRET |
For API | HMAC key for session token hashing (auto-generated if unset) |
OTEL_EXPORTER_OTLP_ENDPOINT |
Optional | OpenTelemetry endpoint (e.g., http://localhost:4318) |
CENTURION_INFRA_ROOT |
No | Override infra path |
CENTURION_NETWORKS_ROOT |
No | Override networks path |
Key material security
BLS mnemonics and keystore passwords are passed to deposit-cli via stdin, never as command-line arguments. This prevents exposure in process listings and shell history.
5. Running the System¶
5.1 API Server + Admin Frontend¶
# Terminal 1: API backend
DATABASE_URL="postgresql://..." \
ADMIN_TOKEN="your-secure-token" \
SEAT_MANAGER_OWNER_PRIVATE_KEY=0x... \
SEAT_MANAGER_INTENTS_MNEMONIC="your mnemonic words" \
SEAT_MANAGER_INTENTS_KEYSTORE_PASSWORD="your-password" \
node dist/index.js api-v2 --port 8080
# Terminal 2: Admin frontend (port 3003, proxies API to :8080)
cd apps/admin && npm run dev
5.2 Watchers¶
# Terminal 3: EL + CL watchers
DATABASE_URL="postgresql://..." \
SEAT_MANAGER_OWNER_PRIVATE_KEY=0x... \
node dist/index.js watch
Watchers are a separate process from the API. They poll the blockchain and update seat statuses in the shared PostgreSQL database.
5.3 Authentication¶
The API supports two auth mechanisms:
Session auth (primary):
# Login
curl -X POST http://localhost:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password"}'
# Returns: { "token": "session:abc123..." }
# Use session token
curl -H "Authorization: Bearer session:abc123..." http://localhost:8080/v1/seats
Static admin token (legacy):
5.4 RBAC¶
| Role | Can do |
|---|---|
| ADMIN | Everything: user management, approve/revoke/deposit, generate keys, freeze, switch |
| OPERATOR | Create seats, create operators, view all data |
| VIEWER | Read-only access to seats, operators, audit logs |
6. Seat Lifecycle Operations¶
6.1 BYO-BLS Onboarding (Recommended for External Operators)¶
The default onboarding model for external operators is bring-your-own-BLS (BYO-BLS). The operator generates their own BLS keypair — the foundation never touches the signing key.
Flow:
- Operator generates BLS keypair externally and provides the pubkey to the foundation
- Foundation creates seat with vault:
- Foundation tells operator the vault address (withdrawal credentials)
- Operator generates deposit data signed over the vault's withdrawal credentials
- Operator submits deposit data via API:
curl -X POST http://localhost:8080/v1/seats/<id>/deposit-data \ -H "Authorization: Bearer session:..." \ -H "Content-Type: application/json" \ -d '{ "pubkey": "0x...", "withdrawal_credentials": "0x01000000000000000000000<vault_address>", "signature": "0x...", "deposit_data_root": "0x...", "amount": "32000000000" }' - Foundation approves and deposits (steps 6.2 and 6.3 below)
- Operator loads their BLS key into their own validator client
Deposit data validation
The API enforces strict server-side validation: pubkey, withdrawal credentials, and amount must exactly match the seat. Mismatched deposit data is rejected.
Kill switch
Treasury can force-exit the validator via seat force-exit <id> --send (EIP-7002) without needing the BLS key. The vault's withdrawal credentials address requests exits through the system contract.
6.1b Generate Keys (Internal Only — Foundation-Managed Validators)¶
For foundation-managed validators, the "Generate Keys" flow auto-generates a BLS keypair from the foundation's mnemonic. Do not use this for external operators — it creates key custody obligations.
Via the admin UI: Seats → Create Seat → Internal (Managed Keys) tab.
Via the API:
curl -X POST http://localhost:8080/v1/seats/generate \
-H "Authorization: Bearer session:..." \
-H "Content-Type: application/json" \
-d '{
"operatorId": "clxxx...",
"beneficiary": "0x...",
"treasury": "0x...",
"principalCtn": "32"
}'
This atomically:
1. Generates a BLS validator keypair via deposit-cli
2. Deploys a WithdrawalVault contract on-chain
3. Derives withdrawal credentials from the vault address
4. Creates the seat in CREATED status
6.2 Approve (Allowlist On-Chain)¶
Via the admin UI: click Approve on a CREATED seat.
Via CLI:
Calls addAllowedDeposit(pubkey, wc) on the deposit contract. Seat transitions to ALLOWLISTED.
6.3 Deposit 32 CTN¶
Via the admin UI: click Deposit on an ALLOWLISTED seat.
Via CLI:
Sends 32 CTN to the deposit contract. Post-send verification checks: receipt success, exactly 1 DepositEvent, matching fields, depositCount increments by 1.
6.4 Watcher Progression¶
After deposit, the watchers handle the rest automatically:
| Watcher | Transition | Trigger |
|---|---|---|
| EL Watcher | → DEPOSITED |
Detects DepositEvent in contract logs |
| CL Watcher | → SEEN_BY_CL |
Sees validator pubkey in beacon chain |
| CL Watcher | → ACTIVE |
Validator status = active_ongoing |
6.5 Revoke¶
Via the admin UI: click Revoke on any seat (with confirmation dialog).
| Current State | What Happens |
|---|---|
CREATED |
Marked REVOKED — no on-chain action |
ALLOWLISTED |
Sends removeAllowedDeposit, then marks REVOKED |
DEPOSITED+ |
Marked REVOKED operationally (deposit is irreversible on-chain) |
7. Preflight & Safety System¶
7.1 Cross-Check Preflight¶
All admin commands verify chain state across 2+ endpoints with distinct EL clients before proceeding:
| Check | Pass Condition |
|---|---|
| Chain ID | Matches canonical config |
| Contract code | Non-empty bytecode at contract address |
| Fingerprint | keccak256(bytecode) matches canonical |
| Owner | owner() matches canonical owner |
7.2 Freeze Safety Gates¶
freeze execute requires all four gates:
| Gate | Mechanism |
|---|---|
| 1 | --dangerous CLI flag |
| 2 | CONFIRM_FREEZE=I_UNDERSTAND env var |
| 3 | Cross-check preflight on 2+ labels |
| 4 | Timelock maturity (notBefore <= now) |
8. Watchers¶
EL Watcher¶
Monitors DepositEvent logs from the deposit contract.
| Parameter | Value |
|---|---|
| Poll interval | 12 seconds |
| Max backoff | 60 seconds |
| Endpoints | 2+ with distinct EL clients |
Per poll cycle: fetch logs → decode DepositEvent → compute intentHash → look up seat → record deposit → transition to DEPOSITED.
CL Watcher¶
Monitors validator visibility on beacon nodes.
| Parameter | Value |
|---|---|
| Poll interval | 24 seconds |
| Max backoff | 120 seconds |
| Beacon API | /eth/v1/beacon/states/head/validators/{pubkey} |
Per poll cycle: query seats in DEPOSITED/SEEN_BY_CL → check each on CL endpoints → SEEN_BY_CL on first visibility → ACTIVE on activation.
Both watchers use bounded exponential backoff and respond to SIGINT/SIGTERM for graceful shutdown.
9. Database¶
PostgreSQL 18 via Prisma ORM. Schema in prisma/schema.prisma.
| Table | Purpose |
|---|---|
users |
Admin users with RBAC roles, scrypt password hashes |
sessions |
Session tokens (HMAC-SHA256 hashed), expiry tracking |
operators |
Operator entities linked to seats |
seats |
Seat lifecycle: pubkey, wc, status, vault metadata |
seat_events |
Append-only status transition history |
seat_bls_material |
BLS keystores + deposit data (1:1 with seat) |
seat_operations |
Multi-step workflow journal |
allowlist_actions |
On-chain allowlist tx records |
deposits |
Observed deposit events from EL |
cl_observations |
Beacon API visibility checks |
audit_log |
All actions with userId, requestId, ipAddress |
idempotency_keys |
POST request deduplication |
Key Features¶
- Optimistic concurrency: Seat status transitions use a
versioncolumn to prevent race conditions - Audit propagation: Every mutation records userId, requestId, and ipAddress
- Idempotency: POST routes support
Idempotency-Keyheader for safe retries
10. Runbook: Common Operations¶
Onboarding an External Operator (BYO-BLS — Recommended)¶
- Operator provides their BLS public key to the foundation
- In admin UI: Seats → Create Seat → External (BYO-BLS) tab
- Enter operator, pubkey, beneficiary address → creates seat + vault
- Share the vault address with the operator
- Operator generates deposit data with vault address as withdrawal target
- Operator submits deposit data via API (
POST /v1/seats/:id/deposit-data) - Click Approve on the seat — allowlists on-chain
- Click Deposit on the approved seat — sends 32 CTN
- Watchers progress the seat to ACTIVE automatically
- Operator loads BLS key into their validator client
Onboarding via CLI (BYO-BLS)¶
# 1. Create seat with vault (operator already generated BLS key)
export SEAT_MANAGER_OWNER_PRIVATE_KEY=0x...
node dist/index.js seat create-with-vault \
--operator <id> --pubkey <0x...> --beneficiary <0x...>
# 2. Operator submits deposit data via API
# (pubkey, WC, signature, deposit_data_root, amount validated server-side)
# 3. Approve
node dist/index.js seat approve <seatId> --send
# 4. Deposit (auto-detects BYO-BLS deposit data)
node dist/index.js seat deposit <seatId> --amount-ctn 32 --send
# 5. Watchers handle the rest
node dist/index.js watch
Force-Exit a Validator (EIP-7002)¶
Treasury can exit a validator without the BLS signing key via the vault's EIP-7002 integration:
Or via API: POST /v1/seats/:id/force-exit
The vault calls the EIP-7002 system contract (0x00000961Ef480Eb55e80D19ad83579A64c007002) with the validator's pubkey. Treasury-only — requires the owner private key or KMS signer.
Managing Users¶
# Create an admin user (via API)
curl -X POST http://localhost:8080/v1/users \
-H "Authorization: Bearer session:..." \
-H "Content-Type: application/json" \
-d '{"username":"operator1","password":"secure-pass","role":"OPERATOR","displayName":"Operator One"}'
Querying Audit Logs¶
curl "http://localhost:8080/v1/audit-logs?limit=50&actionType=seat.deposit" \
-H "Authorization: Bearer session:..."
11. Runbook: Emergency Procedures¶
Temporarily Disable Allowlist¶
export SEAT_MANAGER_OWNER_PRIVATE_KEY=0x...
node dist/index.js switch off --send
unset SEAT_MANAGER_OWNER_PRIVATE_KEY
Re-enable with switch on --send.
Permanent Allowlist Disable (Freeze)¶
Irreversible — no undo
# Day 0: Schedule with timelock
node dist/index.js freeze schedule --delay-seconds 172800 --send
# Day 2+: Execute
export CONFIRM_FREEZE=I_UNDERSTAND
node dist/index.js freeze execute --dangerous --send
12. Security Considerations¶
BLS Key Custody¶
| Model | Who holds BLS key | When to use |
|---|---|---|
| BYO-BLS (recommended) | External operator | External operators — foundation never touches the key |
| Managed keys (internal) | Foundation (encrypted in PostgreSQL) | Foundation-managed validators only |
In BYO-BLS mode, the foundation retains control via EIP-7002 force-exit — the vault can trigger validator exits without the BLS key. Principal-first settlement protects the foundation financially: if a validator's lifetime inflows fall below 32 CTN (e.g., due to slashing), the treasury recovers all available inflows up to the principal target before any rewards are distributed.
Key Material¶
| Rule | Details |
|---|---|
| BLS secrets via stdin | Mnemonic and keystore password passed to deposit-cli via stdin, never as CLI args |
| Owner key from env | Read once at startup, never stored or logged |
| KMS signer | Alternative to raw key — SEAT_MANAGER_KMS_KEY_ID uses AWS KMS (key never leaves HSM) |
| Session tokens hashed | HMAC-SHA256 hashed before storage — plaintext never persisted |
| Passwords scrypt-hashed | User passwords hashed with scrypt (N=32768, r=8, p=1) |
| Timing-safe comparisons | Admin token uses timingSafeEqual; login has timing cover for missing users |
API Security¶
| Feature | Details |
|---|---|
| RBAC enforcement | Role hierarchy: ADMIN > OPERATOR > VIEWER |
| Session auth | Tokens are session:<random>, HMAC-hashed in DB |
| Idempotency | POST routes support Idempotency-Key header |
| Request IDs | Every request gets a unique ID for audit correlation |
| Error sanitization | 5xx errors return generic message, 4xx return details |
| CSP headers | Content-Security-Policy on admin frontend |
Evidence Bundles¶
Every admin action writes a JSON evidence file to ./evidence/ before any transaction is sent. Bundles capture canonical config, preflight results, calldata, and (if sent) tx receipts.