XDC Fix signing old blocks#12085
Conversation
|
Claude finished @batrr's task in 4m 54s —— View job Review
The fix is correct and well-scoped. The core logic — skip signing if No Critical / High / Medium findings. Three Low notes posted as inline comments:
All three are nits — none block merge. |
| public const int SignTransactionDataLength = 68; | ||
|
|
||
| // Skip stale blocks during sync. | ||
| public const int MaxSignableBlockPeriods = 2; |
There was a problem hiding this comment.
Low — the comment says "during sync" but the IsSyncing() check on line 70 of SignTransactionManager already handles in-progress sync. This constant guards the subtler case: a node that was offline for a while restarts, IsSyncing() settles to false, but BlockAddedToMain fires for the trailing backlog of historical blocks. Suggest:
| public const int MaxSignableBlockPeriods = 2; | |
| // Guard against signing stale blocks replayed on restart (IsSyncing() handles active sync separately). | |
| public const int MaxSignableBlockPeriods = 2; |
|
|
||
| // Sign only live head blocks; stale ones are replayed during catch-up sync. | ||
| long window = spec.MergeSignRange * spec.MinePeriod * XdcConstants.MaxSignableBlockPeriods; | ||
| if (window > 0 && (long)xdcHeader.Timestamp + window < _timestamper.UnixTime.SecondsLong) |
There was a problem hiding this comment.
Low — the window > 0 guard is correct and load-bearing: without it, window == 0 would make timestamp + 0 < now true for every past block, silently disabling signing entirely when MinePeriod or MergeSignRange is zero. Worth adding a brief comment to document this intent so a future reader doesn't simplify it away:
| if (window > 0 && (long)xdcHeader.Timestamp + window < _timestamper.UnixTime.SecondsLong) | |
| if (window > 0 && (long)xdcHeader.Timestamp + window < _timestamper.UnixTime.SecondsLong) // window==0 disables the guard |
|
|
||
| IBlockTree blockTree = Substitute.For<IBlockTree>(); | ||
| blockTree.WasProcessed(block.Number, block.Hash!).Returns(true); | ||
| blockTree.FindBestSuggestedHeader().Returns(header); // bestSuggested == head => not syncing |
There was a problem hiding this comment.
Low — since blockTree is an NSubstitute mock, IsSyncing() returns default (i.e. isSyncing: false) without any explicit setup, so the FindBestSuggestedHeader().Returns(header) call doesn't actually influence the test outcome. The comment // bestSuggested == head => not syncing implies a behavioral dependency that doesn't exist for a mock. The line could be removed, or if you want to document the intent, add a comment explaining IsSyncing() returns false by default on a mock.
|
@claude re review |
|
Claude finished @batrr's task in 4m 14s —— View job Re-review
The fix is correct and the approach is sound. The timestamp guard correctly identifies stale catch-up blocks, the window formula ( Correcting a prior review error: my previous comment on No Critical / High / Medium findings.
Mergeable — both findings are nits; neither blocks merge. |
| return; | ||
|
|
||
| // Sign only recent head blocks; older ones are replayed during catch-up. | ||
| long window = spec.MergeSignRange * spec.MinePeriod * XdcConstants.MaxSignableBlockPeriods; |
There was a problem hiding this comment.
Low — window == 0 is a silent failure mode: if either MergeSignRange or MinePeriod is ever 0, timestamp + 0 < now is true for every past block and signing is silently disabled. Both are always positive on XDC mainnet so this is not a production risk, but worth a brief guard or a comment to protect against future misconfiguration:
| long window = spec.MergeSignRange * spec.MinePeriod * XdcConstants.MaxSignableBlockPeriods; | |
| // Sign only recent head blocks; older ones are replayed during catch-up. | |
| // window > 0 guard: if MinePeriod or MergeSignRange is 0, skip would match everything. | |
| long window = spec.MergeSignRange * spec.MinePeriod * XdcConstants.MaxSignableBlockPeriods; | |
| if (window > 0 && (long)xdcHeader.Timestamp + window < _timestamper.UnixTime.SecondsLong) | |
| return; |
|
|
||
| IBlockTree blockTree = Substitute.For<IBlockTree>(); | ||
| blockTree.WasProcessed(block.Number, block.Hash!).Returns(true); | ||
| blockTree.FindBestSuggestedHeader().Returns(header); // bestSuggested == head => not syncing |
There was a problem hiding this comment.
Correction to previous review — the previous comment called this a no-op, but it is load-bearing. IBlockTree.IsSyncing() is a static extension method that calls blockTree.FindBestSuggestedHeader()?.Number ?? 0. Without this stub, the mock returns null, giving bestSuggestedNumber = 0, and isSyncing = (0 == 0) = true — all shouldSign = true cases would fail. The comment here is accurate; the stub should stay.
| return; | ||
|
|
||
| // Sign only recent head blocks; older ones are replayed during catch-up. | ||
| long window = spec.MergeSignRange * spec.MinePeriod * XdcConstants.MaxSignableBlockPeriods; |
There was a problem hiding this comment.
Think we can probably extend the window here. All headers in current epoch should be signable.
Changes
XDC masternodes send a "sign" tx every
MergeSignRange(15) blocks. This is triggered bySignTransactionManager.OnBlockAddedToMain, which runs for every block added to the chain — including old blocks processed while the node iscatching up. So a node that was offline for a while would send sign txs for lots of old blocks when it syncs back.
XDC Go client doesn't have this problem: only the head path signs, and catch-up runs through the downloader, which never signs. We could check "are we behind the peers' head?" using the sync layer, but pulling
that into the XDC sign logic isn't worth it.
So we check the block's age instead: only sign if the block's timestamp is within
MergeSignRange * MinePeriod * 2seconds of now, about the last 2 signing rounds (~30 blocks / ~1 minute on XDC).Types of changes
Testing
Requires testing
If yes, did you write tests?