XDC - Fix subnet penalties and rewards#12080
Conversation
|
Claude finished @cicr99's task in 8m 7s —— View job PR Review: XDC - Fix subnet penalties and rewardsTasks
SummaryThis PR introduces a dedicated High — Null dereference in
|
| if (parentNumber == minBlockNumber + 1) | ||
| if (parentNumber == stopNumber + 1) | ||
| { | ||
| foreach (Address penalty in parentHeader.PenaltiesAddress) |
There was a problem hiding this comment.
High — Potential null dereference on PenaltiesAddress
PenaltiesAddress is ImmutableArray<Address>? (nullable struct). When C# evaluates foreach (T x in nullableStruct) on a null nullable value, it applies an implicit conversion from Nullable<T> → T, which throws InvalidOperationException if the value is null.
Blocks that are not gap/checkpoint blocks will have Penalties bytes as null, making PenaltiesAddress return null. If stopNumber + 1 does not correspond to a block with encoded penalties, this line will throw at runtime.
The fix from PenaltyHandler.cs (line 34) already shows the correct null-guard idiom:
foreach (Address penalty in parentHeader.PenaltiesAddress ?? [])
{
penalties.Add(penalty);
}There was a problem hiding this comment.
Maybe better throw in this case?
We are expecting the field to be set when this is called.
| @@ -87,9 +87,6 @@ protected override void Load(ContainerBuilder builder) | |||
| // penalty handler | |||
| .AddSingleton<IPenaltyHandler, PenaltyHandler>() | |||
There was a problem hiding this comment.
Medium — Duplicate IPenaltyHandler registration
IPenaltyHandler is registered as PenaltyHandler twice: here at line 88 and again at line 108. In Autofac the last registration wins, so behaviour is correct at runtime, but the first registration is dead code and creates confusion about intent. One of the two should be removed.
| transactionProcessor, | ||
| rewardsStore) | ||
| { | ||
| protected internal override HashSet<Address> GetRewardMasternodes(XdcBlockHeader checkpointHeader, IXdcReleaseSpec spec) => |
There was a problem hiding this comment.
Medium — Silent empty-set on null validators
When ValidatorsAddress is null, this returns [], which means the entire epoch reward is silently zeroed. The base class implementation at the same override point throws or falls back to V1 extra-data parsing. For the subnet, an empty validator set is almost certainly a bug (corrupt header, missing decode, unexpected state). Consider throwing here — or at minimum logging a warning — so the issue surfaces during debugging rather than producing a chain with no rewards and no diagnostics.
protected internal override HashSet<Address> GetRewardMasternodes(XdcBlockHeader checkpointHeader, IXdcReleaseSpec spec) =>
checkpointHeader.ValidatorsAddress is { } validators
? [.. validators]
: throw new InvalidOperationException(
$"ValidatorsAddress is null on subnet checkpoint header {checkpointHeader.Number} ({checkpointHeader.Hash})");| blockTree.FindHeader(Arg.Any<Hash256>(), Arg.Any<long>()) | ||
| blockTree.FindHeader(Arg.Any<Hash256>(), Arg.Any<BlockTreeLookupOptions>(), Arg.Any<long>()) | ||
| .Returns(ci => hashToHeader.TryGetValue(ci.ArgAt<Hash256>(0), out XdcSubnetBlockHeader? h) ? h : null); | ||
| blockTree.FindBlock(Arg.Any<Hash256>(), Arg.Any<long>()) |
There was a problem hiding this comment.
Medium — Mock intercepts the wrong FindBlock overload
SigningTxCache.GetSigningTransactions calls _blockTree.FindBlock(blockHash, SigningTxLookupOptions, blockNumber) — the three-parameter overload Block? FindBlock(Hash256, BlockTreeLookupOptions, long?).
The mock configured here matches Block? FindBlock(Hash256, long?) (two parameters), which is a separate interface method. NSubstitute resolves by exact method signature, so the 3-parameter call made by SigningTxCache falls through to NSubstitute's default (returns null), causing SigningTxCache to throw InvalidOperationException("Expected block … to exist in block tree").
This means tests like TestSigningTxRemovesPenalty are not correctly exercising the code path they describe. Fix by also mocking the three-parameter overload:
blockTree.FindBlock(Arg.Any<Hash256>(), Arg.Any<BlockTreeLookupOptions>(), Arg.Any<long?>())
.Returns(ci => hashToBlock.TryGetValue(ci.ArgAt<Hash256>(0), out Block? block) ? block : null);| if (parentNumber == minBlockNumber + 1) | ||
| if (parentNumber == stopNumber + 1) | ||
| { | ||
| foreach (Address penalty in parentHeader.PenaltiesAddress) |
There was a problem hiding this comment.
Maybe better throw in this case?
We are expecting the field to be set when this is called.
|
|
||
| public void SetSigningTransactions(Hash256 blockHash, Transaction[] transactions) => _signingTxsCache.Set(blockHash, transactions); | ||
|
|
||
| public bool TryGetHeader(Hash256 blockHash, out XdcBlockHeader? header) => _headersCache.TryGet(blockHash, out header); |
There was a problem hiding this comment.
what is this for ?
Dont think you need this or the header cache.
| StoreSnapshotNumber(snapshot); | ||
| } | ||
|
|
||
| private TSnapshot? GetSnapshotByStoredGapNumber(long gapNumber) |
There was a problem hiding this comment.
What is the reason exactly for this method?
When fetching from BlockTree we have a certain guarantee that we are looking at the canonical header.
What will happen during a reorg? Can we end up looking at the wrong header?
No description provided.