feat(cluster-tool): per-producer finalizer keys, genesis producer registration, and the producer-registration flow (WIRE-367) - #94
Open
heifner wants to merge 8 commits into
Conversation
…istration, and the producer-registration flow (WIRE-367)
A cluster could never publish a ranked schedule. `update_ranked_producers`
schedules only producers with an active finalizer key, and every producer on a
node shared one — `regfinkey` enforces global key uniqueness, so only the first
could ever register — while genesis producers got an account and nothing else:
no `regproducer`, no `regfinkey`, no opreg row. Every flow ran on whatever
`setprodkeys` stamped at bootstrap.
Each producer ACCOUNT now owns a BLS finalizer key (the block-signing K1 stays
node-shared; `setprodkeys` maps every producer on a node to it). That turns
`NodeopOptions.operator` into `operators: OperatorAccount[]` — one
`--producer-name` and one finalizer provider per hosted account — and moves the
genesis finalizer policy onto those accounts, because
`update_ranked_producers` rebuilds it from exactly those keys and a node-keyed
policy would be replaced by one the node holds no key for: LIB freezes and the
pending schedule never activates.
Bootstrap ordering follows from that. Identities are materialized in a new
`ProducerIdentities` phase before any node starts, since a producing node
renders its accounts' signature providers at launch (and, under SSM, fetches
them — so their publication moves to the beforeNodes phase). `Finality` stays
ahead of `SystemContract`, because `bios::setfinalizer` stops existing once the
system contract replaces the bios code. `GenesisProducerRegistration` grants RAM
then registers each producer and its key; the opreg half waits for
`GenesisProducerOperators`, after sysio.opreg is deployed and configured.
New: `Steps.contracts.sysio.system.plan{Regproducer,Regfinkey,Actfinkey,
Unregprod,Setscorecfg,Setacctram}` (each producer action signed by the producer
its own data names, derived rather than carried beside it);
`Steps.consensus.plan{RegisterProducer,RegisterFinalizerKey,GrantProducerRam}`
for the keys-from-the-store paths; and `ProducerNodeTool`, which starts and
stops an ad-hoc producing node the way `OperatorDaemonTool` starts a daemon.
`WireOperatorProvisioningTool` routes a producer with no `producerNodeIndex`
through the collateral-backed path — sponsored account, authex links,
`regoperator`, plus its own finalizer key.
`flow-producer-registration` drives a fresh account from provisioning to
producing blocks and back out: registered-but-unbonded stays UNKNOWN and
produces nothing, both bonds flip it ACTIVE, it enters the ranked schedule and
produces, its node is stopped until demotion fires at exactly the threshold,
`regproducer` recovers it, and withdrawing the bond takes it out again.
Change-Id: I5a92bc26a7483b0f1f1b00dde56248bf1536de13
…eads, schedule-based flow asserts (WIRE-367) Review fixes on #94: - NodeConfig.createAdHoc / adHocNodeName / peerEndpointFor / producerPeerEndpoints / AdHocIndex / NodeNamePrefix: the one composition OperatorDaemonTool and ProducerNodeTool share. BindConfigProvider. findAvailableAdHocPorts with DefaultAdHocHttp/P2p in the daemon layout (the old 8988/9976 sat inside the reserved agave band, so the preference never applied). - WireClient.activeAuthorization is the one spelling of <account>@Active; getProducers and getProducerSchedule are typed reads. - ConsensusSteps runners resolve from the key store and delegate to the SystemContractSteps runners; Steps.consensus.ProducerRamBytes is the one RAM grant; KeySteps.keyGeneratorContext replaces four hand-built contexts. - Constants.MIN_SCHEDULE_SIZE; ProtocolTiming.BlockIntervalMs / ProducerRepetitions / producerRotationMs / OutpostWriteBudgetMs; utils/producerRankUtils decodes the tier off rank_score. - flow-producer-registration installs setscorecfg, asserts on the ACTIVE schedule via get_producer_schedule instead of head_block_producer, and checks the healthy tier off rank_score. - EMISSION_CONFIG_DEFAULTS carries standby_bps: 800. - Tests for every new or changed symbol, the two-producer NodeopProcess argv, the producer node stop runner, the identity phase, and the collateral-route producer; the ConsensusSteps runner tests live in ConsensusSteps.test.ts. Change-Id: I9b98755b5e348f8c88e1d75b9ead9206d271e8c7
… deadlines, SSM publication, hosted-producer key guard (WIRE-367) - ProtocolTiming.ScheduleRebuildIntervalMs (60 s) joins the flow's schedule and demotion deadlines: the ranked schedule is rebuilt at most once per 120 slots, so a deadline sized on rotations alone undershot by one rebuild tick. - verifyProducerLeavesSchedule asserts the demoted producer is gone AND that the schedule holds only genesis producers with at least MIN_SCHEDULE_SIZE entries; the row read after demotion is null-guarded. - WireOperatorProvisioningTool: a collateral-backed PRODUCER's phase reports under the Producer actor, defaults to a non-bootstrapped registration, and a spec naming both producerNodeIndex and ethereumHdIndex is refused. - NodeopProcess.create refuses a node whose hosted producer accounts do not share one block-signing K1. - SSM: a flow-provisioned operator's freshly materialized keys are published by its own provisioning phase (K1/EM/ED, plus BLS for a producer) through the same renderer the bootstrap walker uses, for labels the bootstrap does not publish itself; runPublishSignatureProviderKey refuses an existing parameter that holds a different key instead of retaining a stale copy. - Stale comments in ClusterState, ClusterBuild, and the external-config test. Change-Id: I548be9406604c0ad4877f6f26fdc606a0924a875
…tscorecfg (WIRE-367) `prodscorecfg` gained the rolling window a producer's miss RATE is measured over and the percentage limit that goes with it, mirroring the batch-operator availability gates. Both are required fields on the generated action type, so every payload that installs a score config carries them. The flow's snapshot weight now matches what the contract ships, a tenth of collateral rather than parity, so the config it installs stays representative of production. It changes none of the flow's assertions: no producer in it holds an attestation, so the factor is zero either way. Worth knowing for the next ABI change here: the first `pnpm build` after regenerating passed while this config was still missing both new fields, because `tsc -b` kept a stale output for the dependent package. `pnpm clean` first, or the error arrives as a runtime serialization failure in the gate instead of a compile error. Change-Id: Id9290f06f512a0bdb6150e41c538bdea3b47b180
…t the old one (WIRE-367) The flow asserted that `regproducer` clears both the demotion AND the miss streak. That was the old behaviour; the streak now deliberately survives re-registration, because re-registering costs nothing but a signature and can be repeated, so clearing it there would let an absent operator call it on a timer and never produce a block. The gate caught this as a failed `demotion-cleared` step -- the assertion was encoding a contract the contract no longer offers. It now checks both halves of the rule, which is stronger than what it replaced: re-registration must clear the demotion and must NOT clear the streak, and the block the producer subsequently makes is what clears it. That second half is a new step, so the flow now proves the recovery path end to end rather than assuming it. Change-Id: I7d6ef7550a2e8e5ee36c83e4f6f4d31ab1e2ea79
…identity (WIRE-367) `min_blocks_per_round` rides `setscorecfg`. The producer-registration flow sets it to 0 -- the disabled spelling -- because it demotes by stopping a node outright, so every round it charges is a whole round with no blocks at all; leaving the check armed would make the assertions depend on how many blocks the node happened to land before it died. `NodeopProcess.create` now asserts the hosted operators ARE the node's producers, not merely that there are the same number of them. `buildArgs` renders `--producer-name` from the operators while the key store, the SSM secret ids and start.sh all read `node.producers`, so matching counts alone let a caller hand over the right number of the wrong accounts -- or the right accounts in a different order -- and the node would sign for producers it holds no slot for, silently. `NodeConfig.createAdHoc` now stores the key-store LABEL there, as `plan` does; an on-chain account in that field reads as a label that resolves to nothing. `producerTier` refuses a tiered `rank_score` rendered as a number rather than decoding a rounded one. Every non-healthy tier sets bit 62 or 63, so such a value is far past the safe-integer range: the demoted tier's `unscored()` rounds UP across the tier boundary and reads as a tier the contract never packs -- on a correctly demoted producer, which is exactly what the flow asserts on. Change-Id: Ice0bdfb4a39f1eb6823e5a5d99fbb63c3030bfc7
…abel alone (WIRE-367) The identity assertion added with the node/operator pinning rejected the BIOS node and took every e2e flow down with it: `node.producers` is not uniformly one identifier. A planned producer node carries key-store labels -- and a producer never calls `roa::newuser`, so its label IS its account -- while the bios node carries the ON-CHAIN genesis producer name `sysio` under an operator labelled `node_bios`. Demanding labels failed at `start-bios`, which aborted every downstream phase, so all fifteen flows died in about two seconds without reaching any of their own assertions. Each producer name is now matched by an operator on EITHER its label or its account, which still catches the defect the assertion exists for -- the right NUMBER of the wrong accounts, or the right accounts in a different order. The unit tests missed this because the `producerOperator` fixture sets `label = account`, so every shape exercised had the two identifiers identical -- precisely the condition under which the bug is invisible. Two tests now pin the shapes that differ: the bios node is accepted with `label != account` and still renders `--producer-name sysio`, and an operator matching by neither is rejected. Change-Id: Ia77daf62129812fc400d8ce530ed2f3b0ec88972
… (WIRE-367) Under SSM the bootstrap wrote a live BLS private key into every replication region for each producer node, at `/wire/<cluster>/node_NN/BLS`, that no daemon ever read. Finality moved to per-ACCOUNT finalizer keys in this work -- a shared node key cannot pass `regfinkey`'s global uniqueness check -- and both former readers moved with it, but the publishing side did not. Tracing what a node actually resolves showed the whole node identity was dead, not just its BLS half. `buildArgs` renders every `--signature-provider` spec from an ACCOUNT's `publicationLabel`: the block-signing K1 from the first hosted account, one finalizer BLS per hosted account. A `node_NN` parameter is fetched by nobody, and the producer-ACCOUNT rows already republish that same K1 under each account's own label -- which is the point of publishing per identity. The bios node is not an exception: its identity is an operator row whose label IS the node name its daemon renders. Removing the row left `SignatureKeySource` single-valued and `nodeIndex` write-only, so both go, and the key resolver collapses to the one lookup it always was. Every published identity is an operator account keyed by its own label. The cross-check test asserted [K1, BLS] per producing node -- an assumption, and the wrong one. It now DERIVES the expected ids from what `buildArgs` renders, and names the bios label divergence explicitly, so it fails when a rendered spec has no publication rather than when a guess about node keys stops holding. Note the coverage limit: these steps are composed only when `signatureProvider.type === SSM`, and the e2e gate runs KEY mode, so the gate exercises none of this. The unit tests are the whole safety net until a cluster workflow boots an SSM cluster against it. Change-Id: I392a8ab918b6f79ec99a2cb7c7b0b957ef532faf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of WIRE-367. Gives the harness what it needs to stand up a real block producer, and adds the flow that drives one from a fresh account to producing blocks and back.
Per-producer BLS finalizer keys
The harness minted one K1+BLS set per producer node and every producer account on that node shared it.
regfinkeyenforces a global duplicate-key check, so N accounts sharing one BLS key cannot all register — which meantupdate_ranked_producerscould never publish, and no harness cluster had ever run on a ranked schedule. Every flow to date ran on whateversetprodkeysstamped at bootstrap.BLS keys are now per producer account (
OperatorAccount.wireFinalizer); the K1 block-signing key stays node-shared, since it carries no uniqueness constraint.NodeopProcesstakesoperators: OperatorAccount[]and emits one--producer-nameplus its BLS--signature-providerper hosted producer, and the genesis finalizer policy is built from producer keys rather than node keys. Because that K1 is shared by construction,NodeopProcess.createnow refuses a node whose hosted producer accounts do not all carry the same one.Bootstrap ordering
Three constraints, each of which cost a failed flow run to find, now pinned by tests in
ClusterBuildDefaultsGenesisProducers.test.ts:ProducerIdentitiesmust precedeProducerNodes. A producing node renders one--signature-providerper hosted account at launch, so the keys must exist by then. Key material is a separate phase from thenewaccountwrite, which cannot run untilsysio.systemis deployed. Symptom:ClusterKeyStore: operator "defproducera" has not been provisioned.Finalitymust stay beforeSystemContract.bios::setfinalizeris a BIOS-ABI action and stops existing once the system contract replaces bios code. Symptom:Unknown action setfinalizer in contract sysio.regoperatorfor genesis producers waits forOPPConfig—sysio.opregisn't deployed untilOPPContracts.Plus one non-ordering trap:
regfinkeybills its rows to the producer, and neither route to a producer account leaves room.Steps.consensus.planGrantProducerRamgrants it, keyed by handle because a sponsored producer's on-chain name is generated at run time. Symptom:Account using more than allotted RAM usage.New surface
SystemContractSteps—planRegproducer,planRegfinkey,planActfinkey,planUnregprod,planSetscorecfg,planSetacctram, all on the generated action types —planSetscorecfgcarries the rolling-window gate (missed_round_window_ms,max_pct_missed_rounds_in_window) alongside the consecutive threshold.ProducerNodeTool— spawns an ad-hoc producing node outsideNodeConfig.plan, ports fromBindConfigProvider.findAvailable.WireOperatorProvisioningTool— a PRODUCER withoutproducerNodeIndexnow routes to the collateral-backed OPP-operator path (unique K1/EM/ED plus a BLS finalizer key of its own, sponsored account, ETH+SOL funding, authex links on both chains,regoperator). A spec naming bothproducerNodeIndexandethereumHdIndexis refused, since genesis and collateral-backed are mutually exclusive routes.ProtocolTiming.ScheduleRebuildIntervalMs— the ranked schedule is rebuilt at most once per 120 slots, so the flow's schedule and demotion deadlines derive from it rather than from producer rotations alone.SSM key custody
A producer NODE now publishes nothing under its own name. Finality moved to per-account
finalizer keys, so
buildArgsrenders every--signature-providerspec from an ACCOUNT'spublicationLabel— the block-signing K1 from the first hosted account, one finalizer BLSper hosted account — and the producer-account rows already republish that K1 under each
account's own label. The
node_NNparameters were fetched by nobody, so publishing them wrotelive signing keys into every replication region for no reader. Removing the row left
SignatureKeySourcesingle-valued andnodeIndexwrite-only, so both go and the key resolvercollapses to the one lookup it always was. The bios node is not an exception: its identity is an
operator row whose label IS the node name its daemon renders.
The cross-check test asserted
[K1, BLS]per producing node — an assumption, and the wrong one.It now DERIVES the expected ids from what
buildArgsrenders and names the bios label divergenceexplicitly, so it fails when a rendered spec has no publication rather than when a guess about
node keys stops holding. Worth knowing: these steps are composed only under
--signature-provider-type SSM, which the e2e gate does not exercise, so the unit tests are thewhole safety net until a cluster workflow boots an SSM cluster against it.
Two gaps under
--signature-provider-type SSM, both invisible under the default KEY mode:SSM:specs pointing at parameters nothing had written. Its provisioning phase now publishes them itself, through the same renderer the walker uses, and only for labels the walker does not already cover.The flow
flow-producer-registration, 9 phases: provision → negative case (registered but unbonded never enters the schedule) → ETH deposit → SOL deposit → start node → enters schedule and produces a block → missed rounds drive demotion →regproducerclears the demotion while the miss streak survives → producing a block clears the streak → withdraw below minimum removes it. The two recovery assertions are separate on purpose:regproduceris a free signature, so it re-opens the schedule without pardoning the record, and only producing retires the streak. Assertions read the active schedule viaget_producer_schedulerather thanhead_block_producer, and the exit check requires the demoted producer to be absent while the schedule still holds only genesis producers at or aboveMIN_SCHEDULE_SIZE. Picked up by dynamic discovery, so no workflow edit.Validation
pnpm build,pnpm run lint, and 2070 tests across 229 suites.emissions-soak, where genesis producers newly drawing producer pay would have surfaced.CI
Build and test is red here until Wire-Network/wire-libraries-ts#80 merges and publishes. This repo resolves
@wireio/sdk-corefrom the registry, and the published version predates the WIRE-367 ABI: every error is a missingsetscorecfg,rank_scoreorstandby_bps. No code change here will fix it, and none is needed — the sibling-linked build used by the e2e gate above is green.Merge order
Last. After Wire-Network/wire-sysio#599 and Wire-Network/wire-libraries-ts#80.