WNE-41: derive and supply the Ethereum outpost's genesis batch-operator roster - #87
WNE-41: derive and supply the Ethereum outpost's genesis batch-operator roster#87brianjohnson5972 wants to merge 4 commits into
Conversation
|
E2E flow tests dispatched → https://github.com/Wire-Network/wire-platform-build-system/actions/runs/33407253427 Branch combination under test (everything else floats to the manifest defaults):
|
…or roster Change-Id: I4aa55e1b880e08a2fc80e8fb4d3de3ec286928dd
wire-sysio PR #578 ("fix/drop-daemon-enable-flags") REMOVED both options;
enablement now derives from the configured account, the way producer_plugin
keys off producer-name. The harness kept passing them, and nodeop rejects
unknown options outright, so every operator daemon failed to start: no batch
operator delivered, no OPP circulated, and every flow died in bootstrap.
e2e 33105055065 was 0/14 with exactly that signature — all fourteen flows
failing fast and identically, while the platform build itself passed. Neither
WNE-41 branch touches daemon flags; the break came in when the run floated
wire-sysio to master.
`--batch-operator-account` / `--underwriter-account` were already being passed
on the following lines, so the new enable mechanism was satisfied all along.
The two assertions that required the flags are inverted to `toEqual([])`, so
reintroducing either fails the suite instead of silently disabling every
daemon. Three arbitrary extraArgs fixtures naming --batch-enabled are swapped
for a live option so they stop reading as documentation of a real flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Change-Id: I39644a49882640a8f21af2314e12f8b72944d7b6
27e7d13 to
482756c
Compare
…test The master merge kept both sides' `extraArgs` property on the non-producing-node case. The first resolution left them without a separator (TS1005 at 195,7, which took the e2e build down); the second added the comma but left two properties with the same name — TS1117, which fires even under `strict: false`, and which would have dropped `--batch-operator-account` from the literal and failed the `arrayContaining` assertion below it. Both sides' flags belong in one array; the assertion already expects all four entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Change-Id: I13ce5a4a9db53468c73b05e1e3ea091a1561f27d
jglanz
left a comment
There was a problem hiding this comment.
Rename genesis -> initial EVERYWHERE, genesis already has a discreet meaning
| export function batchOperatorEthereumHdIndex(index: number): number { | ||
| return index + 1 | ||
| } | ||
|
|
||
| /** | ||
| * HD account index for an underwriter's EM key — continues past the batch | ||
| * operators. See {@link batchOperatorEthereumHdIndex}. | ||
| * | ||
| * @param batchOperatorCount - Size of the batch-operator roster. | ||
| * @param index - Zero-based underwriter index. | ||
| * @returns The HD account index. | ||
| */ | ||
| export function underwriterEthereumHdIndex( | ||
| batchOperatorCount: number, | ||
| index: number | ||
| ): number { | ||
| return batchOperatorCount + index + 1 | ||
| } |
There was a problem hiding this comment.
both of these should use the label prefix filtered - the index is guaranteed
| extraArgs: [ | ||
| "--batch-operator-account", | ||
| "wireno.batchopaaaa", | ||
| "--batch-epoch-poll-ms", | ||
| "500" | ||
| ] |
There was a problem hiding this comment.
remove change, its unrelated and requires clear reasoning
| "wireno.batchopaaaa", | ||
| "--batch-epoch-poll-ms", | ||
| "500" |
| const relaunchNode = node("relaunch", NodeRole.producer, ["sysio"]), | ||
| operator = producerOperator("sysio"), | ||
| extraArgs = ["--batch-enabled", "true"] | ||
| extraArgs = ["--batch-epoch-poll-ms", "500"] |
There was a problem hiding this comment.
same as previous, also, I believe --batch-enabled is removed in master
| /** A valid WNE-41 genesis roster — one operator, one positive duration. */ | ||
| genesisRoster: EthereumOutpostGenesisRoster = { | ||
| groups: [[AnvilAccount0Address]], | ||
| epochDurationSec: 60 |
There was a problem hiding this comment.
Magic numbers are not aloud, and i believe there is a global constant elsewhere
… HD index jglanz's review on wire-tools-ts#87: * "Rename `genesis` -> `initial` EVERYWHERE, `genesis` already has a discreet meaning" — renamed the WNE-41 roster vocabulary (EthereumOutpostGenesisRoster -> EthereumOutpostInitialRoster, genesisRoster -> initialRoster, resolveGenesisRoster -> resolveInitialRoster) and the cross-repo deploy-config key genesisOperatorGroups -> initialOperatorGroups, which moves in lockstep with the wire-ethereum companion branch. The chain's own genesis vocabulary is deliberately untouched: genesisTimestamp, genesisFile, the bios/finalizer dev-key comments, "genesis policy" and "starts at genesis" are the discreet meaning the review is protecting. * Constants.ts "both of these should use the label prefix filtered - the index is guaranteed" — batchOperatorEthereumHdIndex(index) and underwriterEthereumHdIndex(batchOperatorCount, index) are replaced by ONE ethereumHdIndex(label). The label's PREFIX selects the role's HD band and its letter gives the ordinal, so no caller supplies a roster size and the two roles cannot collide. The prefixes are now named constants (BatchOperatorLabelPrefix / UnderwriterLabelPrefix) that the label builders share, rather than literals spelled twice. Bands are sized to the label space itself (LowercaseAlphabet), which is what makes a label alone sufficient. Two constants follow from that and are derived, not re-guessed: SwapUserIdentities.DefaultEthereumHdIndex is now Constants.FirstNonOperatorHdIndex (was a bare 32, which the new underwriter band would have overlapped), and AnvilProcess.AccountCount tracks the same floor so every operator band stays pre-funded — its JSDoc already promised exactly that. * NodeopProcess.test.ts "remove change, its unrelated" (x3) — reverted the file to master. Those three edits came from the unrelated --batch-enabled/--underwriter-enabled commit, whose only contribution to this branch was that file. Note --batch-enabled is still on master (NodeopProcess.test.ts:823, OperatorDaemonTool.test.ts:401). * EthereumOutpostBootstrapper.test.ts "magic numbers are not aloud" — epochDurationSec: 60 -> ClusterConfigProvider.DefaultEpochDurationSec, at both fixture sites. The `0` in the non-positive-duration case stays a literal: it is the value under test. Verified: eslint clean; 135/135 suites, 1415/1415 tests (node 24). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Change-Id: I21c13b7199f820180052664cad3c12ae3ac967f2
jglanz
left a comment
There was a problem hiding this comment.
Brian you missed my point regarding indexes; the index #s and sort order of the cluster keys json can not be guaranteed, so when you are searching the dataset for a batchop or a uw, you need to filter first by the label prefix and then once you have an array that only has the batchops only then can you use the index.
I'd revert the latest changes and try again with the refined explanation.
| export const NonOperatorAccountHeadroom = 8 | ||
| export const AccountCount = | ||
| Constants.FirstNonOperatorHdIndex + NonOperatorAccountHeadroom |
There was a problem hiding this comment.
AccountCount is not static, it varies by config
| label, | ||
| type: OperatorType.BATCH, | ||
| ethereumHdIndex: index + 1, | ||
| ethereumHdIndex: Constants.ethereumHdIndex(label), |
| label, | ||
| type: OperatorType.UNDERWRITER, | ||
| ethereumHdIndex: config.batchOperatorCount + index + 1, | ||
| ethereumHdIndex: Constants.ethereumHdIndex(label), |
| /** HD index past every operator slot in the largest planned cluster. */ | ||
| export const DefaultEthereumHdIndex = 32 | ||
| /** HD index past every operator slot — the floor Constants reserves for non-operators. */ | ||
| export const DefaultEthereumHdIndex = Constants.FirstNonOperatorHdIndex |
There was a problem hiding this comment.
this is not a constant # - driven by config
Change-Id: I13e0a6fcdbad287a553ae7d7e702a8c60326a089
a1be2de to
f790f2e
Compare
fix(harness): WNE-41 — derive and supply the Ethereum outpost's genesis batch-operator roster
Repo:
wire-tools-ts· Branch:fix/wne-41_corrected2· Base:masterCompanion PR:
wire-ethereum—wne-41_corrected2(must land together)Why this is needed
wire-ethereum's WNE-41 fix makesOPPInbound.isActiveOperatorfail-closed and moves thebatch-operator roster into
initialize. On a WIRE cluster the addresses that actually sendepochInare the batch-operator daemons' own EOAs, not the deployer — so without this change the outpost
deploys with a roster that authorizes nobody who matters, the first envelope is refused, and the
epoch never advances.
What it does
EthereumOutpostSteps.resolveGenesisRosterderives the roster and threads it throughEthereumOutpostBootstrapperinto theoutpost.jsondeploy config thatdeployLocal.tsconsumes.The addresses are derived, not read off provisioned operators, because of phase ordering: the
outpost deploys in Cluster Prerequisites (
EthereumOutpost), while batch operators are provisionedlater in Cluster Post Contract Deployment. The
(mnemonic, HD index)pair is fully determined bythen, and both sides read it from the same two authorities —
KeySteps.ethereumMnemonic(ctx)andConstants.batchOperatorEthereumHdIndex(index)— so the rostercannot disagree with the keys the daemons later sign with.
Grouping is sized by the depot's own
operatorsPerEpochfromBatchOperatorSchedule.resolve,because
batchOpGroups[0].lengthis the outpost's consensus threshold. Membership in latergroups grants delivery rights without raising it. The depot replaces the whole roster on its first
BATCH_OPERATOR_GROUPSattestation.Changes
Constants.tsbatchOperatorEthereumHdIndex/underwriterEthereumHdIndex— the HD-index rule now has one home instead of being spelled inlineClusterBuildDefaults.tsindex + 1/batchOperatorCount + index + 1EthereumOutpostSteps.tsresolveGenesisRoster+ wiring into the deploy stepEthereumOutpostBootstrapper.tsgenesisRosteroption, fail-fast asserts, writesgenesisOperatorGroups+epochDurationSecinto the deploy config, logs the rosterThe HD-index extraction is the load-bearing detail: the roster is derived at a point in the
bootstrap where operator accounts do not yet exist, so a second spelling of that rule would silently
authorize the wrong addresses — and fail closed, minutes later, as a stalled epoch.
Fail-fast
EthereumOutpostBootstrapper's constructor rejects a roster with no operators, or a non-positiveepochDurationSec, before anvil is touched.OPPInbound.initializeis one-shot with no rostersetter, so a bad roster produces an outpost recoverable only by a UUPS implementation upgrade —
failing at construction is strictly cheaper.
Duplicate and empty-group rejection lives in the contract and the wire-ethereum TS builder (see
the companion PR). The harness derivation cannot produce either:
chunk()never emits an empty groupfor non-empty input, and distinct HD indices give distinct addresses. No redundant check added here.
Scope
MaterializeExternalOutposts, soplanDeploynever runs and no roster is derived.wire-solana.Testing
pnpm --filter @wireio/cluster-tool test: 1410 passing, 0 failing.pnpm buildandpnpm run lintclean.EthereumOutpostSteps.test.tspins the derivation: every batch operator seated at the address itsown EM key derives to (verified against an independent
ethers.HDNodeWalletderivation, not byre-running the code under test), group 0 sized to
operatorsPerEpoch, later-group members stilladmitted, and the deploy owner excluded — deployment privilege is not delivery privilege.
EthereumOutpostBootstrapper.test.tscovers the new required option and both fail-fast rejections.flow-operator-collateral-depositSUCCEEDED — 140/140 steps, epochs 1→6, all fourOPP directions circulating, 0
OPP_NotActiveOperatorin the cluster log while 11 realepochIndeliveries from batch-operator EOAs landed. The deploy config written by this branch carried
[[0x7099…], [0x3C44…], [0x90F7…]]withepochDurationSec: 60— three distinct addresses, noempty group, deployer absent.
Zero
OPP_NotActiveOperatoris the decisive number: it is the exact revert a wrong derivationproduces, and the fail-closed gate means a mismatch could not pass silently.
🤖 Generated with Claude Code