From 8498d0cae58995c9d8c3dd055e0483598543f036 Mon Sep 17 00:00:00 2001 From: AnkushinDaniil Date: Thu, 30 Oct 2025 18:22:46 +0400 Subject: [PATCH 1/4] feat: Add configurable EIP-2935 ring buffer size Enable chains to override the EIP-2935 historical block hash ring buffer size via chainspec configuration. Changes: - Add Eip2935RingBufferSize property to IReleaseSpec with default implementation - Update BlockhashStore to use spec.Eip2935RingBufferSize instead of hardcoded constant - Add eip2935RingBufferSize parameter to chainspec configuration system - Add Eip2935RingBufferSize property to ReleaseSpec for override capability This allows chains like Arbitrum (393,168 blocks) to use different ring buffer sizes than Ethereum mainnet (8,191 blocks) without modifying core code. --- .../Nethermind.Blockchain/Blocks/BlockhashStore.cs | 6 +++--- src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs | 6 ++++++ .../Nethermind.Specs/ChainSpecStyle/ChainParameters.cs | 1 + .../ChainSpecStyle/ChainSpecBasedSpecProvider.cs | 1 + .../Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs | 1 + .../ChainSpecStyle/Json/ChainSpecParamsJson.cs | 1 + src/Nethermind/Nethermind.Specs/ReleaseSpec.cs | 1 + 7 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs b/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs index 00d7a44cda82..ba20c83578f3 100644 --- a/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs +++ b/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs @@ -30,7 +30,7 @@ public void ApplyBlockhashStateChanges(BlockHeader blockHeader, IReleaseSpec spe if (!worldState.IsContract(eip2935Account)) return; Hash256 parentBlockHash = blockHeader.ParentHash; - var parentBlockIndex = new UInt256((ulong)((blockHeader.Number - 1) % Eip2935Constants.RingBufferSize)); + var parentBlockIndex = new UInt256((ulong)((blockHeader.Number - 1) % spec.Eip2935RingBufferSize)); StorageCell blockHashStoreCell = new(eip2935Account, parentBlockIndex); worldState.Set(blockHashStoreCell, parentBlockHash!.Bytes.WithoutLeadingZeros().ToArray()); } @@ -41,11 +41,11 @@ public void ApplyBlockhashStateChanges(BlockHeader blockHeader, IReleaseSpec spe public Hash256? GetBlockHashFromState(BlockHeader currentHeader, long requiredBlockNumber, IReleaseSpec? spec) { if (requiredBlockNumber >= currentHeader.Number || - requiredBlockNumber + Eip2935Constants.RingBufferSize < currentHeader.Number) + requiredBlockNumber + (spec?.Eip2935RingBufferSize ?? Eip2935Constants.RingBufferSize) < currentHeader.Number) { return null; } - var blockIndex = new UInt256((ulong)(requiredBlockNumber % Eip2935Constants.RingBufferSize)); + var blockIndex = new UInt256((ulong)(requiredBlockNumber % (spec?.Eip2935RingBufferSize ?? Eip2935Constants.RingBufferSize))); Address? eip2935Account = spec.Eip2935ContractAddress ?? Eip2935Constants.BlockHashHistoryAddress; StorageCell blockHashStoreCell = new(eip2935Account, blockIndex); ReadOnlySpan data = worldState.Get(blockHashStoreCell); diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 2960b6443c7f..c2b7221bde4d 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -306,6 +306,12 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec bool IsEip7709Enabled { get; } Address Eip2935ContractAddress { get; } + /// + /// EIP-2935 ring buffer size for historical block hash storage. + /// Defaults to 8,191 blocks for Ethereum mainnet. + /// + long Eip2935RingBufferSize => Eip2935Constants.RingBufferSize; + /// /// SELFDESTRUCT only in same transaction /// diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index ff14862f117f..34546fca0e45 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -129,6 +129,7 @@ public class ChainParameters public Address Eip7251ContractAddress { get; set; } public ulong? Eip2935TransitionTimestamp { get; set; } public Address Eip2935ContractAddress { get; set; } + public long Eip2935RingBufferSize { get; set; } public ulong? Eip7951TransitionTimestamp { get; set; } public ulong? Rip7212TransitionTimestamp { get; set; } public ulong? Eip7692TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index 0b4fdc3c8fa4..9ab4f76e4081 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -257,6 +257,7 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsEip2935Enabled = (chainSpec.Parameters.Eip2935TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEofEnabled = (chainSpec.Parameters.Eip7692TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.Eip2935ContractAddress = chainSpec.Parameters.Eip2935ContractAddress; + releaseSpec.Eip2935RingBufferSize = chainSpec.Parameters.Eip2935RingBufferSize; releaseSpec.IsEip7702Enabled = (chainSpec.Parameters.Eip7702TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip7823Enabled = (chainSpec.Parameters.Eip7823TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs index 7374ae2341d4..13a2f5994e38 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -159,6 +159,7 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip4788ContractAddress = chainSpecJson.Params.Eip4788ContractAddress ?? Eip4788Constants.BeaconRootsAddress, Eip2935TransitionTimestamp = chainSpecJson.Params.Eip2935TransitionTimestamp, Eip2935ContractAddress = chainSpecJson.Params.Eip2935ContractAddress ?? Eip2935Constants.BlockHashHistoryAddress, + Eip2935RingBufferSize = chainSpecJson.Params.Eip2935RingBufferSize ?? Eip2935Constants.RingBufferSize, TransactionPermissionContract = chainSpecJson.Params.TransactionPermissionContract, TransactionPermissionContractTransition = chainSpecJson.Params.TransactionPermissionContractTransition, ValidateChainIdTransition = chainSpecJson.Params.ValidateChainIdTransition, diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index 6a80e4a902e1..4611a4f6f7eb 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -148,6 +148,7 @@ public class ChainSpecParamsJson public Address Eip4788ContractAddress { get; set; } public ulong? Eip2935TransitionTimestamp { get; set; } public Address Eip2935ContractAddress { get; set; } + public long? Eip2935RingBufferSize { get; set; } public UInt256? Eip4844BlobGasPriceUpdateFraction { get; set; } public UInt256? Eip4844MinBlobGasPrice { get; set; } public ulong? Eip4844FeeCollectorTransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index ea60eb262bf4..3c0929f02995 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -166,6 +166,7 @@ public Address Eip2935ContractAddress private FrozenSet? _precompiles; FrozenSet IReleaseSpec.Precompiles => _precompiles ??= BuildPrecompilesCache(); + public long Eip2935RingBufferSize { get; set; } public virtual FrozenSet BuildPrecompilesCache() { From cff6799323db114fb5939a00afa360d57a17a39a Mon Sep 17 00:00:00 2001 From: AnkushinDaniil Date: Thu, 30 Oct 2025 19:20:57 +0400 Subject: [PATCH 2/4] fix tests --- src/Nethermind/Nethermind.Specs/ReleaseSpec.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index 3c0929f02995..9eb2c4d5f7ba 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -166,7 +166,7 @@ public Address Eip2935ContractAddress private FrozenSet? _precompiles; FrozenSet IReleaseSpec.Precompiles => _precompiles ??= BuildPrecompilesCache(); - public long Eip2935RingBufferSize { get; set; } + public long Eip2935RingBufferSize { get; set; } = Eip2935Constants.RingBufferSize; public virtual FrozenSet BuildPrecompilesCache() { From fd24d4c2a47e2eaa85e728216e8cc9f950fbea4a Mon Sep 17 00:00:00 2001 From: AnkushinDaniil Date: Thu, 30 Oct 2025 19:42:25 +0400 Subject: [PATCH 3/4] fix tests --- .../Nethermind.Specs/ChainSpecStyle/ChainParameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index 34546fca0e45..a1a02a58ea9a 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -129,7 +129,7 @@ public class ChainParameters public Address Eip7251ContractAddress { get; set; } public ulong? Eip2935TransitionTimestamp { get; set; } public Address Eip2935ContractAddress { get; set; } - public long Eip2935RingBufferSize { get; set; } + public long Eip2935RingBufferSize { get; set; } = Eip2935Constants.RingBufferSize; public ulong? Eip7951TransitionTimestamp { get; set; } public ulong? Rip7212TransitionTimestamp { get; set; } public ulong? Eip7692TransitionTimestamp { get; set; } From 84d1fe5abf6c991f5635d1740f68c9e22617901c Mon Sep 17 00:00:00 2001 From: AnkushinDaniil Date: Fri, 31 Oct 2025 16:47:38 +0400 Subject: [PATCH 4/4] address comments --- .../BlockhashProviderTests.cs | 67 +++++++++++++++++++ .../Blocks/BlockhashStore.cs | 8 +-- .../Blocks/IBlockhashStore.cs | 2 +- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Blockchain.Test/BlockhashProviderTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/BlockhashProviderTests.cs index 0a04c6bce7fb..07bd47c544a2 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/BlockhashProviderTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/BlockhashProviderTests.cs @@ -351,4 +351,71 @@ public void Eip2935_poc_trimmed_hashes() var result = store.GetBlockHashFromState(current.Header, current.Header.Number - 1); Assert.That(result, Is.EqualTo(current.Header.ParentHash)); } + + [Test, MaxTime(Timeout.MaxTestTime)] + public void BlockhashStore_uses_custom_ring_buffer_size() + { + const int customRingBufferSize = 100; + const int chainLength = 150; + + Block genesis = Build.A.Block.Genesis.TestObject; + BlockTree tree = Build.A.BlockTree(genesis).OfHeadersOnly.OfChainLength(chainLength).TestObject; + BlockHeader? head = tree.FindHeader(chainLength - 1, BlockTreeLookupOptions.None); + + (IWorldState worldState, Hash256 stateRoot) = CreateWorldState(); + Block current = Build.A.Block.WithParent(head!).WithStateRoot(stateRoot).TestObject; + tree.SuggestHeader(current.Header); + + // Custom spec with non-standard ring buffer size + ReleaseSpec customSpec = new() + { + IsEip2935Enabled = true, + IsEip7709Enabled = true, + Eip2935RingBufferSize = customRingBufferSize + }; + + var specProvider = new CustomSpecProvider( + (new ForkActivation(0, genesis.Timestamp), customSpec)); + BlockhashStore store = new(specProvider, worldState); + + using IDisposable _ = worldState.BeginScope(current.Header); + + // Insert code (account already created by CreateWorldState) + byte[] code = [1, 2, 3]; + worldState.InsertCode(Eip2935Constants.BlockHashHistoryAddress, ValueKeccak.Compute(code), code, customSpec); + + // Process current block (150) - stores block 149's hash + store.ApplyBlockhashStateChanges(current.Header); + + // Simulate processing blocks 51-149 to fill the ring buffer + // At block 150 with buffer size 100, we need hashes for blocks 50-149 + // Block 150 stores block 149's hash (already done above) + // Now process blocks 51-149 from the tree to store blocks 50-148 + for (long blockNum = chainLength - customRingBufferSize + 1; blockNum < chainLength; blockNum++) + { + BlockHeader? header = tree.FindHeader(blockNum, BlockTreeLookupOptions.None); + Assert.That(header, Is.Not.Null, $"Block {blockNum} should exist in tree"); + + store.ApplyBlockhashStateChanges(header!); + } + + // Now verify all blocks behave correctly with custom ring buffer size + // At block 150 with buffer size 100, only blocks [50, 149] should be retrievable + for (long blockNum = 1; blockNum < chainLength - customRingBufferSize; blockNum++) + { + Hash256? result = store.GetBlockHashFromState(current.Header, blockNum, customSpec); + + Assert.That(result, Is.Null, + $"Block {blockNum} should be outside custom ring buffer of size {customRingBufferSize} (proves custom size is used, not default 8191)"); + } + + for (long blockNum = chainLength - customRingBufferSize; blockNum < chainLength; blockNum++) + { + BlockHeader? expectedHeader = tree.FindHeader(blockNum, BlockTreeLookupOptions.None); + Hash256? result = store.GetBlockHashFromState(current.Header, blockNum, customSpec); + + Assert.That(result, Is.EqualTo(expectedHeader!.Hash), + $"Block {blockNum} should be retrievable within custom ring buffer of size {customRingBufferSize}"); + } + } } diff --git a/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs b/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs index ba20c83578f3..68c338bda3ce 100644 --- a/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs +++ b/src/Nethermind/Nethermind.Blockchain/Blocks/BlockhashStore.cs @@ -30,7 +30,7 @@ public void ApplyBlockhashStateChanges(BlockHeader blockHeader, IReleaseSpec spe if (!worldState.IsContract(eip2935Account)) return; Hash256 parentBlockHash = blockHeader.ParentHash; - var parentBlockIndex = new UInt256((ulong)((blockHeader.Number - 1) % spec.Eip2935RingBufferSize)); + UInt256 parentBlockIndex = new UInt256((ulong)((blockHeader.Number - 1) % spec.Eip2935RingBufferSize)); StorageCell blockHashStoreCell = new(eip2935Account, parentBlockIndex); worldState.Set(blockHashStoreCell, parentBlockHash!.Bytes.WithoutLeadingZeros().ToArray()); } @@ -38,14 +38,14 @@ public void ApplyBlockhashStateChanges(BlockHeader blockHeader, IReleaseSpec spe public Hash256? GetBlockHashFromState(BlockHeader currentHeader, long requiredBlockNumber) => GetBlockHashFromState(currentHeader, requiredBlockNumber, specProvider.GetSpec(currentHeader)); - public Hash256? GetBlockHashFromState(BlockHeader currentHeader, long requiredBlockNumber, IReleaseSpec? spec) + public Hash256? GetBlockHashFromState(BlockHeader currentHeader, long requiredBlockNumber, IReleaseSpec spec) { if (requiredBlockNumber >= currentHeader.Number || - requiredBlockNumber + (spec?.Eip2935RingBufferSize ?? Eip2935Constants.RingBufferSize) < currentHeader.Number) + requiredBlockNumber + spec.Eip2935RingBufferSize < currentHeader.Number) { return null; } - var blockIndex = new UInt256((ulong)(requiredBlockNumber % (spec?.Eip2935RingBufferSize ?? Eip2935Constants.RingBufferSize))); + UInt256 blockIndex = new UInt256((ulong)(requiredBlockNumber % spec.Eip2935RingBufferSize)); Address? eip2935Account = spec.Eip2935ContractAddress ?? Eip2935Constants.BlockHashHistoryAddress; StorageCell blockHashStoreCell = new(eip2935Account, blockIndex); ReadOnlySpan data = worldState.Get(blockHashStoreCell); diff --git a/src/Nethermind/Nethermind.Blockchain/Blocks/IBlockhashStore.cs b/src/Nethermind/Nethermind.Blockchain/Blocks/IBlockhashStore.cs index 77cc6b214f5a..0c19c64986d0 100644 --- a/src/Nethermind/Nethermind.Blockchain/Blocks/IBlockhashStore.cs +++ b/src/Nethermind/Nethermind.Blockchain/Blocks/IBlockhashStore.cs @@ -12,5 +12,5 @@ public interface IBlockhashStore public void ApplyBlockhashStateChanges(BlockHeader blockHeader); public void ApplyBlockhashStateChanges(BlockHeader blockHeader, IReleaseSpec spec); public Hash256? GetBlockHashFromState(BlockHeader currentBlockHeader, long requiredBlockNumber); - public Hash256? GetBlockHashFromState(BlockHeader currentBlockHeader, long requiredBlockNumber, IReleaseSpec? spec); + public Hash256? GetBlockHashFromState(BlockHeader currentBlockHeader, long requiredBlockNumber, IReleaseSpec spec); }