Skip to content

Design Patterns

What this document covers

How patterns from Lido and StakeWise map to the Centurion Seat Manager design. For each pattern: what we adopt, what we skip, and why.


1. Lido: Operator Registry

Source: NodeOperatorsRegistry.sol

Lido Pattern Centurion Equivalent
Sequential numeric operator IDs seats table uses auto-increment id + operator_id string
48-byte pubkey validation We enforce 48-byte pubkey + 32-byte withdrawal credentials
Curated onboarding (DAO role holders add operators) Seats are created then approved (allowlisted) by the contract owner — same "propose then vet" pattern
Counter invariants (total >= vetted >= deposited >= exited) Seat status progression CREATED → ALLOWLISTED → DEPOSITED → SEEN_BY_CL → ACTIVE enforces a similar monotonic lifecycle
Lido Pattern Why We Skip It
Aragon ACL role-based permissions Protocol-specific — Centurion uses simple owner-only calls with CLI-level PROPOSE/SEND separation
Multiple signing keys per operator Each (pubkey, wc) pair is a distinct seat with its own intent hash — simpler, more auditable
Packed key storage assembly (SigningKeys.sol) Gas-optimized on-chain storage is unnecessary — we use SQLite rows off-chain

Concept Mapping

Lido Centurion
addNodeOperator(name, addr) seat create --operator <id> --pubkey <pk> --wc <wc>
setNodeOperatorStakingLimit() seat approve <id> (allowlists the intent)
vettedSigningKeysCount Seat status ALLOWLISTED
depositedSigningKeysCount Seat status DEPOSITED (observed via EL watcher)
Key removal seat revoke <id>removeAllowedDeposit or operational revoke

2. Lido: Module Routing / Gradual Opening

Source: StakingRouter.sol

Lido Pattern Centurion Equivalent
Reversible vs irreversible transitions (Active/Paused/Stopped) setPubkeyAllowlistEnabled(bool) = reversible toggle; disablePubkeyAllowlistForever() = irreversible
Governance timelocks for critical changes scheduleDisablePubkeyAllowlistForever(delay) with notBefore timestamp + CLI gates (--dangerous, CONFIRM_FREEZE)
Guardian attestations before deposits (DepositSecurityModule) Cross-check preflight on 2+ EL endpoints before any dangerous action
Lido Pattern Why We Skip It
Stake share limits / basis points Percentage-based allocation is Lido protocol logic — Centurion has binary allow/deny per intent
Fee distribution (module + treasury splits) Irrelevant — Centurion seats don't involve stETH or rewards routing
Multiple staking modules Centurion has one deposit contract, not a router

Concept Mapping

Lido Centurion
Module Active/Paused setPubkeyAllowlistEnabled(true/false)
Module Stopped (irreversible) disablePubkeyAllowlistForever()
Governance timelock scheduleDisablePubkeyAllowlistForever(delay) + notBefore check
Guardian attestations Cross-check preflight on 2+ distinct EL endpoints

3. StakeWise: Operator Service Daemon

Source: v3-operator/src/

StakeWise Pattern Centurion Equivalent
Block-driven task loop (BaseTask with SECONDS_PER_BLOCK sleep) EL watcher: 12s poll with bounded backoff
Multi-endpoint support (EXECUTION_ENDPOINTS, CONSENSUS_ENDPOINTS) Endpoint discovery from deploy-node.sh with distinct-client preference
Graceful shutdown (InterruptHandler) AbortController with SIGINT/SIGTERM handlers
SQLite for local state (per-network tables, checkpoint tracking) Same pattern: seats, deposits, cl_observations tables
Environment-based secrets (wallet keys from env vars) SEAT_MANAGER_OWNER_PRIVATE_KEY — never stored
StakeWise Pattern Why We Skip It
Python async / Web3.py We use TypeScript + viem for type safety and ecosystem fit
Subgraph dependency We read directly from chain + beacon API
Mnemonic management (BIP-39 / EIP-2334) Centurion seats receive pre-existing pubkeys — key generation is out of scope
HashiCorp Vault / remote signer Over-engineered for our scope — local-env-key only

Concept Mapping

StakeWise Centurion
BaseTask.process_block() runElWatcher() / runClWatcher() poll loops
InterruptHandler AbortController + SIGINT/SIGTERM
Settings singleton (env) loadAppConfig() + loadSignerConfig()
NetworkValidatorCrud (SQLite) SeatStore (SQLite via better-sqlite3)
ValidatorRegistrationSubtask seat approve (allowlists intent on-chain)
Multi-endpoint config Endpoint discovery from deploy-node.sh

4. StakeWise: CLI

Source: stakewise_cli/

StakeWise Pattern Centurion Equivalent
Click-style command groups with subcommands Commander.js: seat create, seat approve, switch on, freeze schedule, etc.
Deposit data verification via Merkle roots Intent hash verification (keccak256(pubkey || wc)) + cross-check preflights
StakeWise Pattern Why We Skip It
IPFS upload + governance forum specs We use local evidence bundles under ./evidence/
Shamir secret sharing Committee-based key custody is unnecessary for Centurion's direct owner model

Concept Mapping

StakeWise CLI Centurion
create-deposit-data seat create (stores intent locally)
verify-deposit-data Preflight checks + intent hash validation
IPFS evidence ./evidence/*.json bundles (local, gitignored)

5. Key Design Principle

Centurion's model is fundamentally simpler

What Lido/StakeWise Have What Centurion Doesn't Need
Tokenomics (stETH, rewards, fee splits) No tokenomics
Key generation (BIP-39, EIP-2334) Pubkeys arrive externally — we only manage the allowlist
Multi-module routing One contract, one allowlist, binary allow/deny
Complex governance (Aragon, Shamir, IPFS) Evidence-first local audit trail

The Seat Lifecycle Maps 1:1 to On-Chain State

graph LR
    A[CREATED] -->|approve| B[ALLOWLISTED]
    B -->|deposit observed| C[DEPOSITED]
    C -->|CL visible| D[SEEN_BY_CL]
    D -->|2+ CL labels| E[ACTIVE]

Revocation is Honest

Timing What Happens
Pre-deposit removeAllowedDeposit on-chain + mark REVOKED
Post-deposit Mark REVOKED operationally (audit note only — validators can't be removed from the beacon chain via the deposit contract)

PROPOSE-ONLY default

The seat manager generates calldata and evidence without ever holding signing authority by default — minimizing the trusted code surface.