Skip to content

System Architecture

Components

Centurion phase 1 has one backend, one admin frontend, one operator console, and five on-chain contracts. The backend is a Fastify service with a Postgres store, operation worker, EL deposit watcher, and optional CL promotion watcher; the runtime commands are api, watch, and api-watch in seat-manager/src/index.ts:11 and seat-manager/README.md:65. The admin console reads backend state and can revoke seats; it does not provision seats or issue enrollment tokens. The staker console is the operator-facing CLI and web surface; it runs onboard, status, rotate-api-key, claim, exit, migrate, verify-artifact, and verify-token from staker-console/src/index.ts:19.

The 32 CTN principal amount is owned by the Foundation. Operators bring hardware and local validator key custody; the backend uses the configured executor path for sponsored operations and writes durable Operation rows before on-chain submission. This is the shipped implementation, not a variant matrix.

graph TD
  Ops[Foundation Ops] -->|seed seats by env| Backend[Seat Manager API]
  Ops -->|issue signed JWT outside codebase| Operator[Approved Operator]
  Admin[Admin Console] -->|/admin/v1 read + revoke| Backend
  Operator -->|staker-console onboard| Console[Staker Console]
  Console -->|POST /v1/seats/:seatId/wc-template| Backend
  Console -->|POST /v1/seats/:seatId/register| Backend
  Console -->|GET status and day-2 actions| Backend
  Backend -->|Prisma transactions| DB[(Postgres)]
  Backend -->|queue worker| Executor[Operation Executor]
  Executor -->|deployVault| Factory[CenturionVaultFactory]
  Executor -->|allowlist + deposit| Deposit[DepositContractCTN]
  Executor -->|claims + exit| Controller[CenturionEconomicController]
  Factory -->|CREATE2| Vault[CenturionWithdrawalVault]
  Controller -->|pending claim guard| Gatekeeper[CenturionClaimGatekeeper]
  Deposit -->|DepositEvent| EL[Execution Layer]
  Backend -->|poll DepositEvent| EL
  Backend -->|poll validator state| CL[Consensus Layer]

Data Plane

Seat is the lifecycle record keyed by seat_id; pubkey, vault_address, and nonce_consumed are unique, and the one-active-seat-per-operator invariant is enforced by uniq_seats_active_operator_id in prisma/schema.prisma:10 and prisma/migrations/20260514165745_one_active_seat_per_operator/migration.sql:37.

Operation is the durable queue row for deposit, claim, voluntary_exit, and migrate; read paths use idx_operations_seat_created_desc, and live duplicate work is blocked by uniq_operations_live_seat_kind in prisma/schema.prisma:152 and prisma/migrations/20260507080000_security_remediation_hardening/migration.sql:19.

OperationTxIntent is the per-transaction intent ledger; the operation/action/target/calldata/value fingerprint and sender/nonce pair are unique in prisma/schema.prisma:175 and prisma/migrations/20260421043000_hardening_idempotency_nonce_and_rotation/migration.sql:13.

WcTemplate records the issued withdrawal-credential prediction before register; seat_id, pubkey, and vault_address are unique in prisma/schema.prisma:91.

OperatorApiKey stores post-register API keys and scopes; active lookup is indexed by seat and revocation state in prisma/schema.prisma:104.

AuditEvent is the append-only operational audit log with descending ID lookup in prisma/schema.prisma:115.

LifecycleEvidence stores state-machine evidence for each transition and is indexed by seat and creation time in prisma/schema.prisma:140.

NonceLedger is the JWT replay ledger keyed by nonce in prisma/schema.prisma:35.

Idempotency stores operator endpoint replay responses keyed by endpoint, seat, and idempotency key in prisma/schema.prisma:44.

AdminSession stores admin session state, CSRF token, and step-up expiry in prisma/schema.prisma:57.

AdminWriteIdempotency stores admin mutation replay responses and expiry in prisma/schema.prisma:75.

WatcherState stores persistent cursors such as el:last_scanned_block in prisma/schema.prisma:204 and seat-manager/src/api/server.ts:2640.

Control Plane

method path auth model rate limit idempotency
GET /v1/meta none none none; route at seat-manager/src/api/server.ts:1415
POST /v1/seats/:seatId/wc-template enrollment JWT 30 writes/window required Idempotency-Key; route at seat-manager/src/api/server.ts:1446
POST /v1/seats/:seatId/register enrollment JWT 20 writes/window required Idempotency-Key; route at seat-manager/src/api/server.ts:1594
GET /v1/seats/:seatId/status operator API key, status scope 60/minute none; route at seat-manager/src/api/server.ts:1782
POST /v1/seats/:seatId/voluntary-exit operator API key, voluntary_exit scope 20 writes/window required Idempotency-Key; route at seat-manager/src/api/server.ts:1984
POST /v1/claims operator API key, claim scope 40 writes/window required Idempotency-Key; route at seat-manager/src/api/server.ts:2077
POST /v1/seats/:seatId/migrate operator API key, migrate scope 20 writes/window required Idempotency-Key; route at seat-manager/src/api/server.ts:2222
POST /v1/seats/:seatId/operator-api-key/rotate operator API key, rotate_api_key scope 20 writes/window required Idempotency-Key; route at seat-manager/src/api/server.ts:2315
GET /admin/v1/session admin session cookie none none; route at seat-manager/src/api/server.ts:2429
POST /admin/v1/login username/password admin login limiter none; route at seat-manager/src/api/server.ts:2465
POST /admin/v1/logout admin session plus CSRF if present none none; route at seat-manager/src/api/server.ts:2531
POST /admin/v1/step-up admin session plus CSRF admin step-up limiter none; route at seat-manager/src/api/server.ts:2567
GET /admin/v1/health admin session none none; route at seat-manager/src/api/server.ts:2625
GET /admin/v1/dashboard admin session none none; route at seat-manager/src/api/server.ts:2670
GET /admin/v1/seats admin session none none; route at seat-manager/src/api/server.ts:2715
GET /admin/v1/seats/:seatId admin session none none; route at seat-manager/src/api/server.ts:2846
GET /admin/v1/seats/:seatId/events admin session none none; route at seat-manager/src/api/server.ts:2935
POST /admin/v1/seats/:seatId/revoke admin session, CSRF, fresh step-up, governance approval none required Idempotency-Key; route at seat-manager/src/api/server.ts:2983
GET /admin/v1/operations admin session none none; route at seat-manager/src/api/server.ts:3151
GET /admin/v1/operations/:operationId admin session none none; route at seat-manager/src/api/server.ts:3207
POST /admin/v1/operations/:operationId/retry admin operator/admin session, CSRF, fresh step-up none required Idempotency-Key; route at seat-manager/src/api/server.ts:3260
POST /admin/v1/operations/:operationId/cancel admin operator/admin session, CSRF, fresh step-up none required Idempotency-Key; route at seat-manager/src/api/server.ts:3333
GET /admin/v1/audit admin session none none; route at seat-manager/src/api/server.ts:3406
GET /admin/v1/support/bundle/:seatId admin session none none; route at seat-manager/src/api/server.ts:3460

Operator registration uses enrollment JWTs only for wc-template and register; post-register operations use operator API keys. DEFAULT_OPERATOR_API_SCOPES are status, claim, and rotate_api_key in seat-manager/src/onboarding/store.ts:201. voluntary_exit and migrate are valid scopes but are not in the default issue set in seat-manager/src/onboarding/store.ts:191.

Workers

Operation Queue Worker

The operation queue worker polls the database, not the chain. It claims the next live Operation row with claimNextQueuedOperation, leases it to a worker id, and hands it to the execution adapter in seat-manager/src/watchers/operationWorker.ts:157 and seat-manager/src/watchers/operationWorker.ts:165. The worker is started by both api-watch and watch; both read SEAT_MANAGER_WATCH_POLL_MS with default 1000 milliseconds in seat-manager/src/api/runtime.ts:273 and seat-manager/src/api/runtime.ts:320. The inner loop floors the poll interval at 100 milliseconds and the lease at 60_000 milliseconds in seat-manager/src/watchers/operationWorker.ts:143.

This worker does not advance a WatcherState cursor. Its durable cursor is the operation row itself: status, processing_by, lease_expires_at, attempt_count, last_error, and result_json live on Operation in seat-manager/prisma/schema.prisma:152. Each transaction plan is recorded as an OperationTxIntent; the fingerprint and sender nonce uniqueness constraints are in seat-manager/prisma/schema.prisma:175, and the backend execution adapter writes those tracked intents through the store hooks declared in seat-manager/src/contracts/operationExecutor.ts:121. Deposit operations also drive a lifecycle side effect: when execution is finalized and the seat is still ALLOWLISTED, the worker writes LifecycleEvidence and transitions the seat to DEPOSITED in seat-manager/src/watchers/operationWorker.ts:64.

Failure handling is explicit. A retryable execution failure calls requeueOperation, increments the requeue count, leaves the row live, and logs operation requeued in seat-manager/src/watchers/operationWorker.ts:181. A non-retryable failure calls markOperationFailed, records last_error, and logs operation failed in seat-manager/src/watchers/operationWorker.ts:190. Duplicate or already-submitted exit requests are treated as terminal for voluntary_exit and migrate through isTerminalAlreadySubmittedExit in seat-manager/src/watchers/operationWorker.ts:41.

Operators see worker health through the admin health payload and the staker console status output. The admin health endpoint marks operation_worker.ok false when a processing lease is stale and returns queue, failed, and stale counts in seat-manager/src/api/server.ts:2625 and seat-manager/src/api/server.ts:2649. The operator CLI prints open exceptions returned by status in staker-console/src/index.ts:163; a healthy queue shows fresh operation movement, while a degraded queue shows pending, failed, or stale operation rows in the admin operations endpoint at seat-manager/src/api/server.ts:3151.

EL Deposit Watcher

The EL deposit watcher subscribes by polling the configured execution RPC with getBlockNumber, getBlock, and getLogs for the deposit contract address. The deposit event ABI is embedded in seat-manager/src/watchers/lifecycleWatchLoop.ts:12, the viem client is created from config.previewParity.rpcUrl in seat-manager/src/watchers/lifecycleWatchLoop.ts:333, and logs are filtered by args.config.contracts.depositContract in seat-manager/src/watchers/lifecycleWatchLoop.ts:452. The loop cadence is the same SEAT_MANAGER_WATCH_POLL_MS value used by the operation worker; api-watch and watch pass it into runLifecycleWatchLoop in seat-manager/src/api/runtime.ts:297 and seat-manager/src/api/runtime.ts:337, and the loop floors it at 250 milliseconds in seat-manager/src/watchers/lifecycleWatchLoop.ts:348.

Its persistent cursor is WatcherState.stateKey = el:last_scanned_block. Startup reads that key, initializes it from the latest block when absent, and writes it back in seat-manager/src/watchers/lifecycleWatchLoop.ts:356. During normal operation it scans from cursor + 1 through the current latest block, then rewinds by the confirmation window before storing the next cursor in seat-manager/src/watchers/lifecycleWatchLoop.ts:500. It also maintains ring-buffer keys named el:block_ring:<slot> to detect persisted block hash mismatches across restarts in seat-manager/src/watchers/lifecycleWatchLoop.ts:399.

The rows it writes are WatcherState and LifecycleEvidence; it does not create Operation rows. For each accepted DepositEvent, the watcher matches an ALLOWLISTED seat by pubkey and withdrawal credentials, then calls markDeposited in seat-manager/src/watchers/lifecycleWatchLoop.ts:475 and seat-manager/src/watchers/lifecycleWatchLoop.ts:479. That writes the ALLOWLISTED -> DEPOSITED evidence fields required by seat-manager/src/workflow/stateMachine.ts:17 through the lifecycle engine at seat-manager/src/watchers/lifecycleEngine.ts:9.

