Skip to content

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

npm install
npm run build
npm run ci                    # typecheck + lint + format + test

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):

curl -H "Authorization: Bearer your-admin-token" http://localhost:8080/v1/seats

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

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:

  1. Operator generates BLS keypair externally and provides the pubkey to the foundation
  2. Foundation creates seat with vault:
    node dist/index.js seat create-with-vault \
      --operator <id> --pubkey <0x...> --beneficiary <0x...>
    
  3. Foundation tells operator the vault address (withdrawal credentials)
  4. Operator generates deposit data signed over the vault's withdrawal credentials
  5. 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"
      }'
    
  6. Foundation approves and deposits (steps 6.2 and 6.3 below)
  7. 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:

node dist/index.js seat approve <seatId> --send

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:

node dist/index.js seat deposit <seatId> --amount-ctn 32 --send

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 version column to prevent race conditions
  • Audit propagation: Every mutation records userId, requestId, and ipAddress
  • Idempotency: POST routes support Idempotency-Key header for safe retries

10. Runbook: Common Operations

  1. Operator provides their BLS public key to the foundation
  2. In admin UI: Seats → Create Seat → External (BYO-BLS) tab
  3. Enter operator, pubkey, beneficiary address → creates seat + vault
  4. Share the vault address with the operator
  5. Operator generates deposit data with vault address as withdrawal target
  6. Operator submits deposit data via API (POST /v1/seats/:id/deposit-data)
  7. Click Approve on the seat — allowlists on-chain
  8. Click Deposit on the approved seat — sends 32 CTN
  9. Watchers progress the seat to ACTIVE automatically
  10. 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:

node dist/index.js seat force-exit <seatId> --send

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.