feat(clients): executionGroups on endpoints + buildoor testing-build playbook - #233
feat(clients): executionGroups on endpoints + buildoor testing-build playbook#233qu0b wants to merge 11 commits into
Conversation
…playbook Endpoints gain `executionGroups`, mapped to spamoor's `group(a,b)` rpchost prefix, so a scenario with `client_group` sends only to the endpoints in that group (e.g. buildoor's tx intake at /rpc) instead of spreading across every configured execution endpoint. Existing configs keep spamoor's default group. Adds playbooks/dev/buildoor-testing-build.yaml: switch a buildoor to its testing build source, run eoatx into the intake through the `builder` group, require buildoor blocks with N+ transactions, zero tx plan mismatches, no forks, then switch back. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
staticcheck v0.8.x requires Go 1.26; the check workflow runs Go 1.25.x with GOTOOLCHAIN=local, so `@latest` now fails at install time on every PR. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
Review findings on the executionGroups field and the playbook: - The claim that an endpoint in a named group "only receives transactions from scenarios that ask for it" was false. spamoor seeds every client with the "default" group and group() only appends, and group-less selections (wallet funding, refills, deployments, scenarios without client_group) all resolve to "default" — so a named endpoint still receives that traffic. The field docs, the global-config example and the playbook now say so, and show "-default" as the way to actually reserve an endpoint. - verifyClientGroups fails at startup when the linked spamoor ignores a configured group entry, so a removal that is silently dropped can never degrade isolation unnoticed. Group removal needs spamoor > v1.2.2 (ethpandaops/spamoor#283). - The playbook's tx-queue check asserted the match count and the must-be-zero counters in one task with failOnCheckMiss, where an unsatisfied assertion is a failure rather than a wait — so it could fail on the first poll while buildoor was still tallying the last block. Split into a polled wait for the count and a fail-fast guard on the zero counters. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
While buildoor runs the testing source its blocks are the only inclusion path, so spamoor's own wallet funding — which uses the group-less default selection, not the scenario's client_group — has to reach the intake too. Running the playbook with a second execution endpoint hung the scenario with one funding transaction pending in geth forever. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
The --refill-amount / --refill-balance CLI flags take ETH and convert, the scenarioYaml fields are uint256 wei. Passing 50 funded every wallet with 50 wei: above the refill threshold, so spamoor reported 'no funding needed' and then every transaction was skipped for insufficient funds and the blocks came out empty. Now passed as explicit wei config values with a comment. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
configVars are evaluated as jq, whose numbers are float64, so an unquoted 50000000000000000000 reaches spamoor as "5e+19" and fails its uint256 parse. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
assertoor prepends "." to an expression that lacks one, so a bare [..] list became an index and the assertion set unmarshalled as a map. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
There was a problem hiding this comment.
Summary
Adds spamoor client groups (endpoints[].executionGroups, additive, with -name removal) plus a startup-time verifyClientGroups check that fails if the linked spamoor did not apply a configured entry, and a buildoor testing-build playbook. The core feature is correct against the pinned spamoor v1.2.2 (I traced the group() parser, HasGroup, and the default-group selection in the spamoor checkout), but the playbook's recommended 'simple' endpoint setup cannot feed the shipped scenario, and the new group check inherits a fragile client/endpoint index pairing.
Issues
- 🟡
playbooks/dev/buildoor-testing-build.yaml:48— documented 'simple form' can't work with default clientGroup: "builder" — see the thread on that line - 🟡
pkg/txmgr/spamoor.go:49— verifyClientGroups pairs endpoints[i] with allClients[i], which misaligns when a client fails to init — see the thread on that line - 🟢
playbooks/dev/buildoor-testing-build.yaml:136— zero-counter check is a single snapshot, not whole-run coverage — With failOnCheckMiss the check succeeds on its first evaluation and is never re-polled, so the comment "covers the whole run" overstates: blocks proposed after it (e.g. during the following check_consensus_forks epoch, while the background scenario keeps feeding the intake) are never tallied. A mismatch/unknown block in that final ~1 epoch would be missed.
Reviewed @ aae4eef6
"Firefox" is another name for the red panda — yes, that Firefox.
- InitClients appends only the options it could build, so pairing endpoints with GetAllClients() by index misaligns after a failure: every later endpoint gets another endpoint's client, wiring transactions to the wrong node and checking the wrong endpoint's groups. The count is now verified first; a client only fails to build on a malformed rpchost, so stopping is the right answer for what is a config error. - The playbook's "simple form" showed the intake with no executionGroups while the test's clientGroup defaults to "builder". A scenario selecting a group no client is in gets no client, so only the group-less wallet funding would have reached the intake and the proposal check could never pass. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
There was a problem hiding this comment.
Summary
The PR threads a new endpoints[].executionGroups config into per-endpoint spamoor client groups (group(...) rpchost prefix), guarded at startup so a group entry the linked spamoor ignored is a hard error rather than silent under-isolation (needed because the pinned spamoor v1.2.2 is append-only), plus a buildoor testing-build playbook. I traced the group plumbing end-to-end (config -> execution.ClientConfig -> spamoor parser -> verifyClientGroups) and against the cross-referenced spamoor source: the trimming matches spamoor's parser, the -default removal branch correctly fails startup against the pinned append-only parser, and the new length-check on InitClients is sound since a client is only dropped on a malformed rpchost and the pool preserves option order. The Go change and tests are correct; the one issue I found is in the playbook's coverage guarantee.
Issues
- 🟡
playbooks/dev/buildoor-testing-build.yaml:149— The 'no mismatch' guard passes on its first poll and never checks blocks built afterwards — see the thread on that line
Reviewed @ 51518104
"Firefox" is another name for the red panda — yes, that Firefox.
spamoor runs TrimSpace on every group name; the check compared the raw config entry. A stray space therefore made the two disagree: an added group failed startup with a spurious "not in it", and — the case the guard exists for — a removal spamoor applied under a different name passed verification, leaving the endpoint in "default" while the config said otherwise. Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
Endpoint
executionGroups+ buildoor testing-build playbookWhy
assertoor feeds every configured execution endpoint into spamoor's client pool, and scenarios spread their transactions across all of them. That makes it impossible to point one scenario at a single endpoint such as buildoor's tx intake (
POST /rpc, from ethpandaops/buildoor's testing build source), where transactions must go to exactly one place so a block can be built from precisely that list.What changed
endpoints[].executionGroups: [..](global config). The groups become spamoor'sgroup(a,b)rpchost prefix, so a scenario withclient_group: buildercan address those endpoints. Endpoints without the field are unaffected.verifyClientGroupsfails at startup when the linked spamoor did not apply a configured group entry, so isolation can never be quietly weaker than the config says.playbooks/dev/buildoor-testing-build.yaml: switches a buildoor to the testing build source, runseoatxinto the intake through thebuildergroup, and requires that buildoor proposesminVerifiedBlocksblocks withminTxsPerBlock+ transactions, that its plan-check tally shows zero mismatches, zero missing blocks and zero missed payloads, and that the chain has no forks. Cleanup switches the builder back to the pool source.Review fixes (redpandabot)
defaultgroup andgroup()only appends, anddefaultis what every group-less selection resolves to — wallet funding and refills, contract deployments, and any scenario withoutclient_group. So an endpoint in a named group still received that traffic, and the claim that it "only receives transactions from scenarios that ask for it" was wrong in the field docs, the config example and this description. All three now describe the additive behaviour, and point at-defaultas the way to actually reserve an endpoint. Group removal needs feat(client): group(-name) removes a client from a group spamoor#283; until it ships,verifyClientGroupsturns a dropped removal into a startup error rather than silent under-isolation (verified locally: assertoor refuses to start with["builder", "-default"]against the pinned v1.2.2).failOnCheckMiss, where an unsatisfied assertion is a failure rather than a wait — so it could fail while buildoor was still tallying the block that had just been proposed. Now split: a polled wait for the count, then a fail-fast guard on the zero counters, which are cumulative and so still cover the whole run.Found by running it
Re-running the playbook against a kurtosis devnet surfaced three more problems, all fixed here:
defaultselection rather than the scenario'sclient_group, so with two execution endpoints it picked the plain EL and the scenario hung — verified as exactly one transaction pending in geth. The playbook now documents both correct shapes: the intake as the only execution endpoint, or the others removed fromdefault.scenarioYamlrefill amounts are wei. The--refill-*CLI flags take ETH and convert; the YAML fields areuint256wei. Passing50funded every wallet with 50 wei, which cleared the refill threshold, so spamoor reported "no funding needed" and every transaction was then skipped for insufficient funds with empty blocks as the result.50000000000000000000reached spamoor as"5e+19"and failed its uint256 parse, and aconfigVarsexpression starting with[gets a.prepended and becomes an index rather than a list.Second review round, both fixed:
InitClientsappends only the options it could build, so after a failure every later endpoint got another endpoint's client — wiring transactions to the wrong node and checking the wrong endpoint's groups. This was pre-existing in thes.clientsmapping and my check inherited it. The count is now verified before the pairing; a client only fails to build on a malformed rpchost, which is a config error worth stopping for.executionGroupswhile this test'sclientGroupdefaults tobuilder, and a scenario selecting a group no client is in gets no client at all, so only the group-less wallet funding would have reached the intake.TrimSpaceon every group name while the check compared the raw config entry, so a stray space made the two disagree: an added group failed startup with a spurious "not in it", and a removal spamoor applied under a different name passed verification, leaving the endpoint indefaultwhile the config said otherwise. Both sides now normalize, with a test that runs the config through spamoor's own parser.Final run on a minimal-preset devnet with gloas at epoch 1: all ten tasks pass, 256 tx plans checked, 256 matches, zero mismatches, missed payloads or unverifiable blocks. Re-run after the alignment fix: all ten pass again.
Endpoint example
Documented in
docs/02-global-config.md.CI
_shared-check.yamlinstalledstaticcheck@latest, and v0.8.x now requires Go 1.26 while the workflow runs Go 1.25.x withGOTOOLCHAIN=local, so every PR failed at the install step. This branch pinsstaticcheck@v0.7.0(the last release for Go 1.25) in a separate commit; drop it if you prefer to bump the Go version instead.https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo