Repository navigation
fix(net): reject inconsistent terminal sync replies #7079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release_v4.8.3
Are you sure you want to change the base?
Changes from all commits
a8d9cfd
6c90efc
daa5bd4
3916a58
722061c
9b52e54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,10 +18,12 @@ | |||||||||||||||||||||||||||||||||
| import org.tron.core.exception.P2pException.TypeEnum; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.TronNetDelegate; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.message.TronMessage; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.message.handshake.HelloMessage; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.message.sync.ChainInventoryMessage; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.peer.PeerConnection; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.peer.TronState; | ||||||||||||||||||||||||||||||||||
| import org.tron.core.net.service.sync.SyncService; | ||||||||||||||||||||||||||||||||||
| import org.tron.protos.Protocol.Inventory.InventoryType; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @Slf4j(topic = "net") | ||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||
|
|
@@ -95,6 +97,7 @@ public void processMessage(PeerConnection peer, TronMessage msg) throws P2pExcep | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private void check(PeerConnection peer, ChainInventoryMessage msg) throws P2pException { | ||||||||||||||||||||||||||||||||||
| HelloMessage hello = peer.getHelloMessageReceive(); | ||||||||||||||||||||||||||||||||||
| Pair<Deque<BlockId>, Long> requested = peer.getSyncChainRequested(); | ||||||||||||||||||||||||||||||||||
| if (requested == null) { | ||||||||||||||||||||||||||||||||||
| throw new P2pException(TypeEnum.BAD_MESSAGE, "not send syncBlockChainMsg"); | ||||||||||||||||||||||||||||||||||
|
|
@@ -147,6 +150,39 @@ private void check(PeerConnection peer, ChainInventoryMessage msg) throws P2pExc | |||||||||||||||||||||||||||||||||
| + ", futureMaxNum: " + maxFutureNum); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (msg.getRemainNum() == 0) { | ||||||||||||||||||||||||||||||||||
| BlockId lastBlockId = blockIds.get(blockIds.size() - 1); | ||||||||||||||||||||||||||||||||||
| long lastNum = lastBlockId.getNum(); | ||||||||||||||||||||||||||||||||||
| long helloHeadNum = hello.getHeadBlockId().getNum(); | ||||||||||||||||||||||||||||||||||
| if (lastNum < helloHeadNum) { | ||||||||||||||||||||||||||||||||||
| // A fork rollback can temporarily lower the head; allow a fresh HELLO on reconnect. | ||||||||||||||||||||||||||||||||||
| throw new P2pException(TypeEnum.SYNC_FAILED, | ||||||||||||||||||||||||||||||||||
| "lastNum " + lastNum + " (remainNum=0) is below hello head " + helloHeadNum); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // A peer that has the terminal block in our summary should reply with that block alone. | ||||||||||||||||||||||||||||||||||
| if (blockIds.size() > 1 && requested.getKey().contains(lastBlockId)) { | ||||||||||||||||||||||||||||||||||
| // A concurrent fork switch can also produce this response; use a sync failure. | ||||||||||||||||||||||||||||||||||
| throw new P2pException(TypeEnum.SYNC_FAILED, | ||||||||||||||||||||||||||||||||||
| "terminal block already in summary: " + lastBlockId.getString()); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| long requestedAt = requested.getValue(); | ||||||||||||||||||||||||||||||||||
| // Only use recent announcements received before this request, not a newer head | ||||||||||||||||||||||||||||||||||
| // announced while the peer was preparing its response. A concurrent fork switch also cause it | ||||||||||||||||||||||||||||||||||
| long maxAdvInvNum = peer.getAdvInvReceive().asMap().entrySet().stream() | ||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Please consider maintaining recent BLOCK announcements separately, avoiding a scan of the entire mixed transaction/block cache just to check a small number of block announcements.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The observation that First, this operation is a single-pass, in-memory reduction with O(N) complexity, rather than a sorting operation. Transaction entries are rejected by the first type filter. Timestamp filtering and BlockId construction are performed only for the relevant BLOCK entries, with block-number extraction limited to announcements eligible for this request. The scan itself performs no database reads or network operations. Second, 20,000 is the cache’s maximum capacity, not its actual entry count on every invocation. We should still account for a full cache, but the worst-case work remains a bounded linear traversal. Capacity establishes an upper bound; it does not demonstrate an unacceptable processing delay. Third, the invocation frequency matters. This check executes only for terminal chain inventory replies ( The assessment also includes visiting entries outside the 15-second eligibility window. That window determines which announcements can constrain the response; it does not remove those entries from the cache or eliminate their traversal cost. The justification is the combination of bounded work, linear complexity, and low invocation frequency. Maintaining BLOCK announcements separately would add per-peer state and require coordinated updates, expiration, cleanup, and concurrency handling. It would also need to preserve the current semantics: considering recent announcements received before the particular synchronization request, while excluding later announcements. That is additional complexity for a performance benefit that has not yet been quantified. This scan has a nonzero cost, but there are currently no measurements showing that it causes meaningful handler latency or allocation pressure. I therefore suggest retaining the existing implementation in this focused correctness fix. If profiling demonstrates a material bottleneck, a separate BLOCK-announcement structure can be evaluated as a follow-up optimization. |
||||||||||||||||||||||||||||||||||
| .filter(entry -> entry.getKey().getType() == InventoryType.BLOCK) | ||||||||||||||||||||||||||||||||||
| .filter(entry -> entry.getValue() <= requestedAt | ||||||||||||||||||||||||||||||||||
| && entry.getValue() >= requestedAt - NetConstants.ADV_TIME_OUT) | ||||||||||||||||||||||||||||||||||
| .mapToLong(entry -> new BlockId(entry.getKey().getHash()).getNum()) | ||||||||||||||||||||||||||||||||||
| .max() | ||||||||||||||||||||||||||||||||||
| .orElse(lastNum); | ||||||||||||||||||||||||||||||||||
| if (lastNum < maxAdvInvNum) { | ||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The existing If we initiate a sync request within 15 seconds of receiving that announcement, the peer can truthfully return Please avoid treating a terminal reply as a sync failure solely on the basis of the announced height, and add a regression test for broadcasting a block followed by a local application failure.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We agree that However, this check is intended as a defensive synchronization-source policy. It addresses a repeated no-progress cycle observed in a production FullNode synchronization-stall incident. A malicious peer can exploit the following sequence:
The following is a sanitized, condensed extract from the observed logs. Peer identities and hashes are removed, heights are expressed relative to the stalled local head This was a 63.350-second interval without advancement beyond Across the inspected log file, we found 36 such replies from 10 peer endpoints, out of 80 received ChainInventory replies. All 36 were singleton replies echoing the exact requested summary-tail hash with These observations establish the repeated message pattern, not malicious intent. Even receiving the higher block body does not prove that the peer successfully applied it. The defensive decision does not require that proof: we temporarily stop relying on a source whose recent announcements and terminal synchronization response indicate inconsistent availability. Consequently, an honest peer affected by a fork or application failure may also be disconnected. We intentionally accept that possibility, using The core tradeoff is to accept occasional false-positive disconnects to reduce the risk of sustained synchronization stalls. This check breaks continued reliance on that connection; it does not claim to identify malicious peers conclusively or guarantee immediate recovery.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the log extract, it makes the target clear. All 36 bad replies have the same shape: The three throws in Suggest scoping the policy to the observed signature: throw
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The broadcast-before-apply race is possible, but its practical likelihood needs to account for the complete message exchange. Receiving a BLOCK INV does not directly trigger synchronization. The receiver first fetches the block and discovers that its parent is missing: From P broadcasting the inventory to P reading its head for the synchronization response, this involves approximately two network round trips, plus block transfer, message processing, summary generation, and any queueing: Using 300ms as an illustrative block execution time, an RTT of 150–180ms already accounts for approximately 300–360ms before those additional costs. Under normal, successful block processing, P will therefore often have applied the advertised block by the time it handles The incident logs also support this timing argument. Among the 36 previously documented contradictory replies:
Assuming execution starts promptly after broadcasting and successfully completes within 300ms, those 25 cases cannot be explained solely by the responder still executing that advertised block. This does not eliminate the race for low-latency connections, unusually slow execution, application failures, or fork switches. However, the existence of the race alone does not establish that honest-peer disconnects would be frequent. In the observed deployment, the complete exchange often provided more time than the assumed block execution duration. We accept occasional defensive disconnects to reduce the risk of sustained synchronization stalls.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also measured the actual local block-processing times from the production FullNode logs, covering approximately 9.5 hours. The dataset contains 10,968 successfully processed broadcast blocks, excluding duplicate-block fast returns and other cases without actual block processing. All durations below are in milliseconds:
The broadcast-to-success measurement is particularly relevant here: it includes broadcast overhead and subsequent lock waiting, rather than measuring only transaction execution. Records were matched by thread and complete block ID. These measurements make 300ms a conservative reference for ordinary broadcast-block processing on this FullNode, although longer processing times do occur. For comparison, in 33 of the 36 contradictory-reply cases (91.67%), the local interval from receiving the preceding BLOCK INV to sending These are local measurements, not direct measurements of the remote peers. Nevertheless, they support the timing argument: the complete INV → FETCH → BLOCK → SYNC exchange will often outlast ordinary block processing. The broadcast-before-apply race remains possible, but its existence alone does not establish that this check would frequently disconnect honest peers. |
||||||||||||||||||||||||||||||||||
| throw new P2pException(TypeEnum.SYNC_FAILED, | ||||||||||||||||||||||||||||||||||
| "lastNum " + lastNum + " (remainNum=0) is below advertised block " + maxAdvInvNum); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also treats a temporary inconsistency caused by a legitimate fork switch as a sync failure. For example, a peer can advertise height 200 in HELLO, switch forks and temporarily roll back to 198, then be disconnected for returning
lastNum=198, remainNum=0. A fork switch interleaved with summary generation can produce a similar result.Although the PR description and tests explicitly accept this behavior, recovery still requires the connection cooldown, reconnection and a new handshake. Please consider bounded renegotiation before disconnecting a peer for a persistent inconsistency, and add coverage for an honest peer recovering synchronization after a fork switch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out. We agree that a legitimate fork switch can temporarily make the current head inconsistent with the earlier HELLO snapshot, and that interleaving a fork switch with summary generation can produce a similar response. This PR intentionally accepts a recoverable disconnect in these cases; the check is not intended to establish that the peer is malicious.
For the HELLO-triggered initial synchronization case, the peer advertised a higher head, so we attempted to synchronize from it. If a concurrent fork switch makes that attempt fail the consistency checks, we consider it acceptable to abandon this connection’s initial synchronization attempt. More precisely, this is a post-handshake initialization failure, since the HELLO exchange itself has already completed.
With other available peers, the node does not need to wait for this particular peer’s cooldown, reconnection, and fresh handshake before making synchronization progress.
SYNC_FAILfollows the ordinary disconnect/cooldown path rather than requesting the longer penalty associated withBAD_PROTOCOLorBAD_BLOCK. We therefore accept the cost of temporarily losing this candidate synchronization source. The same policy applies when an existing connection encounters this inconsistency during a later synchronization attempt.Bounded renegotiation could preserve some honest connections, but it would introduce retry accounting, timeout handling, and additional state transitions. Given the intended tradeoff—accepting occasional disconnects while avoiding repeated unproductive synchronization attempts—we do not consider same-connection renegotiation necessary for this PR.
The existing fork-switch test deliberately expects
SYNC_FAILto document this policy. Additional coverage showing that a fresh connection can synchronize after the fork has settled would be useful, but it can be added independently of bounded renegotiation.