Skip to content

refactor(eth,ethstats,cmd): split the eth package into eth/protocols/eth #21482, close #2856 - #2852

Merged
gzliudan merged 16 commits into
XinFinOrg:dev-upgradefrom
gzliudan:split-eth-pkg
Oct 28, 2026
Merged

gzliudan merged 16 commits into
XinFinOrg:dev-upgradefrom
gzliudan:split-eth-pkg

Conversation

@gzliudan

@gzliudan gzliudan commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Extract the eth wire layer into a new eth/protocols/eth package (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 imports eth, so the dependency points one way.

 eth/
   backend.go              # starts the protocols via eth/protocols/eth.MakeProtocols
   handler.go              # orchestration only
+  handler_eth.go          # Backend implementation + inbound dispatch
+  peerset.go              # peer-set bookkeeping
   peer.go                 # node-layer peer wrapper
-  protocol.go
-  enr_entry.go
-  metrics.go
+  protocols/eth/
+    protocol.go           # codes, packets, error codes
+    peer.go               # wire peer, Send*/Mark*, known sets
+    broadcast.go          # broadcast loops
+    handshake.go          # status handshake (xdc/165, 164, 100)
+    handler.go            # Backend/Handler/MakeProtocols seam + dispatch
+    discovery.go          # ENR entry
+    metrics.go            # metered message writer

The seam is eth/protocols/eth.Backend (plus Handler and MakeProtocols): the wire layer calls back into the node layer through it, which is what keeps the import graph acyclic. eth.peer now 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 codes 0xe0–0xe2 and the pooled transaction codes 0xe3–0xe5. TestProtocolRegistration, TestMessageCodes and TestPacketRoundTrips pin 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:

  • NewPeer seeds td, closing a nil-pointer panic in Head()/Info() when the handshake has not filled the peer's total difficulty yet.
  • the node-layer peer no longer duplicates the negotiated protocol version; it reads the wire peer's Version().
  • maxKnownBlocks is 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 in docs/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 resulting dev-upgrade, so its commits are no longer part of the diff. The split needs GetAncestor — the header walk and the forward-hash check both call it.

#2867 is merged. It rewrote the node-layer GetBlockHeaders loop (eth/handler.go) that this branch moves into eth/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:

That is every ticket in #2856 except #2858: the pre-2019 GetBlockHashesFromHash deletion landed with #2867, and the wire package here resolves the forward walk through GetAncestor instead 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.md sections 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: the transactions variable spelling in eth/handler_eth.go (into the commit that introduces the file) and two wrong doc comments in eth/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 BestPeer duplication, the known*/know* naming mix, the dead tail of synchronise — 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, TestGetPooledTransactionsLookupBudget and TestAllUnknownHashRequestsGetEmptyReplies close that, and the three Get*Packet types the alignment pass defined join TestPacketRoundTrips. 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 the 2*cap boundary 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 td until the handshake:

$ git show c9d32fe183:eth/peer.go
  125: func newPeer(version int, ...) *peer {
  129:         version: version,   // second copy of the negotiated version
                 // no td: -> Head() does new(big.Int).Set(p.td) on a nil *big.Int

After — the wire layer owns the peer, td is seeded, and the node peer reads the negotiated version from it:

$ git show HEAD:eth/protocols/eth/peer.go
  127: func NewPeer(version uint, ...) *Peer {
  136:         td: new(big.Int),
$ git show HEAD:eth/peer.go
   57:     Version:    int(p.Version()),

The frozen wire surface is pinned by tests on this branch, and the data-serving
loops by lookup-budget and all-unknown-hash tests:

$ go test -count=1 -v -run 'TestProtocolRegistration|TestMessageCodes|TestPacketRoundTrips|TestGetNodeDataLookupBudget|TestGetPooledTransactionsLookupBudget|TestAllUnknownHashRequestsGetEmptyReplies' ./eth/protocols/eth/
--- PASS: TestGetNodeDataLookupBudget (0.20s)
--- PASS: TestGetPooledTransactionsLookupBudget (0.00s)
--- PASS: TestAllUnknownHashRequestsGetEmptyReplies (0.00s)
--- PASS: TestProtocolRegistration (0.00s)
--- PASS: TestPacketRoundTrips (0.00s)
--- PASS: TestMessageCodes (0.00s)

The whole branch, re-verified 2026-10-28 after the #2867 rebase and the post-review test additions:

$ make all                              # XDC, evm, puppeth, rlpdump, p2psim, ethkey
$ go test -count=1 ./eth/... ./ethstats/... ./cmd/devp2p/... ./core/...
ok  github.com/XinFinOrg/XDPoSChain/eth                          15.713s
ok  github.com/XinFinOrg/XDPoSChain/eth/protocols/eth             1.983s
ok  github.com/XinFinOrg/XDPoSChain/eth/downloader               12.625s
ok  github.com/XinFinOrg/XDPoSChain/eth/bft                       0.712s
ok  github.com/XinFinOrg/XDPoSChain/ethstats                      0.014s
ok  github.com/XinFinOrg/XDPoSChain/cmd/devp2p/internal/ethtest   0.349s
ok  github.com/XinFinOrg/XDPoSChain/core                          27.699s

gofmt -s -l / goimports -l are clean on all 43 changed *.go files (42 still
present; eth/protocol.go is the one deletion), and offline golangci-lint 2.15.0 --new-from-rev=$(git merge-base xdc/dev-upgrade HEAD) over the touched
packages 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/XDC and the devp2p test 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 filled td); it cannot change any wire byte or block outcome.

Types of changes

Put an ✅ in the boxes that apply

  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Changes that don't change source code or tests
  • docs: Documentation only changes
  • feat: A new feature
  • fix: A bug fix
  • perf: A code change that improves performance
  • refactor: A code change that neither fixes a bug nor adds a feature
  • revert: Revert something
  • style: Changes that do not affect the meaning of the code
  • test: Adding missing tests or correcting existing tests

Impacted Components

Which parts of the codebase does this PR touch?
Put an ✅ in the boxes that apply

  • Consensus
  • Account
  • Network
  • Geth
  • Smart Contract
  • External components
  • Not sure (Please specify below)

Checklist

Put an ✅ in the boxes once you have confirmed below actions (or provide reasons on not doing so) that

  • This PR has sufficient test coverage (unit/integration test) OR I have provided reason in the PR description for not having test coverage
  • Tested on a private network from the genesis block and monitored the chain operating correctly for multiple epochs. — N/A: pure code move, no consensus or state-transition change.
  • Provide an end-to-end test plan in the PR description on how to manually test it on the devnet/testnet. — N/A: wire encoding is unchanged; reply contents may narrow (header reply to a non-canonical origin; four batch-reply lookup budgets — see the ADR-0004 footnote). The devp2p suite is covered by go test ./cmd/devp2p/....
  • Tested the backwards compatibility. — Wire bytes, capability name, version list and message codes are unchanged and pinned by tests. Reply contents may narrow (see the ADR-0004 footnote), but the framing and the meaning of each message do not change.
  • Tested with XDC nodes running this version co-exist with those running the previous version. — Follows from the unchanged capability and message codes above; recommend one devnet soak before a release tag.
  • Relevant documentation has been updated as part of this PR
  • N/A

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c74e24d6-9646-4067-aa5b-d0192eda187e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread eth/protocol_test.go
Comment thread eth/protocol_test.go Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

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.

5 participants