Skip to content

chore(p2p): internalize libp2p v2.2.9 as a local p2p module - #15

Open
barbatos2011 wants to merge 12 commits into
release_v4.8.3from
chore/internalize-libp2p-v229-483
Open

barbatos2011 wants to merge 12 commits into
release_v4.8.3from
chore/internalize-libp2p-v229-483

Conversation

@barbatos2011

@barbatos2011 barbatos2011 commented Sep 21, 2026

Copy link
Copy Markdown
Owner

What

Internalize io.github.tronprotocol:libp2p:2.2.9 source as a new p2p/ Gradle submodule, replacing the external Maven dependency. No functional change to P2P behaviour, protocol, or consensus.

Base tronprotocol/java-tron release_v4.8.3 @ 34d00c5b0
Vendored tronprotocol/libp2p tag v2.2.9 @ c564f263d310d7a964035d3b597634aba6bda86d

Commit 1 is byte-comparable to the upstream tag. It contains upstream's src/main verbatim — all 94 Java files, 90 under src/main/java and the 4 reference classes split into a separate src/example sourceSet — plus the new build files. A reviewer can diff it directly against c564f263 and see that no vendored line was altered. Everything this PR changes about that source lands in commit 2, separately, for exactly this reason.

Why

Upstream tronprotocol/libp2p is maintained in bursts. Over the last 25 months (2024-09 through 2026-09) 11 months had no commits at all, including four consecutive months (2025-12 through 2026-03), and there has been no commit since 2026-07:

Release Date
v2.2.9 2026-07-30
v2.2.8 2026-06-29
v2.2.7 2025-11-11
release_v2.2.6 2025-03-14

Cross-repo maintenance creates friction that shows up on every P2P change:

  • Version coordination overhead — modifying P2P behaviour means publishing libp2p first, then bumping java-tron's dependency
  • Debugging difficulty — the IDE cannot step into an external artifact without downloading source jars
  • CI dependency — the build depends on external Maven/JitPack artifact availability
  • Industry alignment — go-ethereum keeps its p2p/ package in the monorepo; this follows the same pattern

Internalizing makes a P2P change a single PR instead of a cross-repo dance, and does not preclude back-porting individual commits to upstream libp2p when appropriate.

Changes

1. chore(p2p): add libp2p v2.2.9 source as p2p module

Upstream src/main verbatim: 90 files under p2p/src/main/java (including the embedded org.web3j crypto utilities), 2 .proto files, the 4 reference classes under p2p/src/example, plus p2p/build.gradle, p2p/.gitignore and the settings.gradle registration. Protobuf-generated sources are gitignored and regenerated at build time, so they are not in the diff.

2. style(p2p): conform vendored source to project conventions

Three mechanical rewrites plus formatting, kept separate so commit 1 stays diffable against the tag:

  • log.logger. (178 call sites, 158 under src/main and 20 in the example sourceSet) — the root lombok.config sets lombok.log.fieldName=logger
  • Math.StrictMath. (6 sites) — CI's check-math rule rejects java.lang.Math tree-wide
  • toLowerCase() / toUpperCase()Locale.ROOT (4 sites) — compile-forced by errorprone, see Key design decisions

