Skip to content

Boole: discovery filter rejects Anchor ENRs post-fork (checkPeer not fork-aware) — cross-client edges only ever Anchor-initiated #2967

Description

@shane-moore

Summary

Post-Boole, go-ssv's discovery filter cannot match Anchor. Anchor advertises the active domain in its main domaintype key, while checkPeer compares that against the static ssvConfig.DomainType, so the comparison never succeeds. Anchor fixed the reverse direction in sigp/anchor#1187, so cross-client edges do form, but only Anchor can ever create one.

We would like to ask you to revisit #2952 item 4 against a condition it did not weigh. Background and the full audit are in sigp/anchor#1185.

Evidence from hoodi-stage

hi-h-sv-net2 (operator 417) restarted post-Boole on 2026-07-28 and stalled at 3 peers against target_peers=69 with zero go-ssv peers. On 2026-07-29 00:18:46 UTC we put the #1187 build on that node alone, keeping operators 415/416/418 on v1.3.0 as a control since they still held warm pre-fork connections.

before after
peers (in / out) 3 (1 / 2) 7 (1 / 6)
distinct go-ssv ENRs surfaced by discovery 0 7
connected go-ssv peers 0 4

Deriving peer IDs from each discovered ENR's secp256k1 entry and matching them against Handshake completed lines: of the 7 connections, 3 are Anchor and 4 are go-ssv, and Anchor dialed all four go-ssv peers. The single inbound connection is another Anchor node. We attempted 23 distinct dials in the window and no go-ssv node dialed us. No trusted or static peers are configured on these hosts, so all of it came through normal discv5.

Every go-ssv ENR carried domaintype=00003114 with next_domaintype=00003115, and no Anchor ENR carried a next domain. So the stage fleet is populating NextDomainType correctly and #1187 consumes it.

The ask

At network/discovery/dv5_service.go:279 (PR #2941 head 7de8ab0f), widen the comparison to accept either configured domain:

// Accept a candidate on either configured domain, so peers that advertise the
// post-fork domain in their main key remain discoverable across the transition.
domainMatches := nodeDomainType == dvs.ssvConfig.DomainType ||
    (dvs.ssvConfig.NextDomainType != (spectypes.DomainType{}) &&
        nodeDomainType == dvs.ssvConfig.NextDomainType)

if !domainMatches {
    recordPeerSkipped(ctx, skipReasonDomainTypeMismatch)
    return newPeerSkipError(skipReasonDomainTypeMismatch,
        fmt.Errorf("domain type %x matches neither %x nor %x",
            nodeDomainType, dvs.ssvConfig.DomainType, dvs.ssvConfig.NextDomainType))
}
  • No structural change. ssvConfig.NextDomainType is on the same struct (networkconfig/ssv.go:40) and already used at :498 and :571. One production call site, no Options change, no slot clock.
  • Strictly widens an accept-set, so it cannot partition a homogeneous fleet, and no existing discovery test result changes.
  • The zero-value guard is load-bearing. UnmarshalJSON defaults NextDomainType to DomainType only for parsed configs (ssv.go:125-127); the struct literals in the discovery tests leave it [4]byte{}, where a bare || would start accepting an all-zero domain.

It also makes records.KeyNextDomainType do something: today it is written at :498 and :571 and, as far as we can tell, never read anywhere in the non-test tree.

The fuller version is your own isDomainCompatible from boole-fork, which uses the runtime CurrentDomainType() and the transition window. That is the better end state, but it needs DiscV5Service to hold the whole *networkconfig.Network for the slot clock, so we are not asking for it here.

Why we think it is worth revisiting

#2952 item 4 closed deliberately, with a condition attached:

only port isDomainCompatible if full mixed-fleet interop during testnet migration is required

The case weighed there was interop with older boole-fork binaries during a migration. That is transient, and we agree it would not justify the change on its own. Anchor is a permanent second implementation that advertises the active domain rather than a static one, so this is the steady state rather than a window that closes.

Mainnet exposure

Mainnet is pre-Boole, so both clients are on Alan, discovery is symmetric, and that symmetry currently supplies most of our connectivity:

operator peers target in out inbound share
2042 56 69 43 13 77%
2043 70 72 56 14 80%
2044 40 69 27 13 68%
2045 60 69 45 15 75%
948 22 63 18 4 82%

Our nodes sustain only 13-15 outbound connections each, and 4 on operator 948. Anchor also stops running discovery once connected_peers >= target_peers, where the target is min(60 + 3*subnets, 150), and operator 2043 is already at 70 of 72. On a densely peered network that combination leaves a restarted go-ssv node with no route back: it cannot discover us, and we may have stopped looking.

If Boole activates on mainnet with the current filter, the inbound path stops being replenished. Warm connections survive the transition, as they did on hoodi-stage in July, so it would look clean at first and then degrade with restarts and ordinary churn.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions