Skip to content

feat(client): group(-name) removes a client from a group - #283

Open
qu0b wants to merge 2 commits into
masterfrom
qu0b/exclusive-client-groups
Open

feat(client): group(-name) removes a client from a group#283
qu0b wants to merge 2 commits into
masterfrom
qu0b/exclusive-client-groups

Conversation

@qu0b

@qu0b qu0b commented Sep 3, 2026

Copy link
Copy Markdown
Member

group(-name) removes a client from a group

The problem

Every client starts in the implicit default group and group(name) only appends, so a client configured with group(builder) is in both builder and default.

default is not just one group among many. It is the group every selection that names none resolves to: wallet funding and refills, contract deployment, root-wallet operations, and any scenario run without --client-group. So putting a client in a named group does not keep group-less traffic away from it, and there is currently no way to reserve an endpoint for the scenarios that ask for it.

That matters whenever the endpoint is not an ordinary node:

  • a private transaction intake, where a stray funding transaction changes what a block contains
  • a builder submission endpoint
  • a rate-limited or paid provider you want one scenario to use and nothing else

The change

A -name token inside group(...) removes a group instead of adding one:

# reachable only by scenarios with --client-group=private
--rpchost "group(private,-default)name(Private Intake)http://localhost:8080/rpc"

Removal is order independent and works across repeated prefixes, so group(-default,builder) and group(builder)group(-default) both yield [builder].

Additive behaviour is unchanged: group(builder) still gives [default, builder], so nothing existing shifts.

Removing every group is refused at construction. Such a client is unselectable by every code path (HasGroup walks the actual list) while GetClientGroups still reports ["default"], so it would look configured and silently never receive a transaction.

Tests

Table test over the parsing (additive, comma-separated, duplicates, removal in either order and across prefixes, removing an absent group, the two refusal cases), plus a selection-level test asserting the property that actually matters: a reserved client is never returned by group-less GetClient() and only by GetClient(WithClientGroup("private")), while an additive client still serves both.

Context

This came out of wiring assertoor at a buildoor private transaction intake (ethpandaops/assertoor#233, ethpandaops/buildoor#186), where the built block is verified to hold exactly the transactions the operator planned. Without a way to leave default, wallet-funding transactions land in that intake and end up in the measured blocks. assertoor fails at startup when the linked spamoor ignores a removal entry, rather than running with weaker isolation than configured, so it can adopt this the moment it ships.

https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo

qu0b added 2 commits September 3, 2026 10:46
Every client starts in the implicit "default" group and group(name) only
appends, so a client with group(builder) is in both "builder" and "default".
Group-less selections — wallet funding and refills, contract deployments, and
any scenario without --client-group — all resolve to "default", so a client
put in a named group still receives that traffic and cannot be reserved for
the scenarios that ask for it.

A "-name" token inside group(...) removes a group, making
group(private,-default) the way to reserve an endpoint. This matters when the
endpoint is not an ordinary node: a private transaction intake, a builder
submission endpoint, or a rate-limited provider where stray funding or deploy
transactions must not land.

Additive behaviour is unchanged. Removing every group is refused: such a
client is unselectable by every path while GetClientGroups still reports
"default".

Claude-Session: https://claude.ai/code/session_01P3LSEorv3qsJrWb12ZKHNo
qu0b added a commit to ethpandaops/assertoor that referenced this pull request Sep 3, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants