Conversation
…ory is confirmed; revert to BAD_BLOCK when processSyncBlock
This was referenced Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fix block-handling paths where unverified announcements can refresh peer activity, invalid responses can complete fetches too early, and a short fetch timeout can leave later announcements unable to trigger a retry. The changes also separate transaction requests from block-fetch eligibility and make
blockRcvTimereflect the result of processing a block supplied by that peer.This is the "block fetching, validation, and contribution timestamps" work described as PR 2 in
peer-quality-evaluation-design.md(sections 4.3–4.5, 5.2–5.3, and 6.3). Related parts of that design have separate PRs:syncChainRequestedtimeoutThese are separate fixes; #6993 and #7000 are not code dependencies of this PR. The details below describe the implementation against
release_v4.8.3.1. Unverified BLOCK INV can refresh
lastInteractiveTimeBug and impact
InventoryMsgHandlerpreviously updatedlastInteractiveTimewhen a BLOCK INV advertised a height above the local head andadvInvSpreaddid not contain the item. At that point, the peer had supplied only a block hash. The node had not received or validated the block.A peer could therefore advertise invented block IDs to appear active without delivering valid block data. At the same time, a peer that announces a valid block may not be selected to provide its body, so its announcement needs to be distinguished from actual block delivery.
Solution
Call
AdvService.recordInventory(peer, item, receivedAt)beforeaddInv(). Continue recording inventory inadvInvReceive, and record an eligible BLOCK announcement's receipt time separately inPeerConnection.advBlockInvReceive.Eligibility is captured at receipt: the block height must exceed the local head, and
advInvSpreadmust not contain the item. Capturing it before fetching matters because an immediate response can advance the head or broadcast the block before the inventory handler finishes.After the matching block is successfully processed through the broadcast or sync path, and after the
isHitDown()check, callconfirmBlockInventory(blockId). It removes the matching pending entry and callsupdateLastInteractiveTime(receivedAt). A different hash at the same height does not confirm the announcement.Confirmation uses the recorded receipt time, and
updateLastInteractiveTime()only advances the timestamp. Announcements do not updateblockRcvTime. The pending cache holds at most 100 entries, expires entries one minute after write, and is cleared on disconnect; its eviction does not removeadvInvReceiveinformation needed for fetching.2. Invalid BLOCK responses can complete a fetch before validation
Bug and impact
The broadcast-block path removed the responding peer's
advInvRequestentry and calledblockFetchSuccess()beforetronNetDelegate.validBlock(). A requested block with a bad signature or Merkle root could therefore clear both the request record and retry tracking before being rejected.When the peer was then disconnected,
AdvService.onDisconnect()could no longer use that request entry to retry the block from another peer.Solution
In
BlockMsgHandler.processMessage(), check the raw parent hash length and the parent/child block numbers beforesanitize(). Reject a nonpositive block number, a parent hash whose size is notSha256Hash.LENGTH, or a parent number other thanblockNum - 1.In the broadcast
processBlock()path, calltronNetDelegate.validBlock()beforeblockFetchSuccess()and removal of the peer'sadvInvRequestentry. This preserves the request when structural, Merkle, or signature validation throws, so disconnect recovery can still find it.Fetch completion remains separate from full block execution. Once
validBlock()returns without throwing, the response completes the fetch; itsactiveWitnessresult then determines whether processing continues or synchronization starts. An inactive witness does not receive either timestamp update.3. A normally returning handler does not necessarily mean useful block delivery
Bug and impact
P2pEventHandlerImplrefreshedlastInteractiveTimeafter a BLOCK handler returned normally. However,BlockMsgHandler.processBlock()could return for an inactive witness or catch a processing exception internally. The dispatcher could not distinguish these outcomes from successful processing. Sync blocks could also return from the message handler before asynchronous validation and processing completed.Contribution updates had different gaps: a validated block with a missing parent did not update
blockRcvTime, while already-known blocks or shutdown returns could reach an update without useful processing. These cases should not be treated as the same result.Solution
Remove BLOCK from the dispatcher's generic
updateLastInteractiveTime()branch. In the broadcast path, update the sending peer'slastInteractiveTimeonly aftervalidBlock()returnstrue. Return an explicitBlockResultand let the caller updateblockRcvTimeonly forACCEPTEDorSYNC_REQUIRED.BlockResultlastInteractiveTimeblockRcvTimeisHitDown()is falseACCEPTEDSYNC_REQUIREDIGNOREDisHitDown()IGNOREDvalidBlock()returnsfalsefor an inactive witness; start syncSTATE_FAILEDprocessBlock(block, false)throws; start syncSTATE_FAILEDStructural, Merkle, and signature validation failures throw before these successful-validation timestamp updates.
Keep
broadcast()after validation and the head/parent checks, beforeprocessBlock(block, false). Moving broadcast after full execution would add execution time to every propagation hop. Broadcast-path execution failures start synchronization without converting every exception into a bad-block penalty.In
SyncService.processSyncBlock(), updatelastInteractiveTimeaftervalidSignature()andprocessBlock(block, true)succeed andisHitDown()is false. UpdateblockRcvTimeonly ifusefulwas true before processing: the block number was at least the local head andcontainBlock(blockId)was false. Both paths updateblockRcvTimeonly for the peer that supplied the block.The existing sync-path
BAD_BLOCKhandling remains, including its provider-only signature/Merkle failure branch. This PR does not complete the design's broader penalty-classification work.4. Pending TRX requests and a hard latency cutoff can exclude usable block peers
Bug and impact
Initial and backup fetching used
PeerConnection.isIdle(), which requires the entireadvInvRequestmap to be empty. A peer with pending TRX requests was therefore excluded even if it had announced the next missing block and had no block or sync work outstanding.Backup selection also discarded peers whose
getPeerTop75()exceededfetchBlockTimeout. This could exclude the only available alternative after the original request had already waited long enough.Solution
Add
isBlockFetchIdle(): no BLOCK entries inadvInvRequest, no outstanding sync block or chain-summary requests, and nosyncBlockInProcessentries. Pending TRX requests are allowed.Both initial and backup selection use
FetchBlockService.canFetchBlock(), which additionally checks that the peer is connected, both sync-direction flags are false, and the item'sadvInvReceivetimestamp is withinADV_TIME_OUT.Keep separate ranking policies:
AdvService.consumerInvToFetch()prefers smallerInvSender.getSize(peer)values. These count assignments in the current pass, not all outstanding requests.FetchBlockServiceprefers lowergetPeerTop75()values among eligible backups, without the hard P75 cutoff.Because reserving a BLOCK request makes that peer busy for subsequent block selection, sort queued BLOCK items by height before
checkAndPutAdvInvRequest(). Sorting only the outgoing message would be too late to control which block gets the peer. Skip cached, stored, or already requested blocks; leave items without an eligible peer queued. An unavailable earlier block does not prevent fetching an available later block.5. A short fetch timeout can leave retry tracking empty while INV deduplication remains
Bug and impact
AdvService.addInv()puts the item ininvToFetchCachebefore fetching. Previously,FetchBlockServiceclearedfetchBlockInfowhen the shortfetchTimeOutelapsed without an alternative, and also cleared it after sending a backup.For example:
fetchTimeOut; no backup is available, so tracking is cleared.advInvReceiveentry is recorded, butaddInv()returns because H is still ininvToFetchCache.The cache uses
blockCacheTimeoutin minutes, with a default of 60. This is not a guaranteed hour-long stall—disconnect recovery can intervene—but the short timeout no longer provides continued retry handling.Solution
Keep
fetchBlockInfowhen no alternative is currently eligible and after a backup is sent. The worker can discover later announcements through per-peeradvInvReceive, even when globaladdInv()deduplication rejects the same item.fetchBlock()registers the currentheadNum + 1request using the timestamp already in that peer'sadvInvRequest. Switching tracking does not remove the original peer's pending request or restart its deadline.At
ADV_TIME_OUT, leave final timeout enforcement toPeerStatusCheck, which examines each peer's own request timestamps and disconnects the responsible peer withTIME_OUT. Clear obsolete tracking when its block height is at or below the head. Queued BLOCK items useADV_TIME_OUT; expired queue entries release their deduplication entries so a future announcement can enqueue them again.6. Continued retry handling needs a limit on outstanding backup requests
Problem introduced by retaining retry state
Once
fetchBlockInfois retained after a backup, repeatedly applying the old selection logic could send the same block request to more advertisers on subsequent worker passes.The old
shouldFetchBlock()also returnedtruewheneveroldPeerTop75 > fetchTimeOut, even if the current request had only just been sent. Retaining retry state without changing this condition would make that repeated dispatch especially easy.Solution
Count active peers with the same BLOCK item in
advInvRequest. When the count reachesMAX_IN_FLIGHT_REQUESTS_PER_BLOCK = 2, keep retry tracking and send no further backup. If either pending peer disconnects, the released capacity can allow a replacement.shouldFetchBlock()now requires either:oldPeerSpendTime >= fetchTimeOut; ornewPeerTop75 < oldPeerLeftTime * BLOCK_FETCH_LEFT_TIME_PERCENTandoldPeerSpendTime + newPeerTop75 < fetchTimeOut.A high historical P75 alone no longer triggers dispatch. The existing early switch to a sufficiently faster peer remains available.
7. Disconnect recovery can resend work that another peer already has pending
Bug and impact
AdvService.onDisconnect()previously requeued an item whenever another peer had advertised it. It did not first check whether another peer already had that BLOCK request pending, whether the block had already arrived, or whetherfetchBlockInfostill referred to the disconnected peer.With an original request and a backup in flight, recovery could schedule another request unnecessarily or leave tracking attached to a disconnected peer.
Solution
Call
FetchBlockService.onDisconnect(peer)before examining the disconnected peer's outstanding requests. For each item:blockCacheor local storageadvInvRequestfetchBlock()reads its originaladvInvRequesttimestampadvInvReceiveentryinvToFetchand let normal scheduling check eligibilityinvToFetchCacheso a later announcement can enqueue itThis also makes the retained request from a rejected block response usable for recovery, as described in section 2.
8. Sending before registration and processing stale snapshots can restore obsolete state
Bug and impact
InvSender.sendFetch()previously sent the message before registeringfetchBlockInfo. An immediate response could complete the fetch before registration, after which the sender would register an already-completed request.The fetch worker also used a snapshot without synchronized state transitions. A worker operating on an older record could clear or replace tracking established by another thread. The original
HashSetused forsyncBlockInProcesswas also unsafe for concurrent updates and idle checks.Solution
fetchBlockInfobefore sending both initial and backup requests.volatile, makeFetchBlockInfo.peer/hash/timefinal, and synchronize registration, completion, disconnect, and worker transitions.fetchBlockInfo != fetchBlock; only clear successful tracking when the block hash matches.ConcurrentHashMap.newKeySet()forsyncBlockInProcess.AdvServicelock. Register eligible INV receipt times and confirm them under that same lock so confirmation cannot miss registration.These changes address the covered races within each service. Atomic completion across block processing and disconnect recovery remains a follow-up item.
Why are these changes required?
lastInteractiveTimeis an activity signal used by existing peer-quality logic;blockRcvTimerecords useful block delivery by the actual sender. Correctness requires keeping these meanings separate and tying request timeout decisions to the peer that still owes a response.The fixes apply to ordinary slow responses, busy peers, and concurrent processing as well as peers that advertise data they do not deliver. They provide the block-handling foundation for later connection-quality changes.
This PR has been tested by:
Local ARM64 / JDK 17 tests passed for the following areas, together with related existing message-handler, service, peer-status, and resilience regressions:
BlockContributionTestBlockInventoryActivityTestPeerBlockIdleTest,PeerConnectionTestFetchBlockRetryTestSyncBlockContributionTest./gradlew checkstyleMain checkstyleTestpassed with zero violations in the framework and plugins production/test reports.Full-repository tests, x86/JDK 8 execution, and manual network testing have not been run for this PR. Retry and timing scenarios use controlled local tests; this description does not claim a live replay of the TronSpark incident.
Follow up and scope boundaries
fetchBlockInfowhen the head advances.blockFetchSuccess()versus disconnect recovery, andblockRcvTimeupdates for concurrent duplicate responses, remain unresolved.trxRcvTime, peer scores, or persistent reputation.MessageCountconcurrency fixes are external integration prerequisites. This PR does not implement them or update dependency versions.Extra details
Targets
release_v4.8.3. Existing public method signatures, wire formats, database formats, and user configuration remain unchanged. Added methods and collections are Java 8 compatible.The design's proposed
isBlockIdle()is implemented asisBlockFetchIdle(). Initial and backup fetching share eligibility throughcanFetchBlock(), while retaining their separate batch-size and P75 ranking policies.