Repository navigation
refactor(eth,ethstats,cmd): split the eth package into eth/protocols/eth #21482, close #2856 - #2852
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
4b2bd8c to
9712af6
Compare
111c6a5 to
895128a
Compare
8653a43 to
44ca140
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Test peer cleanup remains incomplete, leaving inbound handlers blocked on open message pipes and broadcaster goroutines running after handshake rejection.
2 open issues
What changed in this PR
This PR separates the XDC wire protocol into eth/protocols/eth, following upstream geth’s package boundary while keeping synchronization, peer management, and BFT reactions in eth.
Changes:
- Extracts wire packets, handshake, broadcasts, discovery, and dispatch behind a backend interface; rewires callers without compatibility aliases.
- Preserves wire encoding and carries forward bounded header serving, while adding lookup budgets for batch replies.
- Expands protocol and lifecycle tests, standardizes downloader peer logging, and documents the split and deliberate upstream differences.
| File | Description |
|---|---|
GLOSSARY.md |
Adds block-schedule and wire-protocol terminology. |
ethstats/ethstats.go |
Reads versions from the wire package. |
eth/sync.go |
Adapts synchronization to the handler and wire peer APIs. |
eth/sync_test.go |
Updates synchronization fixtures and assertions. |
eth/protocols/eth/protocol.go |
Defines exported wire packets, codes, and validation. |
eth/protocols/eth/protocol_test.go |
Tests registration, packets, codes, and sanity checks. |
eth/protocols/eth/protocol_encoding_test.go |
Pins packet bytes against pre-split encodings. |
eth/protocols/eth/metrics.go |
Moves message metering into the wire package. |
eth/protocols/eth/handshake.go |
Extracts version-specific status handshakes. |
eth/protocols/eth/handshake_test.go |
Tests handshake success and rejection paths. |
eth/protocols/eth/discovery.go |
Extracts ENR construction and updates. |
eth/protocols/eth/discovery_test.go |
Tests discovery records and head-triggered updates. |
eth/protocols/eth/broadcast.go |
Extracts asynchronous broadcast loops. |
eth/protocols/eth/backend_test.go |
Adds standalone wire-layer test fixtures. |
eth/protocol.go |
Removes the former node-layer wire definitions. |
eth/peerset.go |
Extracts node-layer peer bookkeeping. |
eth/peerset_test.go |
Tests registration and removal cleanup. |
eth/peer_test.go |
Retains node-specific removal tests. |
eth/helper_test.go |
Adapts fixtures to wrapped wire peers. |
eth/handler_eth.go |
Implements backend callbacks and node-level reactions. |
eth/downloader/statesync.go |
Uses consistent short peer IDs in logs. |
eth/downloader/queue.go |
Uses the shared peer-ID log helper. |
eth/downloader/peer.go |
Uses unsigned versions and centralizes log-ID formatting. |
eth/downloader/peer_test.go |
Updates version types and tests ID formatting. |
eth/downloader/downloader.go |
Updates registration types and peer logging. |
eth/downloader/downloader_test.go |
Adapts downloader tests to unsigned versions. |
eth/bft_queue_test.go |
Updates peer fixtures and exercises inbound dispatch. |
eth/backend.go |
Constructs the handler and registers extracted protocols. |
eth/api_debug_test.go |
Runs isolated debug tests in parallel. |
eth/api_backend.go |
Updates internal handler references. |
docs/adr/0004-split-eth-wire-layer-into-protocols-eth.md |
Records boundaries, compatibility, and upstream deviations. |
cmd/XDC/misccmd.go |
Reports versions from the wire package. |
cmd/devp2p/internal/ethtest/protocol.go |
Documents existing protocol-length divergence; no behavior change. |
cmd/devp2p/internal/ethtest/conn.go |
Imports moved message codes directly. |
cmd/devp2p/internal/ethtest/conn_test.go |
Updates status-code references. |
cmd/devp2p/internal/ethtest/conn_decode_test.go |
Updates decoder tests to moved message codes. |
Review effort: Balanced
b6bd0de to
7a14a95
Compare
The wire layer cannot import eth without a cycle, so everything it needs from the node now arrives through the Backend interface: Chain, TxPool, AcceptTxs and PeerInfo for the data it serves, RunPeer for the peer life cycle, Handle for the packets the wire layer only decodes, and NodeInfo for protocol registration. MakeProtocols returns one p2p.Protocol per supported version, sets the ENR attributes from inside that construction and hands inbound traffic to Handle, which dispatches through Backend instead of ProtocolManager methods. The size of every handled packet goes along with it, since that is what the node layer meters. The ENR updater drops its *Ethereum receiver and becomes StartENRUpdater(chain, ln), and the negotiated protocol version becomes a uint because that is the type MakeProtocols iterates over. Wire behaviour is unchanged: the capability stays "eth", the xdc versions stay 165, 164, 100 in that order, the message-length bounds stay 230/227/227, and every message code keeps its value. The package becomes usable here, so its tests arrive with it: a fake Backend drives the header, body, node data and receipt serving paths once per supported version, the ENR entry and updater are covered, and the handshake had no tests at all before this change - both payload forms, every rejection path and the extra-status case are now driven through Peer.Handshake. The copied serving code carries the ancestor-based hash walk of the node layer: the forward hash traversal resolves the expected hash through GetAncestor rather than the removed GetBlockHashesFromHash helper, and the header test states its descending expectation by walking block numbers.
The peer now takes care of the two steps the node layer used to have to remember: NewPeer wraps the message pipe in the metered reader/writer and starts the broadcast loops, and the returned peer must be closed to wind them down. That gives the wire layer one constructor that leaves a fully working peer behind, and it keeps the metering and the broadcasters in the package that owns the traffic they account for. The accessors the node layer needs move out with the sets: ID, Version, KnownBlock, KnownTransaction and the per-message Known... queries are exported, because the known sets are private to this package from now on. What the node layer owns comes back out: the removal bookkeeping (markRemoved) and the fork-validation drop timer leave the wire peer, which no longer decides when a peer is removed or dropped. The broadcast loops had no tests while their start-up sat in the node layer; now that NewPeer starts them the tests can drive them directly: a peer keeps draining its queues after a write failure, Close terminates the broadcasters and is idempotent, and marking through every entry point is visible through the exported accessors.
The capability name, the version table and the message codes now live in eth/protocols/eth. Move the external readers over first, while eth/ still states the same values, so this step compiles on its own and the node-layer switch that follows can drop its copies in one go. - ethstats and cmd/XDC take ProtocolVersions from the new package. - cmd/devp2p/internal/ethtest takes the seventeen message codes from it. The wire facts the client keeps locally are now commented as what they are: a second copy, kept on purpose, because ethProtoLen counts messages while the node's ProtocolLengths bounds the code space.
The wire layer now lives in eth/protocols/eth, so eth keeps only what the node does with a message: handler_eth.go implements the wire layer's Backend (Chain, TxPool, AcceptTxs, PeerInfo, Handle) and carries the reactions - BFT queue charging, order and lending landing, the NewBlock syncer hand-off and the DAO fork-drop timer - while handler.go keeps the protocol manager, the sync loops and the broadcast loops. The inbound loop inverts hands: MakeProtocols owns the message loop and calls RunPeer, which wraps the wire peer in eth.peer, runs the maxPeers check, the handshake, the registration and the downloader hand-off, and then hands the peer back to the wire layer. eth.peer is a thin handle over *ethproto.Peer: it keeps the fork-drop timer and the removal flag, and reads the negotiated version and the peer id through the wire layer's accessors. The wire facts leave eth entirely: protocol.go, metrics.go and enr_entry.go are deleted, protocol registration goes through MakeProtocols (which sets the ENR attribute itself), and StartENRUpdater replaces the method on Ethereum. The wire-level tests already moved with the code; the node-level ones stay, adapted to the new peer type. The port also seeds the peer's total difficulty with zero in NewPeer, which upstream 017831d leaves nil while Head and SetHead both dereference it: a peer asked for its head before the status exchange would panic (upstream fix 84c8e32).
The glossary's intro has promised "the block schedule its validators run to" since it landed, but the section itself was missing; this adds it (epoch, epoch switch block, gap block, round, masternode), each entry pinned to the consensus code that defines it. Kept as its own commit: this is consensus vocabulary, independent of the eth wire-layer split.
Move the peer set out of peer.go into peerset.go: registration, removal, lookup, best-peer and best-peer-excluding selection, the per-peer "peers without ..." selections, the highest announced tip and Close now live in one unit instead of being spread over the peer object and the message handler. The unit also owns the count the API surfaces read: GetPeer on the Ethereum object called len(peers.peers), reading the guarded map without its lock, and now goes through Len like the API backend already did. No behaviour changes: the wire peer keeps everything it had, and every existing eth test passes with its assertions untouched. Closes XinFinOrg#2849
XinFinOrg#2859 Bring the deployed-branch fix for the GetBlockHeaders reply loop across to the wire package, which is where the loop lives now. The origin is resolved once, both hash traversal directions go through GetAncestor, and a walk off the canonical chain is paid for out of one 100-header non-canonical budget per message. Forward hash traversal advances the origin hash and number together, so the following iterations find the header by the canonical pair instead of re-resolving it by hash. The serve cap and the lookup budget become wire-package local constants: maxHeadersServe keeps this tree's 192 and the loop stops after twice as many lookups, so a request that misses everything cannot walk further than a request that hits. The package keeps its downloader import for the other four serve loops. The header tests gained the deployed branch's cases: a request made entirely of unknown hashes, a skip on the order of the chain height, an allocation bound for that skip, and a reverse walk from a side chain deeper than the non-canonical budget. The last one replied with the origin header and the canonical header the unbudgeted walk drifted to before the fix; it now replies with the origin alone. Two more cases from the same branch ride along: a reverse skip that overflows past genesis, and a forward walk from a side chain whose step lands on the canonical chain. Both now stop at the origin instead of repeating it or serving headers the peer did not ask for.
…ks into the wire loop, close XinFinOrg#2861 The two propagated-block payload self-checks, the NewBlockPacket sanity check, the ReceivedFrom stamp and the peer block mark need no node state, so they move into the wire package's NewBlockMsg case, next to the ReceivedAt stamp that was already there. The same goes for BlockBodiesPacket's sanity check, which the node layer ran for BlockBodiesMsg. The node layer keeps the reactions that do need node state: the fetcher enqueue, the tip tracking with its plausibility window, the total difficulty comparison and the sync request. A payload that fails a check is still dropped without disconnecting the peer, and still leaves no mark, while a later valid block marks the peer as before. Both validation helpers lose their only callers outside the package, so they become unexported.
The peer's own Version accessor returns uint, so the downloader's int parameter only forced the caller to cast it down. Take the version as uint in RegisterPeer, RegisterLightPeer, newPeerConnection and the idle-peer selection that compares against it, then drop the int() casts at the call sites in the eth package. No behaviour change: the accepted version range and every comparison stay as they were.
Every log line in the downloader that names a peer now takes its field from a single shortPeerID helper, which trims a node id to 16 characters and passes a short id through untouched. That covers the logger the register and unregister paths attach to the peer, the header delivery logger, and the inline "peer" fields of the sync and state-sync logs, so a full node id no longer reaches any of them and the truncation lives in one place. The helper carries a table test over a short id, the 15- and 16-character boundary and a node-id-length id.
TestEmptyAccountRange and TestStorageRangeAt build their own in-memory state and share no fixture, so they can run alongside TestAccountRange and the other parallel tests in the package.
…onfig struct, close XinFinOrg#2863 The type, the constructor and the fields it exposes now carry upstream's names: handler, newHandler and handlerConfig. The ten positional parameters collapse into named fields, and handlerConfig also carries the four dependencies only this tree has (chain config, order pool, lending pool and consensus engine). The two constructors and the two pool setters they existed for are gone; newHandler is the single entry point. No behaviour changes: the fast-sync downgrade still happens inside the constructor, the pool setters only ever assigned their argument, and the two nil subscriptions keep their zero values.
7a14a95 to
83c6805
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The wire-layer split changes peer lifecycle and dispatch across supported versions, requiring maintainer validation of mixed-version live-node interoperability and shutdown behavior beyond the supplied in-process tests.
No open issues 🎉
2 resolved since last review
Review effort: Balanced