Failure modes are handled in the loop. RPC failure is caught and logged as lifecycle watcher loop error without advancing the cursor in seat-manager/src/watchers/lifecycleWatchLoop.ts:504. A head rollback resets the cursor to zero and stores the reset in seat-manager/src/watchers/lifecycleWatchLoop.ts:376. A persisted reorg mismatch quarantines deposited-or-later seats by writing EXCEPTION evidence, rewinds by twice the confirmation window, updates el:last_scanned_block, and logs the number of quarantined seats in seat-manager/src/watchers/lifecycleWatchLoop.ts:293 and seat-manager/src/watchers/lifecycleWatchLoop.ts:436.

Operators see a healthy EL watcher through lifecycle_watcher.ok = true and a non-null el_last_scanned_block in /admin/v1/health, returned at seat-manager/src/api/server.ts:2655. A degraded watcher appears as a missing cursor, stale lifecycle state, pending deposit operation, or EXCEPTION reason beginning with reorg_quarantine, which is the evidence string written in seat-manager/src/watchers/lifecycleWatchLoop.ts:317.

CL Promotion Watcher

The CL promotion watcher polls the beacon API only when SEAT_MANAGER_CL_BEACON_API_URL is present. It fetches /eth/v1/beacon/headers/<stateId> for finalized by default, or head only when SEAT_MANAGER_CL_STATE_ID=head, in seat-manager/src/watchers/lifecycleWatchLoop.ts:79 and seat-manager/src/watchers/lifecycleWatchLoop.ts:84. It then queries /eth/v1/beacon/states/<stateId>/validators/<pubkey> for every DEPOSITED or SEEN_BY_CL seat with a pubkey in seat-manager/src/watchers/lifecycleWatchLoop.ts:177. Its cadence is the same lifecycle loop cadence from SEAT_MANAGER_WATCH_POLL_MS, and its promotion delay uses SEAT_MANAGER_CL_MIN_SEEN_EPOCHS_BEFORE_ACTIVE with default 2 epochs in seat-manager/src/watchers/lifecycleWatchLoop.ts:338.

The CL watcher advances WatcherState.stateKey = cl:seen_epoch:<seatId> after the first observed validator state in seat-manager/src/watchers/lifecycleWatchLoop.ts:216. It writes LifecycleEvidence for DEPOSITED -> SEEN_BY_CL and SEEN_BY_CL -> ACTIVE; those evidence fields are the state id, finalized epoch, observed slot, header root, observed timestamp, validator index, activation epoch, and pubkey required in seat-manager/src/workflow/stateMachine.ts:28 and seat-manager/src/workflow/stateMachine.ts:41. The actual transition calls are markSeenByCl and markActive in seat-manager/src/watchers/lifecycleWatchLoop.ts:206 and seat-manager/src/watchers/lifecycleWatchLoop.ts:247.

The watcher refuses unsafe or inconsistent observations. Production-like runs skip head state because head is unsafe for promotion evidence in seat-manager/src/watchers/lifecycleWatchLoop.ts:168. A missing validator stays in the current state, a withdrawal-credentials mismatch logs a warning and skips promotion in seat-manager/src/watchers/lifecycleWatchLoop.ts:188, and beacon request failures are caught without transitioning the seat in seat-manager/src/watchers/lifecycleWatchLoop.ts:262.

Operators see a healthy CL watcher as progression from DEPOSITED to SEEN_BY_CL and then ACTIVE in status. The admin seat detail endpoint includes lifecycle evidence and operation history for the selected seat in seat-manager/src/api/server.ts:2846. A degraded CL watcher leaves the seat stuck in DEPOSITED or SEEN_BY_CL; the visible signal is no new CL evidence, no activation epoch, and a health page that still reports only the EL cursor because the current health endpoint does not expose a separate CL cursor in seat-manager/src/api/server.ts:2655.

Known Gaps

Do not treat these as shipped features: full onboarding OT test-plan matrix, production issuer/JWKS lifecycle plumbing beyond HS256 or pinned Ed25519 public key, multi-source EL/CL quorum with durable reorg replay evidence, and migration phase-2 key retirement. The current gap list is the source of truth in seat-manager/README.md:73.