Formatting: google-java-format over the 9 vendored org/web3j/** files, which came in AOSP 4-space style, plus import reordering. :p2p:checkstyleMain passes with 0 violations.

BasicThreadFactory is deliberately left as upstream writes it — twelve builder() calls and the one new BasicThreadFactory.Builder() in DiscoverServer. The root provides commons-lang3 3.20.0, where builder() exists and the no-arg Builder() constructor is @Deprecated, so rewriting either way would add deviation from the tag and, in one direction, thirteen deprecation warnings.

3. build(common): switch from external libp2p to local p2p module

Replaces the Maven dependency with api project(":p2p"), collapsing 17 lines of dependency plus excludes into one.

The dom4j exclusion tail that used to sit on the libp2p dependency here (jaxen, stax-api, xsdlib, pull-parser, xpp3) does not disappear: it arrives via the Aliyun and Route53 SDKs, which are now :p2p's own dependencies, so a configurations.configureEach block in p2p/build.gradle (added with the module in commit 1) carries it. Dropping it would silently re-admit artifacts the project has excluded for years. That block also names msv and relaxngDatatype, which common/build.gradle did not — the same dom4j tail, excluded completely rather than partially.

Adding a project to the dependency graph also needs task-dependency edges that an external jar did not. :framework's buildFullNodeJar and :plugins' binaryRelease both zip up runtimeClasspath and maintain a hand-written dependsOn list of project jars; p2p-1.0.0.jar is now on both classpaths, and without the edge a parallel build could assemble the shipped fat jar before :p2p:jar exists.

gradle/verification-metadata.xml gains three components that resolve once p2p compiles in-tree: bcutil-jdk18on:1.84, gson:2.9.0 and gson-parent:2.9.0. Checksums taken from Maven Central and cross-checked against the published .sha1.

4. build: track netty-codec-protobuf via a root nettyVersion

Netty 4.2 split io.netty.handler.codec.protobuf out of netty-codec, so :framework and :p2p both declare it explicitly — each puts the varint32 framing codecs on its channel pipelines. Netty itself is declared nowhere; it arrives transitively through grpc-netty, which :p2p tracks as rootProject.grpcVersion. Extracting nettyVersion next to grpcVersion makes that coupling visible in one place instead of leaving two literals to drift.

5. build(framework): declare the direct :p2p dependency

:framework uses org.tron.p2p in 17 files under src/main/java but declared nothing, relying on a three-hop transitive through :common -> :crypto -> :chainbase. Declared as api, not implementation, because framework re-exports p2p types: P2pEventHandlerImpl extends org.tron.p2p.P2pEventHandler, HelloMessage.getFrom() returns org.tron.p2p.discover.Node, PeerManager.add/remove take org.tron.p2p.connection.Channel, and Args.loadDnsPublishConfig returns PublishConfig. No resolution change either way.

6. test(p2p): port and extend the module's test suite

All 23 of v2.2.9's own test files, plus new ones, in p2p/src/test/java next to the code they cover — 64 files in total.

Two upstream tests were unreliable by construction and are fixed rather than carried over as-is:

  • NetUtilTest.testGetIP called three public IP-echo services and asserted all three returned the same string: a network dependency, and a coin flip on any host with more than one egress address. It now runs against a loopback HttpServer, covering the same fetch/parse/validate path plus the rejection branches. (libp2p's own CI never ran its tests, so this had not surfaced.)
  • ConnPoolServiceTest.getNodes_orderByUpdateTimeDesc asserted getNodes() returns nodes ordered by updateTime descending. getNodes() sorts, truncates to max(limit * 10, 50) candidates, then calls Collections.shuffle() — so with two nodes that assertion passes about half the time. It now asserts membership, and a new test covers the descending sort above the candidate bound, where it is actually observable.

Two further defects made coverage depend on fork scheduling rather than on what the tests assert:

  • ConnPoolServiceTest and SocketTest bound fixed ports (10000 / 10001). PeerServer.start only logs on bind failure, so a collision let them pass while exercising nothing. Both now take a free port from TestPort.choose(). framework's own tests use org.tron.common.utils.PublicMethod.chooseRandomPort for this, but :p2p cannot depend on :framework without creating a cycle, so the same few lines live in the module.
  • NodeTableTest read Parameter.p2pConfig without ever setting it, so it depended on an earlier class in the same fork having done so. Running the class on its own failed all eleven methods. It now sets up and restores its own config.

7. test(p2p): replace the example sourceSet with a usage contract test

DnsExample1, DnsExample2 and ImportUsing documented how an embedder configures the module, but they only compiled — each ended in a while (true) loop, bound a fixed port and pointed at live seed nodes, so nothing they showed was checked. Two carried real defects (TestMessage is not serializable, so ByteArray.fromObject returns null and Channel.send closes the channel; DnsExample1 held a signing private key in copyable code). ExampleUsageTest pins the contract they advertised instead: those configuration shapes are still accepted and still mean what the comments said.

8. docs(p2p): add a README, drop the vendored logback sample

The README is promoted from src/example/resources/README.md, where no reader would find it, and rewritten — it still read as upstream's document, with four source links pointing at github.com/tronprotocol/libp2p and prose describing libp2p as a standalone project.

StartApp moves to src/main/java as the standalone entry point and gets the fixes that made it worth documenting, including a real bug: --trust-ips is declared ip[,ip[...]] but resolved the whole comma-separated value as a single hostname, so with more than one address none of the listed peers became trusted.

logback.xml.example came from libp2p as a standalone project; inside java-tron framework/src/main/resources/logback.xml is what applies, and the sample was being packaged into the jar for no reason.

9. test(p2p): cover StartApp's arg parsing, correct the coverage surface

Moving StartApp out of the checkstyle- and coverage-exempt example sourceSet puts 981 instructions at 0% into the denominator. It is argument parsing, option declarations and a main() that starts services and blocks — not module logic, and not code the node runs — so org/tron/p2p/example/** is excluded from this module's jacoco report alongside **/protos/**. The two parsing helpers it does own are package-private now and covered by StartAppArgsTest, so the exclusion does not take regression protection with it.

:p2p:jacocoTestReport also reads :framework's exec data, because framework's own tests execute p2p code and that coverage is real.

10. build(p2p): ship a runnable standalone jar, stop repeating versions

The plain jar is thin, so declaring Main-Class on it would be worse than declaring nothing — the entry point resolves and then dies on NoClassDefFoundError: org/apache/commons/cli/ParseException. It declares none and fails honestly with "no main manifest attribute"; buildStandaloneJar produces p2p-standalone.jar with the runtime classpath bundled, wired exactly like :framework's FullNode.jar and :plugins' Toolkit.jar — same artifacts { archives(...) }, same -PbinaryRelease=false opt-out, same Bouncy Castle signature and dnsjava SPI exclusions. Verified by running both jars.

protobufVersion 3.25.8 was declared in both :protocol and :p2p; checkstyle 8.7 sat in a local versions map in :framework and :plugins and as a literal in :p2p. Both move to the root ext beside grpcVersion and nettyVersion, and all four modules reference them — removed rather than relocated.

11. build(p2p): put .proto files where the protobuf plugin looks for them

src/main/protos with an explicit srcDir override builds fine, but IDEA's protobuf plugin does not read that override — it resolves against the plugin's default path — so import "Discover.proto" and every type it brings in showed as unresolved in the editor. The override is gone; generated sources still land in src/main/java/org/tron/p2p/protos and are still gitignored.

Flagging for a follow-up: :protocol has the identical setup and presumably the identical symptom. Aligning it is the same one-directory rename, but it is core java-tron with a src/main/gen interplay and does not belong here.

Key design decisions

Decision Choice Rationale
Module name p2p Matches package org.tron.p2p, geth's p2p/ convention, and the project's short-name style (common, crypto)
Proto files Stay in :p2p Moving them to :protocol would make :p2p depend on :protocol and cost the module its leaf position — it currently has zero project dependencies, which is what keeps vendored code from reaching back into java-tron internals and keeps upstream re-contribution possible
Unit test location p2p/src/test Same as common and plugins. Keeping them in the module also avoids a :framework test-classpath SDK re-declaration plus dom4j mirror, and a jacoco additionalClassDirs bolt-on
Reference examples Replaced by ExampleUsageTest The originals only compiled; a test pins the same contract and actually runs
commons-lang3 No declaration in :p2p The root already hands every subproject implementation "org.apache.commons:commons-lang3:3.20.0", and an implementation dependency is on the declaring project's own compile classpath. Confirmed by compiling :p2p main and test sources with no commons-lang3 line
BasicThreadFactory Left exactly as upstream builder() exists at 3.20.0 and the no-arg Builder() constructor is @Deprecated; rewriting either way adds deviation from the tag
grpc-netty Tracks rootProject.grpcVersion libp2p pinned 1.81.0; tracking the root's 1.83.1 keeps it from drifting from the Netty the rest of the build resolves
netty-codec-protobuf Root nettyVersion Declared by both :framework and :p2p; a shared version makes the coupling to grpcVersion visible
:framework -> :p2p api framework re-exports p2p types in its own public signatures
MathStrictMath All 6 sites CI check-math rule; integer results identical
Locale.ROOT All 4 sites errorprone StringCaseLocaleUsage is ERROR on every subproject except protocol/errorprone. The one non-mechanical change in this PR — behaviour is identical for ASCII but differs under a Turkish locale. Matches the project's own idiom in Args.java:1273
Checkstyle/jacoco excludes **/protos/**, **/example/** Generated code and the CLI entry point

Scope

  • No protocol or message-format change
  • No consensus change
  • No proto change — the .proto files move verbatim
  • No hard fork
  • Package names preserved (org.tron.p2p.**), so imports in dependent code are untouched
  • Existing nodes upgrading need only git pull and rebuild
  • :p2p remains usable standalone — java -jar p2p-standalone.jar runs the module on its own; see p2p/README.md

Defects found in the vendored source (not fixed here)

Two instances of the same shape: a DNS parse helper throws an unchecked exception past a catch (DnsException) that was clearly written to tolerate unparseable input, so one malformed TXT record aborts the whole operation instead of being skipped.

  1. Short root entry. RootEntry.parseEntry (dns/tree/RootEntry.java:67) does e.substring(rootPrefix.length()) with no length guard, so any value shorter than the 13-character tree-root-v1: prefix throws StringIndexOutOfBoundsException. The caller at dns/update/AwsClient.java:334 catches only DnsException, so it escapes and aborts computeChanges, failing the entire publish.

  2. Malformed base64 in a nodes entry. Algorithm.decode64 (dns/tree/Algorithm.java:121) calls Base64.getUrlDecoder().decode() directly, which throws IllegalArgumentException. NodesEntry.parseEntry converts only InvalidProtocolBufferException and UnknownHostException into DnsException, so it escapes both that and the DnsException-only catch at dns/update/AliClient.java:138, aborting the whole collectRecords and the deploy() that called it.

A third, latent instance: BranchEntry.parseEntry (dns/tree/BranchEntry.java:19) does the same unguarded substring, and unlike its siblings does not declare throws DnsException. Its only caller checks the prefix first, so it is not currently reachable — a hazard for the next caller, not a live bug. LinkEntry.parseEntry shows the correct shape: prefix check, length check, and catch (RuntimeException) around the decode.

Reachability — operator-side, not peer-reachable

Path Guard Result
TCP wire decode Message.parse catch (Exception) contained
UDP discovery decode P2pPacketDecoder catch (Exception) contained
DNS sync / iteration RandomIterator.next() catch (Exception) around syncRandom() contained
DNS publish / collect only catch (DnsException) escapes

No remote peer can trigger these. Both live instances are on the operator's own publish path: they abort a DNS publish for whoever runs it, they do not give an attacker anything.

All three are pre-existing in libp2p and out of scope for a no-functional-changes PR, so they are reported rather than fixed. Instance 2 is pinned by a test that asserts the defectAwsClientRecordsTest.malformedBase64InANodesEntryAbortsTheWholeCollection fails loudly if the escape is ever closed.

Known security issues — pre-existing, deferred

These exist in libp2p today and are unchanged by this PR. Internalizing the source is what makes them fixable in-tree; each gets its own follow-up:

  • Weak PRNG for node ID generation
  • compressPubKey drops leading zeros
  • Unauthenticated handshake
  • Unbounded neighbour injection
  • AwsClient swallows InterruptedException without restoring the interrupt flag

Mixing any of them in would break the "no functional changes" claim, which is the only thing that makes a diff this size reviewable.

Test

Gate Result
Build — ubuntu24 / macos26 / debian11 / rockylinux PASS
:p2p:test PASS — 357 tests, 0 failures, 3 skipped
:framework:test PASS
:p2p:checkstyleMain / checkstyleTest PASS, 0 violations
check-math PASS
Integration test — single node smoke PASS
java -jar p2p-standalone.jar --help PASS — prints help
project :p2p on framework runtimeClasspath PASS
Zero external libp2p artifacts PASS
gson:2.9.0 -> 2.14.0, commons-lang3 at 3.20.0 PASS
No new implicit_dependency warning vs base PASS

Coverage

Vendoring puts 15,893 instructions into the repo-wide denominator that were not there before, and the overall-delta gate is sensitive to exactly that. The choice is between hiding vendored code from the metric and actually testing it; this PR does the latter.

Value Threshold
p2p instruction coverage 79.15% (12,580/15,893)
p2p line coverage 78.44% (2,983/3,803)
Changed-line coverage 78% > 60% PASS
Base overall 79.78%
PR overall 79.77%
Overall delta −0.0100% ≥ −0.1% PASS

The tests added here target what needs no live connection: the web3j copy, every kad discovery message through its own wire bytes, P2pPacketDecoder's drop paths for hostile datagrams, Channel's send guard and exception classification, ChannelManager.processPeer's admission branches, Tree's signing and TXT output, AwsClient's Route53 batching limits, AliClient.deploy's threshold decision, and P2pService, which had no test at all.

What remains uncovered needs a real socket: ConnPoolService.onConnect/onDisconnect/onMessage, NodeDetectService, PeerClient, Channel.init/send. Upstream's own SocketTest for exactly that is entirely commented out, so closing the gap means integration tests with real channels rather than more unit tests.

Known flakiness

Three org.tron.common.runtime.vm tests fail on their first attempt in a full local run and pass on retry — ValidateMultiSignContractTest.testTip854RejectsMalformedCalldata, AllowTvmLondonTest.testBaseFee, AllowTvmLondonTest.testStartWithEF. A full ./gradlew test on an unmodified release_v4.8.3 checkout produces the same three; both runs execute an identical set of 3,307 tests, and this PR adds none to :framework.

Two ported upstream tests need live DNS against a third-party zone and are left as-is rather than disabled, since they exercise real discovery behaviour: RandomTest.testRandomIterator and SyncTest both sync tree://…@nile.trondisco.net through hard-coded public resolvers. On a runner with restricted egress, or if that DNS tree is re-published or retired, they fail permanently. LookUpTxtTest in the same package already @Ignores its network tests, so that is the precedent if reviewers would rather these be skipped than retried; disabling them costs roughly 1.5 points of coverage, which still clears both gates.

A BindException in Metrics.init can cascade through four framework test classes (SRMetricsTest, PrometheusApiServiceTest, JsonrpcServiceTest, RpcApiServicesTest) that each bind the same hard-coded Prometheus port 9527 while the test task runs up to 4 parallel forks. Verified pre-existing by running those four classes together with no p2p tests in the run: it reproduces, and hits a different class each time. The race is java-tron's, not p2p's.

@cubic-dev-ai cubic-dev-ai Bot 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.

40 issues found across 167 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="p2p/src/main/java/org/tron/p2p/discover/message/kad/NeighborsMessage.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/message/kad/NeighborsMessage.java:73">
P1: A neighbour with an out-of-range port passes `valid()` and crashes discovery processing when `getNodeHandler` constructs its socket address. Reject ports outside `1..65535` here before accepting the datagram.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/connection/ChannelManager.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/connection/ChannelManager.java:66">
P2: After one service closes, a later `ChannelManager.init()` leaves `isShutdown` true, so all subsequent outbound connections and reconnection tasks are rejected. Reset the shutdown state when initializing a new manager.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/connection/ChannelManager.java:258">
P1: When an inbound peer sends a registered application message before Hello, this branch admits it and marks the handshake complete without network/version validation. Require `finishHandshake` before dispatching application data instead of setting it here.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/discover/Node.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/Node.java:135">
P2: Arbitrary binary node IDs are converted through the default charset before equality checks, so distinct peers can occasionally compare equal. Compare the byte arrays directly or use a lossless representation such as the existing hexadecimal encoding.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/discover/Node.java:155">
P1: Nodes with the same ID but different addresses compare equal yet hash differently, so the `HashSet`-based node aggregation can retain duplicates and hash-based lookups can miss an equal node. Hash the same identity fields used by `equals()` instead of `format()`.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/sync/Client.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/sync/Client.java:87">
P1: When a refresh fails after partially resolving a changed root, the next refresh treats that root as unchanged and discards entries fetched before the failure. Keep the previous `ClientTree` state until the complete snapshot succeeds, or rebuild the sync state from the new root after a failure.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/update/PublishService.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/update/PublishService.java:44">
P2: When `staticNodes` is configured, this branch performs one deployment and never schedules another. A transient first failure leaves DNS unpublished forever because `startPublish` catches the error; schedule retries after the initial publish.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/dns/update/PublishService.java:120">
P1: When a caller enables publishing without `dnsPrivate`, `checkConfig` still passes and this line builds an unsigned tree. The provider can receive an unsigned root that DNS clients reject; require a nonempty private key before accepting the publish configuration.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/update/AliClient.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/update/AliClient.java:199">
P1: When a DNS deletion returns a non-200 response, `submitChanges` still logs success and `deploy` completes because this boolean is discarded. Check the return value and throw before incrementing `deleteCount`.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/tree/Algorithm.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/tree/Algorithm.java:30">
P1: When the public-key integer starts with a zero nibble, `toString(16)` shortens the 128-digit coordinate pair before this substring, so the compressed key contains part of Y instead of the full X and DNS consumers reject the tree. Left-pad the public-key representation to 128 hex digits before extracting X.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/update/AwsClient.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/update/AwsClient.java:127">
P2: When the configured domain has a trailing dot, `collectRecords` and `tree.toTXT` use different key forms, so every existing record appears missing and Route53 rejects the CREATEs. Normalize the domain before collecting and rendering records.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/dns/update/AwsClient.java:330">
P1: When an existing TXT value differs only by letter case, this comparison suppresses the UPSERT. Route53 TXT RDATA is case-sensitive, so compare the joined values exactly.</violation>

<violation number="3" location="p2p/src/main/java/org/tron/p2p/dns/update/AwsClient.java:498">
P2: When Route53 returns a mixed-case name, `isSubdomain` rejects an otherwise valid record and deployment can issue CREATEs for records that already exist. Use a case-insensitive suffix comparison for DNS names.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/connection/socket/PeerServer.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/connection/socket/PeerServer.java:29">
P1: When shutdown begins before the asynchronous bind completes, `close()` returns because `listening` is still false, and `start()` subsequently binds a server that shutdown no longer closes. Synchronize startup and shutdown, or record shutdown before binding and close the channel as soon as it becomes available.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/connection/socket/PeerServer.java:71">
P2: When the configured TCP port cannot be bound, this handler only logs the error and leaves the p2p service running without a listener. Propagate the startup failure or expose it to the service so a node cannot report successful startup while accepting no TCP peers.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/table/DistanceComparator.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/table/DistanceComparator.java:15">
P1: When more than 16 peers share a leading XOR bucket, this comparator returns 0 for peers with different XOR distances, so `getClosestNodes` can omit closer peers and return arbitrary farther peers. Compare the full XOR distance after the bucket metric, or use the full XOR value directly.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/table/TimeComparator.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/table/TimeComparator.java:12">
P1: When a bucket is full, this ordering makes `NodeBucket.getLastSeen()` select the newest entry instead of the least-recently-seen node. Reverse the comparator ordering or select the final sorted entry so stale peers are challenged rather than the most recently seen peer.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/NodeHandler.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/NodeHandler.java:110">
P2: When an active peer reports a different network ID, this path marks the handler dead but leaves the peer in `NodeTable`, so discovery keeps querying an incompatible node. Drop the node from the table before transitioning it to `DEAD`.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/NodeHandler.java:113">
P1: When a peer has no address in the local IP family, the constructor leaves `state` null, so a matching `handlePing` crashes on `state.equals` and can close the UDP channel. Treat the null state as discovery (or compare the enum null-safely) before restarting the ping.</violation>

<violation number="3" location="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/NodeHandler.java:125">
P2: After a handler recovers from a timeout, `pingTrials` remains exhausted, so the next bucket challenge can evict a healthy node after one lost ping. Reset the retry counter after every successful pong.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/discover/socket/DiscoverServer.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/socket/DiscoverServer.java:72">
P1: When `close()` races with the initial bind, it can observe `channel == null` and return before this assignment. The start thread then waits forever on the newly bound channel's `closeFuture`, leaving discovery listening after `NodeManager.close()`; coordinate bind and close, safely publish the channel, and close a channel bound after shutdown.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/sync/SubtreeSync.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/sync/SubtreeSync.java:57">
P1: When DNS lookup fails, `resolveEntry` returns `null`, but this method passes it through and callers discard the hash. Full sync can commit an incomplete tree, and random discovery skips the entry until the root changes; reject `null` with `NO_ENTRY_FOUND` before removing it.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/tree/RootEntry.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/tree/RootEntry.java:67">
P2: When a DNS root value is shorter than `rootPrefix` or lacks that prefix, `parseEntry` throws `StringIndexOutOfBoundsException` instead of `DnsException`. `AwsClient.computeChanges` catches only `DnsException` around root parsing, so one malformed existing or new root aborts the publish; validate the prefix before calling `substring`.</violation>

<violation number="2" location="p2p/src/main/java/org/tron/p2p/dns/tree/RootEntry.java:70">
P2: When a DNS root payload or its signature is not valid URL-safe base64, `Algorithm.decode64` throws unchecked `IllegalArgumentException` outside `parseEntry`'s declared `DnsException` contract. A single corrupt root record therefore aborts `AwsClient`/`AliClient` record collection; catch decoder failures and convert them to `DnsException` like other invalid roots.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/tree/NodesEntry.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/tree/NodesEntry.java:29">
P2: When a DNS provider returns a malformed Base64 `nodes:` value, `Algorithm.decode64` throws `IllegalArgumentException` before this catch can create `DnsException`. Catch `IllegalArgumentException` here so one corrupt TXT record is skipped by the existing `DnsException` handlers.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/sync/RandomIterator.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/sync/RandomIterator.java:90">
P2: When no DNS tree URLs are configured, `pickTree()` calls `random.nextInt(0)` and `DnsManager.getRandomNodes()` throws instead of returning no node. Return `null` before choosing a random index when `size == 0`.</violation>
</file>

<file name="p2p/src/main/java/org/web3j/utils/Numeric.java">

<violation number="1" location="p2p/src/main/java/org/web3j/utils/Numeric.java:211">
P2: When `Hash.sha3(String)` receives malformed hex such as `0xzz`, `hexStringToByteArray` silently hashes substituted bytes instead of rejecting the input. Validate both nibbles, including the odd-length first nibble, and throw a decoding exception for invalid characters.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/DnsNode.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/DnsNode.java:29">
P2: Every `DnsNode` discards the supplied node ID before serialization. `PublishService` passes real IDs, but `compress` therefore emits endpoints without `nodeId`, losing peer identity across DNS publication; pass `id` to the superclass.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/example/StartApp.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/example/StartApp.java:210">
P2: When `--publish --change-threshold 0` or a negative value is supplied, this condition stores the invalid threshold and the DNS clients publish every non-empty change. Validate that the value is finite and strictly between zero and one before storing it.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/base/Parameter.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/base/Parameter.java:42">
P2: After `P2pService.start()` launches the channel threads, `TronNetService.start()` registers the application handler, so this mutation can race with callbacks. Use a thread-safe registry and make duplicate checking plus insertion atomic; otherwise callbacks can throw `ConcurrentModificationException`, lose handlers, or allow duplicate message types.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/connection/message/handshake/HelloMessage.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/connection/message/handshake/HelloMessage.java:62">
P2: A malformed peer endpoint with port `-1` or `0` passes handshake validation because `NetUtil.validNode` does not check ports. Reject ports outside `1..65535` here before storing the peer node.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/connection/business/handshake/HandshakeService.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/connection/business/handshake/HandshakeService.java:70">
P2: When an inbound peer sends a non-`NORMAL` hello code with the correct network ID, this branch accepts it and fires `onConnect`. Reject non-normal codes before the network-ID check, as the active-channel branch already does.</violation>
</file>

<file name="p2p/build.gradle">

<violation number="1" location="p2p/build.gradle:92">
P2: Consumers compiling directly against `:p2p` cannot resolve the Netty types in its public API because all Netty dependencies are implementation-only. Declare the Netty modules required by public signatures as `api` dependencies, or hide those Netty types behind the p2p API.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/connection/business/pool/ConnPoolService.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/connection/business/pool/ConnPoolService.java:236">
P2: When discovery returns multiple endpoints on one IP, the pool only deduplicates exact socket addresses before selecting the batch. Track selected and in-flight `InetAddress` values as well, otherwise the pool dials beyond `maxConnectionsWithSameIp` and relies on handshake rejection to clean up the excess.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/KadService.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/discover/protocol/kad/KadService.java:136">
P2: When a valid discovery packet arrives from an address family not configured locally, `getNodeHandler` returns a handler with null state and `handlePing` throws a `NullPointerException`. Ignore nodes without a preferred local-stack address before dispatching the event so one packet does not close and restart the discovery UDP channel.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/sync/LinkCache.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/sync/LinkCache.java:77">
P2: When a removed link leads to a downstream cycle, the cycle remains in `backrefs` because `stk` enqueues a node only after its parent set becomes empty. Traverse reachability from configured roots so `RandomIterator.rebuildTrees` drops stale client trees.</violation>
</file>

<file name="p2p/src/main/java/org/web3j/crypto/Sign.java">

<violation number="1" location="p2p/src/main/java/org/web3j/crypto/Sign.java:50">
P2: On a JVM whose default charset is not UTF-8, `getEthereumMessageHash` encodes the EIP-191 prefix differently from other Ethereum implementations, so prefixed signatures are not interoperable. Encode this prefix with UTF-8 explicitly.</violation>

<violation number="2" location="p2p/src/main/java/org/web3j/crypto/Sign.java:127">
P2: When an input signature has `s=0` or `s=n`, this nonnegative-only check accepts an invalid ECDSA component and recovery can return a computed public key for it. Require `1 <= r,s < CURVE.getN()` before recovering the key.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/utils/NetUtil.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/utils/NetUtil.java:120">
P2: When an IP-echo service returns a resolvable hostname such as `localhost`, `getExternalIp` validates the resolved address but returns the hostname. Validate `ip` itself as the requested literal family, or return the canonical address, before storing it in `P2pConfig`.</violation>
</file>

<file name="p2p/src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java">

<violation number="1" location="p2p/src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java:139">
P2: When concurrent or previously timed-out OS lookups fill this bounded executor, `submit` throws `RejectedExecutionException` and `lookUpIp` fails instead of falling back to public DNS. Treat executor saturation as an OS-resolution miss and continue with the fallback path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return false;
}
for (Node node : getNodes()) {
if (!NetUtil.validNode(node)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: A neighbour with an out-of-range port passes valid() and crashes discovery processing when getNodeHandler constructs its socket address. Reject ports outside 1..65535 here before accepting the datagram.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/discover/message/kad/NeighborsMessage.java, line 73:

<comment>A neighbour with an out-of-range port passes `valid()` and crashes discovery processing when `getNodeHandler` constructs its socket address. Reject ports outside `1..65535` here before accepting the datagram.</comment>

<file context>
@@ -0,0 +1,80 @@
+        return false;
+      }
+      for (Node node : getNodes()) {
+        if (!NetUtil.validNode(node)) {
+          return false;
+        }
</file context>
Suggested change
if (!NetUtil.validNode(node)) {
if (!NetUtil.validNode(node) || node.getPort() <= 0 || node.getPort() > 65535) {

return;
}

if (!channel.isFinishHandshake()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When an inbound peer sends a registered application message before Hello, this branch admits it and marks the handshake complete without network/version validation. Require finishHandshake before dispatching application data instead of setting it here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/connection/ChannelManager.java, line 258:

<comment>When an inbound peer sends a registered application message before Hello, this branch admits it and marks the handshake complete without network/version validation. Require `finishHandshake` before dispatching application data instead of setting it here.</comment>

<file context>
@@ -0,0 +1,307 @@
+      return;
+    }
+
+    if (!channel.isFinishHandshake()) {
+      channel.setFinishHandshake(true);
+      DisconnectCode code = processPeer(channel);
</file context>


@Override
public int hashCode() {
return this.format().hashCode();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Nodes with the same ID but different addresses compare equal yet hash differently, so the HashSet-based node aggregation can retain duplicates and hash-based lookups can miss an equal node. Hash the same identity fields used by equals() instead of format().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/discover/Node.java, line 155:

<comment>Nodes with the same ID but different addresses compare equal yet hash differently, so the `HashSet`-based node aggregation can retain duplicates and hash-based lookups can miss an equal node. Hash the same identity fields used by `equals()` instead of `format()`.</comment>

<file context>
@@ -0,0 +1,197 @@
+
+  @Override
+  public int hashCode() {
+    return this.format().hashCode();
+  }
+
</file context>

clientTree.syncAll(tree.getEntries());
} else {
Map<String, Entry> tmpEntries = new HashMap<>();
boolean[] isRootUpdate = clientTree.syncAll(tmpEntries);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a refresh fails after partially resolving a changed root, the next refresh treats that root as unchanged and discards entries fetched before the failure. Keep the previous ClientTree state until the complete snapshot succeeds, or rebuild the sync state from the new root after a failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/dns/sync/Client.java, line 87:

<comment>When a refresh fails after partially resolving a changed root, the next refresh treats that root as unchanged and discards entries fetched before the failure. Keep the previous `ClientTree` state until the complete snapshot succeeds, or rebuild the sync state from the new root after a failure.</comment>

<file context>
@@ -0,0 +1,188 @@
+      clientTree.syncAll(tree.getEntries());
+    } else {
+      Map<String, Entry> tmpEntries = new HashMap<>();
+      boolean[] isRootUpdate = clientTree.syncAll(tmpEntries);
+      if (!isRootUpdate[0]) {
+        tmpEntries.putAll(tree.getLinksMap());
</file context>

"The dns server type must be specified when enabling the dns publishing service");
return false;
}
if (StringUtils.isEmpty(config.getDnsDomain())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a caller enables publishing without dnsPrivate, checkConfig still passes and this line builds an unsigned tree. The provider can receive an unsigned root that DNS clients reject; require a nonempty private key before accepting the publish configuration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/dns/update/PublishService.java, line 120:

<comment>When a caller enables publishing without `dnsPrivate`, `checkConfig` still passes and this line builds an unsigned tree. The provider can receive an unsigned root that DNS clients reject; require a nonempty private key before accepting the publish configuration.</comment>

<file context>
@@ -0,0 +1,146 @@
+          "The dns server type must be specified when enabling the dns publishing service");
+      return false;
+    }
+    if (StringUtils.isEmpty(config.getDnsDomain())) {
+      logger.error("The dns domain must be specified when enabling the dns publishing service");
+      return false;
</file context>
Suggested change
if (StringUtils.isEmpty(config.getDnsDomain())) {
if (StringUtils.isEmpty(config.getDnsPrivate())) {
logger.error(
"The dns private key must be specified when enabling the dns publishing service");
return false;
}
if (StringUtils.isEmpty(config.getDnsDomain())) {

}

if (publishConfig.getStaticNodes() != null && !publishConfig.getStaticNodes().isEmpty()) {
startPublish();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When staticNodes is configured, this branch performs one deployment and never schedules another. A transient first failure leaves DNS unpublished forever because startPublish catches the error; schedule retries after the initial publish.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/dns/update/PublishService.java, line 44:

<comment>When `staticNodes` is configured, this branch performs one deployment and never schedules another. A transient first failure leaves DNS unpublished forever because `startPublish` catches the error; schedule retries after the initial publish.</comment>

<file context>
@@ -0,0 +1,146 @@
+      }
+
+      if (publishConfig.getStaticNodes() != null && !publishConfig.getStaticNodes().isEmpty()) {
+        startPublish();
+      } else {
+        publisher.scheduleWithFixedDelay(this::startPublish, 300, publishDelay, TimeUnit.SECONDS);
</file context>
Suggested change
startPublish();
startPublish();
publisher.scheduleWithFixedDelay(this::startPublish, publishDelay, publishDelay,
TimeUnit.SECONDS);

if (ip == null || ip.trim().isEmpty()) {
throw new IOException("Invalid address: " + ip);
}
InetAddress inetAddress = InetAddress.getByName(ip);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When an IP-echo service returns a resolvable hostname such as localhost, getExternalIp validates the resolved address but returns the hostname. Validate ip itself as the requested literal family, or return the canonical address, before storing it in P2pConfig.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/utils/NetUtil.java, line 120:

<comment>When an IP-echo service returns a resolvable hostname such as `localhost`, `getExternalIp` validates the resolved address but returns the hostname. Validate `ip` itself as the requested literal family, or return the canonical address, before storing it in `P2pConfig`.</comment>

<file context>
@@ -0,0 +1,294 @@
+      if (ip == null || ip.trim().isEmpty()) {
+        throw new IOException("Invalid address: " + ip);
+      }
+      InetAddress inetAddress = InetAddress.getByName(ip);
+      if (isAskIpv4 && !validIpV4(inetAddress.getHostAddress())) {
+        throw new IOException("Invalid address: " + ip);
</file context>

logger.debug("LookUp {} for domain: {}", useIPv4 ? "IPv4" : "IPv6", domain);

// Step 1: OS name resolver — honours /etc/hosts, so LAN mappings work without a DNS query.
Future<InetAddress[]> future = OS_RESOLVER_EXECUTOR.submit(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When concurrent or previously timed-out OS lookups fill this bounded executor, submit throws RejectedExecutionException and lookUpIp fails instead of falling back to public DNS. Treat executor saturation as an OS-resolution miss and continue with the fallback path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java, line 139:

<comment>When concurrent or previously timed-out OS lookups fill this bounded executor, `submit` throws `RejectedExecutionException` and `lookUpIp` fails instead of falling back to public DNS. Treat executor saturation as an OS-resolution miss and continue with the fallback path.</comment>

<file context>
@@ -0,0 +1,204 @@
+    logger.debug("LookUp {} for domain: {}", useIPv4 ? "IPv4" : "IPv6", domain);
+
+    // Step 1: OS name resolver — honours /etc/hosts, so LAN mappings work without a DNS query.
+    Future<InetAddress[]> future = OS_RESOLVER_EXECUTOR.submit(
+        () -> InetAddress.getAllByName(domain));
+    try {
</file context>

// uploads the given tree to Route53.
@Override
public void deploy(String domain, Tree tree) throws Exception {
checkZone(domain);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When the configured domain has a trailing dot, collectRecords and tree.toTXT use different key forms, so every existing record appears missing and Route53 rejects the CREATEs. Normalize the domain before collecting and rendering records.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/dns/update/AwsClient.java, line 127:

<comment>When the configured domain has a trailing dot, `collectRecords` and `tree.toTXT` use different key forms, so every existing record appears missing and Route53 rejects the CREATEs. Normalize the domain before collecting and rendering records.</comment>

<file context>
@@ -0,0 +1,511 @@
+  // uploads the given tree to Route53.
+  @Override
+  public void deploy(String domain, Tree tree) throws Exception {
+    checkZone(domain);
+
+    Map<String, RecordSet> existing = collectRecords(domain);
</file context>

public static boolean isSubdomain(String sub, String root) {
String subNoSuffix = postfix + StringUtils.strip(sub, postfix);
String rootNoSuffix = postfix + StringUtils.strip(root, postfix);
return subNoSuffix.endsWith(rootNoSuffix);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When Route53 returns a mixed-case name, isSubdomain rejects an otherwise valid record and deployment can issue CREATEs for records that already exist. Use a case-insensitive suffix comparison for DNS names.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At p2p/src/main/java/org/tron/p2p/dns/update/AwsClient.java, line 498:

<comment>When Route53 returns a mixed-case name, `isSubdomain` rejects an otherwise valid record and deployment can issue CREATEs for records that already exist. Use a case-insensitive suffix comparison for DNS names.</comment>

<file context>
@@ -0,0 +1,511 @@
+  public static boolean isSubdomain(String sub, String root) {
+    String subNoSuffix = postfix + StringUtils.strip(sub, postfix);
+    String rootNoSuffix = postfix + StringUtils.strip(root, postfix);
+    return subNoSuffix.endsWith(rootNoSuffix);
+  }
+
</file context>
Suggested change
return subNoSuffix.endsWith(rootNoSuffix);
return StringUtils.endsWithIgnoreCase(subNoSuffix, rootNoSuffix);

bladehan1 and others added 12 commits September 22, 2026 15:58
Vendors tronprotocol/libp2p tag v2.2.9 (c564f263d310d7a964035d3b597634aba6bda86d)
into a local `p2p` Gradle module, ahead of switching `common` off the external
io.github.tronprotocol:libp2p Maven artifact.

The source in this commit is byte-identical to `git archive v2.2.9 src/main`, so a
reviewer can diff it directly against the upstream tag and confirm nothing was
altered on the way in. Everything we change about it lands in the next commit,
separately and for exactly that reason.

Two arrangements differ from the upstream layout:

- The example code moves out of src/main into its own `example` sourceSet. It
  still compiles, so an API change in main surfaces here too, but it is not
  packaged into p2p.jar and is not run as tests.
- Generated protobuf sources are gitignored and rebuilt by :p2p:generateProto,
  so they stay out of the diff.

p2p tracks rootProject.grpcVersion rather than pinning libp2p's own gRPC version,
so the module cannot drift from the Netty the rest of the build resolves.
Three mechanical rewrites plus formatting, applied on top of the pristine v2.2.9
source added in the previous commit. Kept separate so commit 1 stays diffable
against the upstream tag.

- log. -> logger. (158 call sites). The root lombok.config sets
  lombok.log.fieldName=logger, so @slf4j generates `logger`, not `log`.

- Math. -> StrictMath. (6 sites). CI enforces a check-math rule that rejects
  java.lang.Math anywhere in the tree, to keep arithmetic deterministic across
  JVMs and architectures.

- toLowerCase()/toUpperCase() -> Locale.ROOT (4 sites). The root build enables
  errorprone StringCaseLocaleUsage as ERROR on every subproject except protocol
  and errorprone, so this is compile-forced. It is the one change here that is
  not purely cosmetic: behaviour is identical for ASCII but differs under a
  Turkish locale. Matches the project's own idiom in Args.java:1273.

BasicThreadFactory is deliberately left as upstream writes it -- twelve
`builder()` calls and the one `new BasicThreadFactory.Builder()` in
DiscoverServer. The root build provides commons-lang3 3.20.0, where `builder()`
exists and the no-arg `Builder()` constructor is @deprecated, so rewriting
either way would add deviation from the tag and, in one direction, thirteen
deprecation warnings.

Formatting: google-java-format over the 9 vendored org/web3j/** files, which
came in AOSP 4-space style, plus import reordering and hand fixes for the
remainder. This takes :p2p:checkstyleMain from 605 violations to 0.

No functional change beyond the Locale.ROOT note above.
Replaces io.github.tronprotocol:libp2p:2.2.9 with `api project(":p2p")`,
collapsing 17 lines of dependency plus excludes into one.

The dom4j exclusion tail (jaxen, stax-api, msv, xsdlib, relaxngDatatype,
pull-parser, xpp3) that used to sit on the libp2p dependency here does not
disappear: it arrives via the Aliyun and Route53 SDKs, which are now p2p's own
dependencies. The exclusions move with them, into a configurations.configureEach
block in p2p/build.gradle. Dropping them would silently re-admit artifacts the
project has excluded for years.

Removing a dependency also removes it as a version requester, so every version
the libp2p POM declared was checked against what :p2p now declares. All twelve
match, except one deliberate difference:

- grpc-netty: libp2p pinned 1.81.0; :p2p tracks rootProject.grpcVersion (1.83.1)
  so it cannot drift from the Netty the rest of the build resolves.

commons-lang3 needs no declaration in :p2p at all. The root build already hands
every subproject `implementation "org.apache.commons:commons-lang3:3.20.0"`,
and an `implementation` dependency is on the declaring project's own compile
classpath -- it is only hidden from that project's consumers. Verified by
compiling :p2p main and test sources with no commons-lang3 line in
p2p/build.gradle.

Adding a project to the dependency graph also needs three task-dependency edges
that an external jar did not, each of which Gradle reported as an
implicit_dependency and answered by disabling execution optimizations:

- framework's buildFullNodeJar and plugins' binaryRelease both zip up
  runtimeClasspath and maintain a hand-written dependsOn list of project jars.
  :common now exposes p2p via `api`, so p2p-1.0.0.jar is on both classpaths;
  without the edge a parallel build could assemble the shipped fat jar before
  :p2p:jar exists.
- p2p's own processExampleResources reads src/example/resources, which the
  protobuf plugin claims as an output of generateExampleProto because
  generatedFilesBaseDir points at $projectDir/src.

verification-metadata.xml gains three components that resolve once p2p compiles
in-tree: bcutil-jdk18on:1.84, gson:2.9.0 and gson-parent:2.9.0. Checksums were
taken from Maven Central and cross-checked against the published .sha1.

gson 2.9.0 is older than the 2.14.0 used elsewhere, and that is fine: it only
appears on :p2p's isolated compile classpath. :framework's runtimeClasspath
still resolves gson:2.9.0 -> 2.14.0.

Verified with :framework:dependencies and :framework:dependencyInsight on
runtimeClasspath: `project :p2p` present, no external libp2p artifact, gson at
2.14.0 and commons-lang3 at 3.20.0 with :p2p among the requesters -- the same
versions the node resolved before.

A full build adds no implicit_dependency warning. The one it still reports,
:chainbase:jacocoTestReport, is present on release_v4.8.3 without this change.
Netty 4.2 split io.netty.handler.codec.protobuf out of netty-codec into its
own artifact, so both :framework and :p2p have to declare it explicitly --
each puts the varint32 framing codecs on its channel pipelines.

Both carried the literal 4.2.15.Final. Netty itself is not declared anywhere;
it arrives transitively through grpc-netty, which :p2p tracks as
rootProject.grpcVersion. So a grpc bump moves Netty while these two literals
stay put -- exactly the mismatch that broke p2p's pipeline when develop moved
to Netty 4.2 in the first place.

Extract nettyVersion next to grpcVersion so the coupling is visible in one
place. Resolution is unchanged: netty-codec-protobuf still resolves to
4.2.15.Final on both :framework:compileClasspath and :p2p:compileClasspath.
:framework uses org.tron.p2p in 17 files under src/main/java, but declared no
dependency on it. The types arrive three hops away, through
:common -> :crypto -> :chainbase, because common exposes p2p with
`api project(":p2p")`.

That export is not a mistake and is not removable here: CommonParameter
publishes `P2pConfig p2pConfig` and `PublishConfig dnsPublishConfig` as public
@Getter fields, so p2p types are part of :common's own API surface. Narrowing
it to `implementation` would break every caller of getP2pConfig(). Actually
de-coupling the graph means moving those fields out of CommonParameter, which
is a functional refactor and out of scope for this PR.

What is fixable now is the undeclared direct use. Declare it, so framework does
not depend on an unrelated module's export choice for code it uses itself.

api rather than implementation, because framework re-exports p2p types itself:
P2pEventHandlerImpl extends org.tron.p2p.P2pEventHandler, HelloMessage.getFrom()
returns org.tron.p2p.discover.Node, PeerManager.add/remove take
org.tron.p2p.connection.Channel, and Args.loadDnsPublishConfig returns
PublishConfig. implementation would compile today only because the transitive
api chain still supplies those types to consumers -- the moment that chain is
narrowed, it breaks.

No resolution change -- p2p was already on framework's compile and runtime
classpaths via the transitive api.
All 23 of v2.2.9's own test files, plus new ones, in p2p/src/test/java next to
the code they cover -- the same place `common` and `plugins` keep theirs.

Two upstream tests were unreliable by construction and are fixed rather than
carried over as-is:

- NetUtilTest.testGetIP called three public IP-echo services and asserted all
  three returned the same string: a network dependency, and a coin flip on any
  host with more than one egress address. It now runs against a loopback
  HttpServer, which exercises the same fetch/parse/validate path deterministically
  and covers the rejection branches too. libp2p's own CI never ran its tests, so
  this had not surfaced.
- ConnPoolServiceTest.getNodes_orderByUpdateTimeDesc asserted that getNodes()
  returns nodes ordered by updateTime descending. getNodes() sorts, truncates to
  max(limit * 10, 50) candidates, then calls Collections.shuffle() -- so with two
  nodes that assertion passes about half the time. It now asserts membership, and
  getNodes_prefersNewestAboveCandidateSize covers the descending sort where it is
  actually observable: above the candidate bound.

Two further defects made coverage depend on fork scheduling rather than on what
the tests assert:

- ConnPoolServiceTest and SocketTest bound fixed ports (10000 / 10001).
  PeerServer.start only logs on bind failure, so a collision let them pass while
  exercising nothing. Both now take a free port from
  PublicMethod.chooseRandomPort(), which is what java-tron's own tests use.
- NodeTableTest read Parameter.p2pConfig without ever setting it, so it depended
  on an earlier class in the same fork having done so. Running the class on its
  own failed all eleven methods -- on this branch and on the unmodified one
  alike. It now sets up and restores its own config, and no longer shares one id
  array between two nodes.

The added tests target what needs no live connection: the web3j copy (Numeric,
Strings, Hash, Sign, ECKeyPair, ECDSASignature), every kad discovery message
through its own wire bytes, P2pPacketDecoder's drop paths for hostile datagrams,
Channel's send guard and exception classification, ChannelManager.processPeer's
admission branches (ban list, global cap, per-IP cap, duplicate node id), Tree's
signing and TXT output, AwsClient's Route53 batching limits, AliClient.deploy's
threshold decision, and P2pService, which had no test at all.

Where a collaborator is genuinely external -- the Aliyun SDK, process-wide
ChannelManager state -- it is mocked, so the logic under test is real and only
the transport is faked.

This takes the module from the ~35% its own tests reach to 79.15% instruction
coverage (12,580/15,893) and 78.44% line coverage (2,983/3,803). What remains
uncovered needs a real socket: ConnPoolService.onConnect/onDisconnect/onMessage,
NodeDetectService, PeerClient, Channel.init/send. Upstream's own SocketTest for
exactly that is entirely commented out, so closing the gap means integration
tests with real channels rather than more unit tests.

One import was dropped: NodeHandlerTest had an unused
org.checkerframework.checker.units.qual.N import, an IDE auto-import artifact
that does not resolve on this classpath.
`DnsExample1`, `DnsExample2` and `ImportUsing` documented how an embedder
configures and drives this module, but they only ever compiled. Each ended in a
`while (true)` loop, bound a fixed port and pointed at live seed nodes, so
nothing they demonstrated was checked -- and cubic found real defects sitting in
them: `TestMessage` is not serializable so `ByteArray.fromObject` returns null
and `Channel.send` closes the channel, and `DnsExample1` carried a signing
private key in copyable code.

Porting them line by line would produce three slow, network-dependent,
port-bound tests. What is worth pinning is the contract they advertised: those
configuration shapes are still accepted and still mean what the comments said.
External embedders copy them, so a renamed setter or tightened validation is a
breaking change even though nothing in this repo calls them.

`ExampleUsageTest` covers all three shapes -- the connection-tuning surface, the
register/start/query/close lifecycle on a free port with discovery off, the
duplicate-message-type rejection, the AwsRoute53 publish config, and the
discovery-off + tree-urls sync config. The signing key moves into the test as a
fixture; it is upstream's well-known test key, already used by AlgorithmTest,
and an embedder has to supply their own.

`StartApp` moves to `src/main/java` and stays, as the entry point for debugging
the module without starting java-tron. Moving it out of the exempt sourceSet
subjects it to the project's checkstyle for the first time: three over-long
lines, wrapped.

It also carried a real bug. `--trust-ips` is declared as `ip[,ip[...]]` but
resolved the whole comma-separated value as a single hostname, so with more than
one address none of the listed peers became trusted. It now splits and resolves
each, skipping and logging any that do not resolve.

The `example` sourceSet and all of its build wiring are gone: the sourceSet
block, the two extendsFrom configurations, the checkstyle opt-out, the encoding
override, the Lombok wiring, and the `processExampleResources` task edge that
only existed because `generatedFilesBaseDir` points into `src/`.

351 tests, 0 failures, 3 skipped. `:p2p:build` clean.
…mple

The README is promoted from src/example/resources/README.md, where no reader
would find it. It still read as upstream's document: four source links pointed at
github.com/tronprotocol/libp2p, the invocations named libp2p.jar, and the prose
described libp2p as a standalone project. Links are module-relative now, the
prose is about this module, and the header states the provenance once -- the one
remaining upstream link.

StartApp gets the fixes that made it worth documenting. It left the
checkstyle-exempt sourceSet, so it is linted for the first time: three over-long
lines, wrapped. It also carried a real bug -- --trust-ips is declared
ip[,ip[...]] but resolved the whole comma-separated value as a single hostname,
so with more than one address none of the listed peers became trusted.

logback.xml.example came from libp2p as a standalone project. Inside java-tron
framework/src/main/resources/logback.xml is what applies, and the sample was
being packaged into the jar for no reason.
…face

Two adjustments to what this module's jacoco report measures. Neither is the
code getting worse; both follow from where StartApp now lives.

**StartApp, 981 instructions at 0%.** Moving it out of the `example` sourceSet
into src/main/java put it in the coverage denominator for the first time --
that sourceSet was exempt from both checkstyle and coverage. It is argument
parsing, option declarations and a main() that starts services and blocks: not
module logic, and not code the node runs. `org/tron/p2p/example/**` is now
excluded from this module's report alongside `**/protos/**`, which keeps the
measured surface the same as before the move rather than hiding newly counted
logic.

The two parsing helpers it does own are real logic, and one of them shipped the
`--trust-ips` bug fixed in the previous commit, so they are package-private now
and `StartAppArgsTest` covers them: comma splitting, whitespace, unresolvable
entries, and the bracketed-IPv6 form of `parseInetSocketAddressList`. The
exclusion does not take regression protection with it.

**Coverage that :framework's tests contribute.** :framework's own tests --
org.tron.core.net and friends -- execute a good deal of this module's code, and
that coverage is real. :p2p:jacocoTestReport reads framework's exec data so it
is counted where the classes actually live. It recovers only 70 instructions,
not the ~700 an earlier measurement suggested: the tests this PR adds already
reach most of what framework's tests were covering. The fileTree is empty when
:framework:test has not run, so :p2p:build on its own still works.

p2p instruction coverage after this commit: 79.15% (12,580/15,893).
Three follow-ups from review.

**`java -jar` failed.** The commit that moved StartApp into src/main put
`Main-Class` on the plain jar,
which is thin: the entry point resolved and then died on the first dependency it
touched, `NoClassDefFoundError: org/apache/commons/cli/ParseException`. That is
worse than declaring nothing -- it advertises support that cannot work. I had
only verified the `-cp` form documented in the README, not `java -jar` itself.

The plain jar drops `Main-Class` again and now fails honestly with "no main
manifest attribute". `buildStandaloneJar` produces `p2p-standalone.jar` with the
runtime classpath bundled, following :framework's FullNode.jar and :plugins'
Toolkit.jar -- same `artifacts { archives(...) }` wiring, the same
`-PbinaryRelease=false` opt-out, and the same exclusions for Bouncy Castle's
signatures and dnsjava's resolver SPI. Verified by running it: `java -jar
p2p/build/libs/p2p-standalone.jar --help` prints the help.

The `printRuntimeClasspath` helper is gone; it existed only to work around the
thin jar.

**The README still read as upstream's.** Four source links pointed at
github.com/tronprotocol/libp2p and the prose described libp2p as a standalone
project. Links are module-relative now, the prose talks about this module, and
the header states the provenance once and explains which of the two jars to use.
The one remaining upstream link is that attribution.

**Duplicated versions.** `protobufVersion` 3.25.8 was declared in both
:protocol and :p2p; checkstyle 8.7 sat in a local `versions` map in :framework
and :plugins and as a literal in :p2p. Both move to the root `ext` beside
`grpcVersion` and `nettyVersion`, and all four modules reference them, so the
duplication is removed rather than relocated. Resolution is unchanged:
`protobuf-java:3.25.8` on :p2p's compile classpath.
`src/main/protos` with an explicit `srcDir` override builds fine, but IDEA's
protobuf plugin does not read that override -- it resolves imports against the
plugin's default path -- so `import "Discover.proto"` in Connect.proto and every
type it brings in showed as unresolved in the editor.

Renamed to `src/main/proto`, the default, and dropped the sourceSet override.
Generated sources still land in `src/main/java/org/tron/p2p/protos` via
`generatedFilesBaseDir` and are still gitignored; `clean` still removes them.

Worth flagging for whoever picks this up: `:protocol` has the identical setup --
`src/main/protos` plus the same explicit `srcDir`, with imports relative to that
root -- so it presumably shows the same red in IDEA. This commit leaves it
alone, which means the two modules now differ. Aligning `:protocol` is a
one-directory rename too, but it is core java-tron with a `src/main/gen`
interplay and does not belong in this PR.

357 tests, 0 failures, 3 skipped. `:p2p:build` clean.
@barbatos2011
barbatos2011 force-pushed the chore/internalize-libp2p-v229-483 branch from 2e50f85 to 99c6344 Compare September 22, 2026 08:50
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