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.