Summary
Extract the
ethwire layer into a neweth/protocols/ethpackage (upstream go-ethereum ethereum#21482) and rewire every caller. The node layer keeps the protocol manager, the peer set and the sync loops; the new package never importseth, so the dependency points one way.The seam is
eth/protocols/eth.Backend(plusHandlerandMakeProtocols): the wire layer calls back into the node layer through it, which is what keeps the import graph acyclic.eth.peernow embeds the wire peer and keeps only node-layer state (fork-drop timer, removal flag).Wire encoding is frozen; reply contents are not.
ProtocolName = "eth",ProtocolVersions = {165, 164, 100},ProtocolLengths = {230, 227, 227}and every message code are unchanged — including the BFT message codes0xe0–0xe2and the pooled transaction codes0xe3–0xe5.TestProtocolRegistration,TestMessageCodesandTestPacketRoundTripspin those invariants. Reply contents are not covered by that promise; the alignment pass narrows two of them (see "Stacked work").Three fixes fall out of the move:
NewPeerseedstd, closing a nil-pointer panic inHead()/Info()when the handshake has not filled the peer's total difficulty yet.Version().maxKnownBlocksis unexported — the node layer never read it.No compatibility alias is left behind in
eth; callers (ethstats,cmd/XDC,cmd/devp2p/internal/ethtest) import the moved symbols from the new package directly. Design notes, the API drift table and the disposition of every upstream deviation live indocs/adr/0004-split-eth-wire-layer-into-protocols-eth.md(renumbered from 0003 after #2822 landed its own ADR-0003).Stacked work
Base #2866 (
add-core-getancestor, ticket #2857): merged, and this branch is rebased onto the resultingdev-upgrade, so its commits are no longer part of the diff. The split needsGetAncestor— the header walk and the forward-hash check both call it.#2867 is merged. It rewrote the node-layer
GetBlockHeadersloop (eth/handler.go) that this branch moves intoeth/protocols/eth/handler.go, and this branch is rebased onto that merge commit (99bca809c2); the wire package carries the same bound through #2859.The alignment pass ADR-0004 promised (#2856) is the tail of this branch, one commit per ticket, each rewriting the same dispatch loop and the same serve limits, so they are only reviewable in sequence:
first,maxNonCanonical,lookups < 2*maxHeadersServe.2x caplookup budget; the wire package stops importing the downloader.NewBlockMsgblock mark, the payload checks and both stamps move into the wire loop; the announce mark and the transaction marks stay in the node layer next to the known-set dedup, as upstream'shandleTransactionsdoes.handlerand built from ahandlerConfigstruct; mechanical.That is every ticket in #2856 except #2858: the pre-2019
GetBlockHashesFromHashdeletion landed with #2867, and the wire package here resolves the forward walk throughGetAncestorinstead of that helper.Two wire-visible changes fall out of the alignment pass: a header reply whose origin is not canonical now stops after the non-canonical budget, and the four batch replies (bodies, node data, receipts, pooled transactions) stop after twice their serve cap in lookups even when most requested hashes are unknown. Everything else — encoding, capability, versions, message codes — is unchanged.
Glossary ratification (#2872). The two
GLOSSARY.mdsections this branch adds — Wire protocol (with the ADR-0004 commit, which cross-references it) and Block schedule (standalone, fulfilling the intro's standing promise) — are ratified by #2872, which supersedes the "the project glossary is untouched" clauses of #2856/#2865 for exactly those two sections. Two comment-level cleanups from review were amended into their introducing commits: thetransactionsvariable spelling ineth/handler_eth.go(into the commit that introduces the file) and two wrong doc comments ineth/peerset.go(Len,OrderPeersWithoutTx, into the commit that extracts the peer set). No behaviour change.Post-split cleanups (#2879). Three review leftovers that must not ride this branch — the
BestPeerduplication, theknown*/know*naming mix, the dead tail ofsynchronise— are deferred to #2879; every other finding from the re-review keeps as-is (upstream-aligned shapes).Post-review test additions (third pass). The third review pass found the #2860 acceptance not fully pinned: the node-data and pooled-transaction loops had no lookup-budget test, and no data-serving loop had an all-unknown-hash case (#2856 seam 1 asks for the reply to "requests made entirely of unknown hashes").
TestGetNodeDataLookupBudget,TestGetPooledTransactionsLookupBudgetandTestAllUnknownHashRequestsGetEmptyRepliesclose that, and the threeGet*Packettypes the alignment pass defined joinTestPacketRoundTrips. ADR-0004 now uses the glossary's "xdc version" term where it said "protocol version", and its serve-cap wording records the pooled-transaction backstop (4096) and that upstream's 4096 on that loop is a lookup cap with no reply cap, which the pre-split loop did not have at all instead of implying every cap unchanged. All amended into their introducing commits via fixups; no new commits, no behaviour change. A fourth pass then put a known entry on each side of the2*capboundary in each of the four fixtures, so a halved lookup budget fails them too — it did not before.Evidence
Before — the wire layer lived inside the node package, and the node peer kept its own copy of the negotiated version and a nil
tduntil the handshake:After — the wire layer owns the peer,
tdis seeded, and the node peer reads the negotiated version from it:The frozen wire surface is pinned by tests on this branch, and the data-serving
loops by lookup-budget and all-unknown-hash tests:
The whole branch, re-verified 2026-10-28 after the #2867 rebase and the post-review test additions:
gofmt -s -l/goimports -lare clean on all 43 changed*.gofiles (42 stillpresent;
eth/protocol.gois the one deletion), and offlinegolangci-lint 2.15.0 --new-from-rev=$(git merge-base xdc/dev-upgrade HEAD)over the touchedpackages reports 0 issues. Every commit is compiled in sequence by
git rebase --exec 'go build ./eth/... ./ethstats/... ./cmd/XDC/... ./cmd/devp2p/...' $(git merge-base xdc/dev-upgrade HEAD), so the branch is bisectable.Merge Danger
Door: two-way
Nothing on the wire encoding, in storage or in the API changes: this is a package move plus the interface seam it needs. Reply contents may narrow (the header reply to a non-canonical origin and the four batch-reply lookup budgets). Reverting restores the three deleted files; there is no migration.
Blast Radius: eth protocol wiring
eth,ethstats,cmd/XDCand thedevp2ptest client. Consensus, state and RPC surfaces are untouched. The one latent behaviour change is a crash that no longer happens (Head()/Info()before the handshake filledtd); it cannot change any wire byte or block outcome.Types of changes
Put an
✅in the boxes that applyImpacted Components
Which parts of the codebase does this PR touch?
Put an
✅in the boxes that applyChecklist
Put an
✅in the boxes once you have confirmed below actions (or provide reasons on not doing so) thatgo test ./cmd/devp2p/....