Unify types with Geth#11937
Conversation
…g arithmetic' migration reference
…are against typed expected values
- ChainSpecBasedSpecProvider: drop verbose BlockOf comment block - HistoryPruner: drop misplaced field-level sentinel comment - Merkleizer: (ulong)1 << k -> 1UL << k - Test files: drop (ulong) casts where suffix or local const suffices
errorMargin / BasisPointsDivisor + 1 collapsed to integer 1 after the ulong swap, losing the basis-point safety head-room and pushing eth_estimateGas to exact minimal gas.
Math.Clamp throws when min > max, which can occur on a pruned node with an ancient receipts barrier higher than BestKnown - MaxReorgDepth. The master form silently returned max in that window.
Encoder still writes Value/GasPrice/MaxFeePerGas at full UInt256 width, so DecodeULong() would fail to round-trip any blob tx with these fields >= 2^64 wei from the persistent blob pool.
Encoder writes Value at full UInt256 width; DecodeULong() would fail to round-trip OP deposit txs with value >= 2^64 wei. Mirrors the adjacent Mint field which correctly stays DecodeUInt256().
The PR changed the 1559 branch from MaxFeePerGas to GasPrice, but for type-2 txs GasPrice is MaxPriorityFeePerGas. A tx with positive fee cap and zero priority would now be wrongly classified as zero-gas.
- FeeHistoryOracle percentile threshold: restore explicit (ulong) floor so reward selection matches Geth instead of advancing one extra step at fractional thresholds (RPC-visible). - FeeHistoryOracle ShouldCache, HeadersSyncFeed prioritization, and VirtualMachine CREATE-revert reportedGas: use SaturatingSub for ulong subtractions that could underflow when the left operand is smaller.
Interlocked.CompareExchange(ref f, f, f) reads f twice; a concurrent mutation between the two reads makes the CAS write the stale earlier value back, rolling the monotonic round guard back. Interlocked.Read has had a ulong overload since .NET 5.
(ulong) cast on a UInt256 estimate then assigning back into UInt256 fields truncates mod 2^64. The estimate stays UInt256 end-to-end.
(decimal)(CurrentValue - StartValue) does the subtraction in ulong *before* the cast (precedence), so non-monotonic progress (deep-reorg during full sync, BlockDownloader steps back) wraps to ~1.8e19 instead of going negative as long did.
_minBlock starts at ulong.MaxValue; _minBlock + MaxDepth * 4 wraps to a small value, so ShouldPrune returns true for any block past 1023 even when the cache is empty. Also migrate Clear()'s leftover int.MaxValue reset to ulong.MaxValue.
The prev-block sentinel migrated long.MinValue -> 0 collided with genesis: a genuine previous-block-0 was treated as 'no previous' and its LatestKey-indexed entry was not removed.
The PR inlined the refund-cap math (in long to preserve a possibly- negative totalToRefund) so the virtual is no longer called and has no overrides. Delete the orphaned hook.
The 'checked' block would throw a bare OverflowException on a hint below 32 MB; restore the targeted 'memory hint is not enough' message that long-arithmetic emitted via AssignPeersMemory.
TrieStore.pruningBoundaryBlock and EthRpcModule.eth_getBlockAccessListByNumber checked '>= 0' on ulong values; the else branches are unreachable.
The PR re-expressed the cap inside ValidateGasLimitRange, which is skipped for genesis, orphaned headers, AuRa contract-gas-limit blocks, XDC block 1, and OP/Taiko subclass overrides — a regression vs the master ValidateFieldLimit that ran on every header. Extract the cap into ValidateAbsoluteGasLimit and call it on the unconditional path.
Keep the user-facing validation for eth_call/eth_simulateV1 blockOverrides.gasLimit; matches HeaderValidator's 2^63-1 protocol cap.
inMemoryStateDepth - _compactSize underflows shortly after a persist (depth < compactSize) and bypasses the keep-in-memory early-return. Pre-PR signed math went negative and triggered the return; use SaturatingSub to restore that semantic.
# Conflicts: # src/Nethermind/Nethermind.Trie/Pruning/CompositePersistenceStrategy.cs
|
@claude review |
|
Claude encountered an error after 11m 23s —— View job PR Review: Unify types with Geth
|
| @@ -179,21 +179,20 @@ private void EnsureIndexLoaded() | |||
|
|
|||
| _componentIndexTlvStart = indexEntryStart; | |||
|
|
|||
| // Read starting block number (first field of index data) | |||
| _startBlock = ReadInt64(indexEntryStart + EntryHeaderSize); | |||
| _startBlock = ReadUInt64(indexEntryStart + EntryHeaderSize); | |||
There was a problem hiding this comment.
Medium: No validation for negative starting_number on disk
The ComponentIndex format defines starting_number as an int64 little-endian field (see the // sizeof(long) comment on IndexFieldSize). Reading it unconditionally as ulong turns a malformed/corrupted archive with a negative value (e.g. starting_number = -1) into ulong.MaxValue. Every subsequent ComponentOffset call then throws ArgumentOutOfRangeException with a confusing range message instead of EraFormatException.
| _startBlock = ReadUInt64(indexEntryStart + EntryHeaderSize); | |
| long startBlockSigned = ReadInt64(indexEntryStart + EntryHeaderSize); | |
| if (startBlockSigned < 0) | |
| throw new EraFormatException($"Invalid starting block number {startBlockSigned} in EraE ComponentIndex."); | |
| _startBlock = (ulong)startBlockSigned; |
(This was flagged by @flcl42 in the 2026-06-15 review and has not been addressed.)
| @@ -66,7 +66,7 @@ public sealed class TrieStore : ITrieStore, IPruningTrieStore | |||
| private readonly bool _pastKeyTrackingEnabled = false; | |||
|
|
|||
| private bool _lastPersistedReachedReorgBoundary; | |||
| private long _toBePersistedBlockNumber = -1; | |||
| private ulong _toBePersistedBlockNumber = ulong.MinValue; | |||
There was a problem hiding this comment.
Medium: ulong.MinValue (0) is a valid block number (genesis), not a safe sentinel
ulong.MinValue == 0, which is genesis block number. When pruning fires at a node that persisted genesis (LastPersistedBlockNumber == 0), line 614 sets _toBePersistedBlockNumber = 0. Then every concurrent BeginScope call on line 350 sees persistedBoundary == ulong.MinValue and spin-waits indefinitely — it interprets genesis as "not yet determined".
The TrieStoreDirtyNodesCache already uses ulong.MaxValue as its "not set" sentinel (NoCommitSentinel). Use the same sentinel here:
| private ulong _toBePersistedBlockNumber = ulong.MinValue; | |
| private ulong _toBePersistedBlockNumber = ulong.MaxValue; |
And update the check on line 350 and all resets (lines 642, 684) from ulong.MinValue → ulong.MaxValue.
- CompositePersistenceStrategyTests: long blockNumber -> ulong to match the post-merge ShouldPersist(ulong) signature. - MemoryHintManTests: drop now-unused Core.Extensions using directive (left behind after the (ulong)256.MB -> 256 * MB cleanup).
# Conflicts: # src/Nethermind/Nethermind.Xdc/SubnetPenaltyHandler.cs

Closes #9685
Changes
ulong.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?