Seat Lifecycle¶
The lifecycle states are exactly CREATED, ALLOWLISTED, DEPOSITED, SEEN_BY_CL, ACTIVE, EXCEPTION, and REVOKED. The type is declared in seat-manager/src/onboarding/store.ts:10.
stateDiagram-v2
[*] --> CREATED
CREATED --> ALLOWLISTED
ALLOWLISTED --> DEPOSITED
DEPOSITED --> SEEN_BY_CL
SEEN_BY_CL --> ACTIVE
CREATED --> EXCEPTION
ALLOWLISTED --> EXCEPTION
DEPOSITED --> EXCEPTION
SEEN_BY_CL --> EXCEPTION
ACTIVE --> EXCEPTION
EXCEPTION --> REVOKED
ACTIVE --> REVOKED
REVOKED --> [*]
| from | to | required evidence fields | who triggers |
|---|---|---|---|
CREATED |
ALLOWLISTED |
token_verified, bls_verified, canonical_tuple_hash |
register transaction, sourced from seat-manager/src/workflow/stateMachine.ts:12 |
ALLOWLISTED |
DEPOSITED |
deposit_tx_hash, deposit_block_number, deposit_block_hash, pubkey, withdrawal_credentials |
EL deposit watcher, sourced from seat-manager/src/workflow/stateMachine.ts:17 |
DEPOSITED |
SEEN_BY_CL |
cl_validator_index, pubkey, cl_state_id, cl_finalized_epoch, cl_observed_slot, cl_header_root, cl_observed_at_ms |
CL promotion watcher, sourced from seat-manager/src/workflow/stateMachine.ts:28 |
SEEN_BY_CL |
ACTIVE |
cl_validator_index, pubkey, cl_activation_epoch, cl_state_id, cl_finalized_epoch, cl_observed_slot, cl_header_root, cl_observed_at_ms |
CL promotion watcher, sourced from seat-manager/src/workflow/stateMachine.ts:41 |
CREATED |
EXCEPTION |
reason |
admin/runtime exception path, sourced from seat-manager/src/workflow/stateMachine.ts:55 |
ALLOWLISTED |
EXCEPTION |
reason |
admin/runtime exception path, sourced from seat-manager/src/workflow/stateMachine.ts:60 |
DEPOSITED |
EXCEPTION |
reason |
admin/runtime exception path, sourced from seat-manager/src/workflow/stateMachine.ts:65 |
SEEN_BY_CL |
EXCEPTION |
reason |
admin/runtime exception path, sourced from seat-manager/src/workflow/stateMachine.ts:70 |
ACTIVE |
EXCEPTION |
reason |
admin/runtime exception path, sourced from seat-manager/src/workflow/stateMachine.ts:75 |
EXCEPTION |
REVOKED |
governance_approval_id, governance_approval_hash, governance_approval_issuer, governance_approval_expires_at, governance_approval_verified_at, governance_approval_nonce, reason_code, actor |
admin revoke after governance approval, sourced from seat-manager/src/workflow/stateMachine.ts:80 |
ACTIVE |
REVOKED |
governance_approval_id, governance_approval_hash, governance_approval_issuer, governance_approval_expires_at, governance_approval_verified_at, governance_approval_nonce, reason_code, actor |
admin revoke after governance approval, sourced from seat-manager/src/workflow/stateMachine.ts:94 |
One Active Seat Per Operator¶
A seat is active for the operator uniqueness rule in every state except REVOKED; that is encoded in isActiveSeatStatus at seat-manager/src/onboarding/store.ts:19. The database backstop is uniq_seats_active_operator_id ON seats(operator_id) WHERE status <> 'REVOKED' in seat-manager/prisma/migrations/20260514165745_one_active_seat_per_operator/migration.sql:37. A revoked seat is terminal and does not block re-enrollment of the same operator into a new seat.
Per-State Semantics¶
CREATED¶
CREATED is a reserved seat with only seat_id, operator_id, status, created_at, and updated_at populated; the nullable validator and vault columns on Seat remain empty at this point in seat-manager/prisma/schema.prisma:10. The row is written by seedCreatedSeat, either from startup seed JSON or an explicit store call, and the Postgres path rejects a second non-revoked seat for the same operator before insert in seat-manager/src/onboarding/prismaStore.ts:274.
No post-register operator API scope is useful yet because no OperatorApiKey row exists. The only valid operator action is enrollment with the JWT-backed wc-template and register endpoints, registered at seat-manager/src/api/server.ts:1446 and seat-manager/src/api/server.ts:1594. Normal wait time is an operational queue outside the codebase: it lasts until the operator runs the console ceremony. The next state requires the token, BLS proof, and canonical tuple hash evidence declared in seat-manager/src/workflow/stateMachine.ts:12.
What goes wrong here is provisioning conflict or token misuse. A duplicate non-revoked operator seat fails with OPERATOR_ALREADY_HAS_ACTIVE_SEAT in seat-manager/src/onboarding/prismaStore.ts:296, a register attempt against a different operator fails with SEAT_OPERATOR_MISMATCH in seat-manager/src/onboarding/prismaStore.ts:885, and a runtime/admin exception can move the seat to EXCEPTION with only reason evidence under seat-manager/src/workflow/stateMachine.ts:55.
ALLOWLISTED¶
ALLOWLISTED is the registered state. The register transaction fills registered_at, pubkey, beneficiary, vault_address, withdrawal_credentials, canonical_tuple_hash, vault_config_version, nonce_consumed, and operator_api_key_expires_at on Seat in seat-manager/src/onboarding/prismaStore.ts:976. The same transaction writes NonceLedger and OperatorApiKey rows in seat-manager/src/onboarding/prismaStore.ts:967 and seat-manager/src/onboarding/prismaStore.ts:1005.
The useful default scopes are status, claim, and rotate_api_key, but claim has no useful on-chain effect until the vault exists and claim policy admits a claim. The scope set is declared in seat-manager/src/onboarding/store.ts:191, and the default issue set is declared in seat-manager/src/onboarding/store.ts:201. Normal wait time is worker-dependent: the operation worker must execute the queued deposit operation, using SEAT_MANAGER_WATCH_POLL_MS default 1000 milliseconds in seat-manager/src/api/runtime.ts:320.
The next state requires a deposit transaction hash, deposit block number, deposit block hash, pubkey, and withdrawal credentials in seat-manager/src/workflow/stateMachine.ts:17. The operation worker can write that evidence when a deposit execution result is finalized in seat-manager/src/watchers/operationWorker.ts:64, and the EL watcher can write it from an accepted DepositEvent in seat-manager/src/watchers/lifecycleWatchLoop.ts:479. Failures include a failed deposit operation, canonical tuple mismatch, or a manual/runtime exception; the generic exception transition is declared in seat-manager/src/workflow/stateMachine.ts:60.
DEPOSITED¶
DEPOSITED keeps the same Seat columns as ALLOWLISTED; the new information is not stored as new seat columns, it is stored in LifecycleEvidence with to_status = DEPOSITED in seat-manager/prisma/schema.prisma:140. The transition helper updates only Seat.status and updated_at, then creates the evidence row in seat-manager/src/onboarding/prismaStore.ts:1116 and seat-manager/src/onboarding/prismaStore.ts:1130.
The useful operator scopes are status, claim, rotate_api_key, and only when explicitly granted, voluntary_exit or migrate. Exit preflight still depends on on-chain readiness and exit trigger state checked in seat-manager/src/api/server.ts:977. Normal wait time before SEEN_BY_CL depends on beacon finality and watcher cadence; the CL watcher polls when SEAT_MANAGER_CL_BEACON_API_URL is set and uses the configured SEAT_MANAGER_CL_STATE_ID in seat-manager/src/watchers/lifecycleWatchLoop.ts:79.
The next state requires validator index, pubkey, CL state id, finalized epoch, observed slot, header root, and observed timestamp in seat-manager/src/workflow/stateMachine.ts:28. The watcher produces those fields after it fetches the validator and verifies the withdrawal credentials match the seat in seat-manager/src/watchers/lifecycleWatchLoop.ts:181 and seat-manager/src/watchers/lifecycleWatchLoop.ts:188. A reorg can quarantine a deposited seat to EXCEPTION using evidence text beginning with reorg_quarantine in seat-manager/src/watchers/lifecycleWatchLoop.ts:293; otherwise a missing CL record leaves it stuck without transition.
SEEN_BY_CL¶
SEEN_BY_CL means the validator record is present on the beacon API. The Seat row still has the same populated columns from register; CL identity and header evidence live in LifecycleEvidence, not extra Seat columns, as defined by seat-manager/prisma/schema.prisma:140. The watcher also writes WatcherState.stateKey = cl:seen_epoch:<seatId> so it can enforce the minimum epochs before active promotion in seat-manager/src/watchers/lifecycleWatchLoop.ts:216.
The useful operator scopes are status, claim, and rotate_api_key; voluntary_exit and migrate only work for keys that include those scopes and for vaults that pass exit preflight. Normal wait time before ACTIVE is at least the CL promotion delay; default delay is 2 epochs from SEAT_MANAGER_CL_MIN_SEEN_EPOCHS_BEFORE_ACTIVE in seat-manager/src/watchers/lifecycleWatchLoop.ts:341. The watcher also requires the beacon status to equal active or start with active_ in seat-manager/src/watchers/lifecycleWatchLoop.ts:223.
The next state requires activation epoch plus the CL observation fields declared in seat-manager/src/workflow/stateMachine.ts:41. It is produced by markActive in seat-manager/src/watchers/lifecycleWatchLoop.ts:247. The state can stall if the beacon endpoint is down, head is configured in a production-like run, or the validator is visible but not active; those skips are implemented in seat-manager/src/watchers/lifecycleWatchLoop.ts:168 and seat-manager/src/watchers/lifecycleWatchLoop.ts:245. Reorg quarantine can also move it to EXCEPTION in seat-manager/src/watchers/lifecycleWatchLoop.ts:300.
ACTIVE¶
ACTIVE is the steady-state operator lifecycle state. The Seat row carries the registered pubkey, beneficiary, vault address, withdrawal credentials, canonical tuple hash, vault config version, nonce, API-key expiry, and current status as defined in seat-manager/src/onboarding/store.ts:28. Activation evidence lives in LifecycleEvidence, and admin seat detail reads both operation history and lifecycle history for support triage in seat-manager/src/api/server.ts:2853.
The useful scopes are all five valid OperatorApiScope values when issued: status, claim, voluntary_exit, migrate, and rotate_api_key in seat-manager/src/onboarding/store.ts:191. The default key does not include exit or migrate, so those require a key minted with the opt-in scopes from a backend/admin process. Normal steady-state has no automatic next transition until an exception or revocation; day-2 operations create Operation rows through claim, exit, migrate, or rotation routes in seat-manager/src/api/server.ts:2077, seat-manager/src/api/server.ts:1984, seat-manager/src/api/server.ts:2222, and seat-manager/src/api/server.ts:2315.
The next live transition is usually ACTIVE -> REVOKED with governance evidence, or ACTIVE -> EXCEPTION with a reason. The state machine requires only reason for an exception at seat-manager/src/workflow/stateMachine.ts:75, and full governance approval evidence for revocation at seat-manager/src/workflow/stateMachine.ts:94. Problems in this state include failed operation rows, stale risk data, pending claim conflicts, exit preflight failures, and reorg quarantine; status exposes open exceptions through the console output path in staker-console/src/index.ts:163.
EXCEPTION¶
EXCEPTION is a stop state for manual review. The Seat row keeps its existing populated columns and changes only status and updated_at; the reason is stored in LifecycleEvidence.evidence_json by transitionSeatStatus in seat-manager/src/onboarding/prismaStore.ts:1130. The source can be admin action, runtime transition, or reorg quarantine, and the valid incoming exception transitions are declared in seat-manager/src/workflow/stateMachine.ts:55.
The useful operator scope is status; claim, exit, and migrate are not normal recovery tools until an admin has reviewed the exception and decided whether to revoke or repair source data. There is no automatic wait time to recovery. The current implementation documents launch-grade recovery gaps in the README list, including durable reorg rollback/replay evidence, at seat-manager/README.md:73.
The only documented terminal transition is EXCEPTION -> REVOKED, which requires governance approval id, hash, issuer, expiry, verified-at timestamp, nonce, reason code, and actor in seat-manager/src/workflow/stateMachine.ts:80. A stuck EXCEPTION without valid governance evidence remains stuck; attempts to force an unsupported transition fail in assertTransitionAllowed in seat-manager/src/workflow/stateMachine.ts:123.
REVOKED¶
REVOKED is terminal for the seat. The row remains in Seat, the LifecycleEvidence row records the revocation evidence, and the one-active-seat-per-operator rule stops counting it because isActiveSeatStatus returns false only for REVOKED in seat-manager/src/onboarding/store.ts:24. The database enforces the same rule with the partial index in seat-manager/prisma/migrations/20260514165745_one_active_seat_per_operator/migration.sql:37.
No operator API scope should be used for new work on this seat. status can still be used for visibility while the key remains live, but operations against a revoked seat are operational mistakes. Normal wait time to the next seat is the Foundation Ops re-enrollment process, not an automatic backend transition. Revocation does not reverse the on-chain deposit or restore the validator key; the admin revoke response says on-chain state was not reversed in seat-manager/src/api/server.ts:3102.
The evidence needed to enter REVOKED is the governance approval set in seat-manager/src/workflow/stateMachine.ts:80 or seat-manager/src/workflow/stateMachine.ts:94. What goes wrong is incomplete approval, missing step-up, missing CSRF, or a wrong lifecycle status; the revoke route requires ADMIN, CSRF, fresh step-up, idempotency, and governance verification in seat-manager/src/api/server.ts:2985.
Recovering From EXCEPTION¶
- Identify the source. The operator runs
staker-console-v2 status --seat-id <seat>and checksexceptions:printed by the CLI instaker-console/src/index.ts:163. The admin opens the seat detail page, which loads the seat, lifecycle evidence, operations, audit events, on-chain evidence, and economics from/admin/v1/seats/:seatIdinseat-manager/src/api/server.ts:2846. - Classify the exception. If the evidence reason starts with
reorg_quarantine, inspect EL watcher logs and theel:last_scanned_blockcursor because quarantine is written byseat-manager/src/watchers/lifecycleWatchLoop.ts:317. If the reason starts withadmin_revoke_requested, continue the revoke flow because the route has already moved a non-active seat intoEXCEPTIONas the first leg of revoke inseat-manager/src/api/server.ts:3049. - Decide repair or revoke. The current state machine has no generic
EXCEPTION -> ACTIVEorEXCEPTION -> DEPOSITEDtransition; unsupported transitions fail inseat-manager/src/workflow/stateMachine.ts:123. A repair that returns a seat to a live state is therefore not documented in the current implementation; the gap is tracked inseat-manager/README.md:73. - For revocation, gather governance evidence. The admin route verifies the approval token and extracts approval id, hash, issuer, expiry, verified-at timestamp, and nonce before writing revocation evidence in
seat-manager/src/api/server.ts:3041andseat-manager/src/api/server.ts:3060. - Communicate the result. A revoked seat no longer blocks a new seat for the same operator because the application rule and database backstop exclude
REVOKEDinseat-manager/src/onboarding/store.ts:19andseat-manager/prisma/migrations/20260514165745_one_active_seat_per_operator/migration.sql:37.