diff --git a/cspell.json b/cspell.json index 435e544e1db..b2ed9089189 100644 --- a/cspell.json +++ b/cspell.json @@ -53,6 +53,7 @@ "auxdata", "backpressure", "badreq", + "bals", "barebone", "baseblock", "basefee", diff --git a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs index 041247ee523..dbf3dbfc385 100644 --- a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs +++ b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs @@ -89,7 +89,7 @@ protected async Task RunTest(BlockchainTest test, Stopwatch? List<(ForkActivation Activation, IReleaseSpec Spec)> transitions = isEngineTest ? [((ForkActivation)0, test.Network)] : - [((ForkActivation)0, test.GenesisSpec), ((ForkActivation)1, test.Network)]; // TODO: this thing took a lot of time to find after it was removed!, genesis block is always initialized with Frontier + [((ForkActivation)0, test.GenesisSpec), ((ForkActivation)1, test.Network)]; // genesis block is always initialized with Frontier if (test.NetworkAfterTransition is not null) { @@ -312,7 +312,7 @@ private static BlockHeader SuggestBlocks(BlockchainTest test, bool failOnInvalid private async static Task RunNewPayloads(TestEngineNewPayloadsJson[]? newPayloads, IEngineRpcModule engineRpcModule) { - (ExecutionPayloadV3, string[]?, string[]?, int, int)[] payloads = [.. JsonToEthereumTest.Convert(newPayloads)]; + (ExecutionPayloadV4, string[]?, string[]?, int, int)[] payloads = [.. JsonToEthereumTest.Convert(newPayloads)]; // blockchain test engine foreach ((ExecutionPayload executionPayload, string[]? blobVersionedHashes, string[]? validationError, int newPayloadVersion, int fcuVersion) in payloads) diff --git a/src/Nethermind/Ethereum.Test.Base/BlockchainTestJson.cs b/src/Nethermind/Ethereum.Test.Base/BlockchainTestJson.cs index c344cd773da..2727692ee16 100644 --- a/src/Nethermind/Ethereum.Test.Base/BlockchainTestJson.cs +++ b/src/Nethermind/Ethereum.Test.Base/BlockchainTestJson.cs @@ -20,6 +20,7 @@ public class BlockchainTestJson public IReleaseSpec? EthereumNetworkAfterTransition { get; set; } public ForkActivation? TransitionForkActivation { get; set; } public string? LastBlockHash { get; set; } + public ConfigJson? Config { get; set; } public string? GenesisRlp { get; set; } public TestBlockJson[]? Blocks { get; set; } @@ -34,4 +35,18 @@ public class BlockchainTestJson public string? SealEngine { get; set; } public string? LoadFailure { get; set; } } + + public class ConfigJson + { + public string? Network { get; set; } + public string? Chainid { get; set; } + public Dictionary? BlobSchedule; + } + + public class BlobScheduleEntryJson + { + public string? Target; + public string? Max; + public string? BaseFeeUpdateFraction; + } } diff --git a/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs b/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs index 0eda74a3459..e5117faa25c 100644 --- a/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs +++ b/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs @@ -19,6 +19,7 @@ using Nethermind.Serialization.Json; using Nethermind.Serialization.Rlp; using Nethermind.Specs; +using Nethermind.Specs.Test; namespace Ethereum.Test.Base { @@ -74,7 +75,7 @@ public static BlockHeader Convert(TestBlockHeaderJson? headerJson) StateRoot = new Hash256(headerJson.StateRoot), TxRoot = new Hash256(headerJson.TransactionsTrie), WithdrawalsRoot = headerJson.WithdrawalsRoot is null ? null : new Hash256(headerJson.WithdrawalsRoot), - // BlockAccessListHash = headerJson.BlockAccessListHash is null ? null : new Hash256(headerJson.BlockAccessListHash), + BlockAccessListHash = headerJson.BlockAccessListHash is null ? null : new Hash256(headerJson.BlockAccessListHash), }; if (headerJson.BaseFeePerGas is not null) @@ -85,7 +86,7 @@ public static BlockHeader Convert(TestBlockHeaderJson? headerJson) return header; } - public static IEnumerable<(ExecutionPayloadV3, string[]?, string[]?, int, int)> Convert(TestEngineNewPayloadsJson[]? executionPayloadsJson) + public static IEnumerable<(ExecutionPayloadV4, string[]?, string[]?, int, int)> Convert(TestEngineNewPayloadsJson[]? executionPayloadsJson) { if (executionPayloadsJson is null) { @@ -98,7 +99,7 @@ public static BlockHeader Convert(TestBlockHeaderJson? headerJson) string[]? blobVersionedHashes = engineNewPayload.Params.Length > 1 ? engineNewPayload.Params[1].Deserialize(EthereumJsonSerializer.JsonOptions) : null; string? parentBeaconBlockRoot = engineNewPayload.Params.Length > 2 ? engineNewPayload.Params[2].Deserialize(EthereumJsonSerializer.JsonOptions) : null; string[]? validationError = engineNewPayload.Params.Length > 3 ? engineNewPayload.Params[3].Deserialize(EthereumJsonSerializer.JsonOptions) : null; - yield return (new ExecutionPayloadV3() + yield return (new ExecutionPayloadV4() { BaseFeePerGas = (ulong)Bytes.FromHexString(executionPayload.BaseFeePerGas).ToUnsignedBigInteger(), BlockHash = new(executionPayload.BlockHash), @@ -113,7 +114,7 @@ public static BlockHeader Convert(TestBlockHeaderJson? headerJson) ReceiptsRoot = new(executionPayload.ReceiptsRoot), StateRoot = new(executionPayload.StateRoot), Timestamp = (ulong)Bytes.FromHexString(executionPayload.Timestamp).ToUnsignedBigInteger(), - // BlockAccessList = executionPayload.BlockAccessList is null ? null : Bytes.FromHexString(executionPayload.BlockAccessList), + BlockAccessList = executionPayload.BlockAccessList is null ? null : Bytes.FromHexString(executionPayload.BlockAccessList), BlobGasUsed = executionPayload.BlobGasUsed is null ? null : (ulong)Bytes.FromHexString(executionPayload.BlobGasUsed).ToUnsignedBigInteger(), ExcessBlobGas = executionPayload.ExcessBlobGas is null ? null : (ulong)Bytes.FromHexString(executionPayload.ExcessBlobGas).ToUnsignedBigInteger(), ParentBeaconBlockRoot = parentBeaconBlockRoot is null ? null : new(parentBeaconBlockRoot), @@ -432,11 +433,11 @@ public static IEnumerable ConvertToBlockchainTests(string json) string[] transitionInfo = testSpec.Network.Split("At"); string[] networks = transitionInfo[0].Split("To"); - testSpec.EthereumNetwork = SpecNameParser.Parse(networks[0]); + testSpec.EthereumNetwork = LoadSpec(networks[0], testSpec.Config?.BlobSchedule); if (transitionInfo.Length > 1) { testSpec.TransitionForkActivation = TransitionForkActivation(transitionInfo[1]); - testSpec.EthereumNetworkAfterTransition = SpecNameParser.Parse(networks[1]); + testSpec.EthereumNetworkAfterTransition = LoadSpec(networks[1], testSpec.Config?.BlobSchedule); } (string name, string category) = GetNameAndCategory(testName); @@ -446,6 +447,22 @@ public static IEnumerable ConvertToBlockchainTests(string json) return testsByName; } + private static IReleaseSpec LoadSpec(string name, Dictionary? blobSchedule) + { + IReleaseSpec spec = SpecNameParser.Parse(name); + if (blobSchedule is null || !blobSchedule.TryGetValue(name, out BlobScheduleEntryJson? blobCount)) + { + return spec; + } + + return new OverridableReleaseSpec(spec) + { + MaxBlobCount = System.Convert.ToUInt64(blobCount.Max, 16), + TargetBlobCount = System.Convert.ToUInt64(blobCount.Target, 16), + BlobBaseFeeUpdateFraction = System.Convert.ToUInt64(blobCount.BaseFeeUpdateFraction, 16) + }; + } + private static (string name, string category) GetNameAndCategory(string key) { key = key.Replace('\\', '/'); diff --git a/src/Nethermind/Nethermind.Blockchain.Test/PrecompileCachedCodeInfoRepositoryTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/PrecompileCachedCodeInfoRepositoryTests.cs index ce42e5e71dc..3fa16f29b7a 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/PrecompileCachedCodeInfoRepositoryTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/PrecompileCachedCodeInfoRepositoryTests.cs @@ -12,6 +12,7 @@ using Nethermind.Evm; using Nethermind.Evm.CodeAnalysis; using Nethermind.Evm.Precompiles; +using Nethermind.Evm.State; using Nethermind.Specs.Forks; using Nethermind.State; using NSubstitute; diff --git a/src/Nethermind/Nethermind.Blockchain.Test/Validators/BlockValidatorTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/Validators/BlockValidatorTests.cs index d74a57ae345..a8349a9f4e2 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/Validators/BlockValidatorTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/Validators/BlockValidatorTests.cs @@ -15,6 +15,8 @@ using NUnit.Framework; using System.Collections.Generic; using FluentAssertions; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Serialization.Rlp; using Nethermind.Core.Test; namespace Nethermind.Blockchain.Test.Validators; @@ -252,7 +254,8 @@ private static IEnumerable BadSuggestedBlocks() .TestObject, parent, Substitute.For(), - "InvalidUnclesHash"); + "InvalidUnclesHash") + { TestName = "InvalidUnclesHash" }; yield return new TestCaseData( Build.A.Block @@ -260,7 +263,8 @@ private static IEnumerable BadSuggestedBlocks() .TestObject, parent, Substitute.For(), - "InvalidTxRoot"); + "InvalidTxRoot") + { TestName = "InvalidTxRoot" }; yield return new TestCaseData( Build.A.Block.WithBlobGasUsed(131072) @@ -275,13 +279,50 @@ private static IEnumerable BadSuggestedBlocks() .TestObject, parent, new CustomSpecProvider(((ForkActivation)0, Cancun.Instance)), - "InsufficientMaxFeePerBlobGas"); + "InsufficientMaxFeePerBlobGas") + { TestName = "InsufficientMaxFeePerBlobGas" }; yield return new TestCaseData( Build.A.Block.WithParent(parent).WithEncodedSize(Eip7934Constants.DefaultMaxRlpBlockSize + 1).TestObject, parent, new CustomSpecProvider(((ForkActivation)0, Osaka.Instance)), - "ExceededBlockSizeLimit"); + "ExceededBlockSizeLimit") + { TestName = "ExceededBlockSizeLimit" }; + + yield return new TestCaseData( + Build.A.Block + .WithParent(parent) + .WithBlobGasUsed(0) + .WithWithdrawals([]) + .WithBlockAccessList(new()) + .WithEncodedBlockAccessList(Rlp.Encode(new BlockAccessList()).Bytes).TestObject, + parent, + new CustomSpecProvider(((ForkActivation)0, Amsterdam.Instance)), + "InvalidBlockLevelAccessListHash") + { TestName = "InvalidBlockLevelAccessListHash" }; + + yield return new TestCaseData( + Build.A.Block + .WithParent(parent) + .WithBlobGasUsed(0) + .WithWithdrawals([]) + .WithBlockAccessList(new()) + .WithEncodedBlockAccessList([0xfa]).TestObject, + parent, + new CustomSpecProvider(((ForkActivation)0, Amsterdam.Instance)), + "InvalidBlockLevelAccessList") + { TestName = "InvalidBlockLevelAccessList" }; + + yield return new TestCaseData( + Build.A.Block + .WithParent(parent) + .WithBlobGasUsed(0) + .WithWithdrawals([]) + .WithBlockAccessList(new()).TestObject, + parent, + new CustomSpecProvider(((ForkActivation)0, Osaka.Instance)), + "BlockLevelAccessListNotEnabled") + { TestName = "BlockLevelAccessListNotEnabled" }; } [TestCaseSource(nameof(BadSuggestedBlocks))] diff --git a/src/Nethermind/Nethermind.Blockchain/BlockAccessLists/BlockAccessListStore.cs b/src/Nethermind/Nethermind.Blockchain/BlockAccessLists/BlockAccessListStore.cs new file mode 100644 index 00000000000..f69dd6183ca --- /dev/null +++ b/src/Nethermind/Nethermind.Blockchain/BlockAccessLists/BlockAccessListStore.cs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac.Features.AttributeFilters; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Crypto; +using Nethermind.Db; +using Nethermind.Serialization.Rlp; +using Nethermind.Serialization.Rlp.Eip7928; +using System; + +namespace Nethermind.Blockchain.Headers; + +public class BlockAccessListStore( + [KeyFilter(DbNames.BlockAccessLists)] IDb balDb, + BlockAccessListDecoder? decoder = null) + : IBlockAccessListStore +{ + private readonly BlockAccessListDecoder _balDecoder = decoder ?? new(); + + public void Insert(Hash256 blockHash, BlockAccessList bal) + { + using NettyRlpStream rlpStream = BlockAccessListDecoder.Instance.EncodeToNewNettyStream(bal); + balDb.Set(blockHash, rlpStream.AsSpan()); + } + + public void Insert(Hash256 blockHash, byte[] encodedBal) + => balDb.Set(blockHash, encodedBal); + + public byte[]? GetRlp(Hash256 blockHash) + => balDb.Get(blockHash); + + public BlockAccessList? Get(Hash256 blockHash) + { + ReadOnlySpan rlp = balDb.GetSpan(blockHash); + return rlp.IsEmpty ? null : _balDecoder.Decode(rlp); + } + + public void Delete(Hash256 blockHash) + => balDb.Delete(blockHash); +} diff --git a/src/Nethermind/Nethermind.Blockchain/BlockAccessLists/IBlockAccessListStore.cs b/src/Nethermind/Nethermind.Blockchain/BlockAccessLists/IBlockAccessListStore.cs new file mode 100644 index 00000000000..857f7dbaf32 --- /dev/null +++ b/src/Nethermind/Nethermind.Blockchain/BlockAccessLists/IBlockAccessListStore.cs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Crypto; + +namespace Nethermind.Blockchain.Headers; + +public interface IBlockAccessListStore +{ + void InsertFromBlock(Block block) + { + if (block.EncodedBlockAccessList is not null) + { + Insert(block.Hash, block.EncodedBlockAccessList); + } + else if (block.BlockAccessList is not null) + { + Insert(block.Hash, block.BlockAccessList); + } + } + + void Insert(Hash256 blockHash, byte[] bal); + void Insert(Hash256 blockHash, BlockAccessList bal); + byte[]? GetRlp(Hash256 blockHash); + BlockAccessList? Get(Hash256 blockHash); + void Delete(Hash256 blockHash); +} diff --git a/src/Nethermind/Nethermind.Blockchain/BlockTree.cs b/src/Nethermind/Nethermind.Blockchain/BlockTree.cs index 30024036510..c273abf46db 100644 --- a/src/Nethermind/Nethermind.Blockchain/BlockTree.cs +++ b/src/Nethermind/Nethermind.Blockchain/BlockTree.cs @@ -44,6 +44,7 @@ public partial class BlockTree : IBlockTree private readonly IDb _blockInfoDb; private readonly IDb _metadataDb; private readonly IBadBlockStore _badBlockStore; + private readonly IBlockAccessListStore _balStore; private readonly LruCache _invalidBlocks = new(128, 128, "invalid blocks"); @@ -113,6 +114,7 @@ public BlockTree( [KeyFilter(DbNames.BlockInfos)] IDb? blockInfoDb, [KeyFilter(DbNames.Metadata)] IDb? metadataDb, IBadBlockStore? badBlockStore, + IBlockAccessListStore? balStore, IChainLevelInfoRepository? chainLevelInfoRepository, ISpecProvider? specProvider, IBloomStorage? bloomStorage, @@ -126,6 +128,7 @@ public BlockTree( _blockInfoDb = blockInfoDb ?? throw new ArgumentNullException(nameof(blockInfoDb)); _metadataDb = metadataDb ?? throw new ArgumentNullException(nameof(metadataDb)); _badBlockStore = badBlockStore ?? throw new ArgumentNullException(nameof(badBlockStore)); + _balStore = balStore ?? throw new ArgumentNullException(nameof(balStore)); SpecProvider = specProvider ?? throw new ArgumentNullException(nameof(specProvider)); _bloomStorage = bloomStorage ?? throw new ArgumentNullException(nameof(bloomStorage)); _syncConfig = syncConfig ?? throw new ArgumentNullException(nameof(syncConfig)); @@ -416,6 +419,7 @@ public AddBlockResult Insert(Block block, BlockTreeInsertBlockOptions insertBloc _blockStore.Insert(block, writeFlags: blockWriteFlags); _headerStore.InsertBlockNumber(block.Hash, block.Number); + _balStore.InsertFromBlock(block); bool saveHeader = (insertBlockOptions & BlockTreeInsertBlockOptions.SaveHeader) != 0; if (saveHeader) @@ -483,6 +487,7 @@ protected virtual AddBlockResult Suggest(Block? block, BlockHeader header, Block } _blockStore.Insert(block); + _balStore.InsertFromBlock(block); } if (!isKnown) @@ -1679,6 +1684,7 @@ public int DeleteChainSlice(in long startNumber, long? endNumber = null, bool fo _blockInfoDb.Delete(blockHash); _blockStore.Delete(i, blockHash); _headerStore.Delete(blockHash); + _balStore.Delete(blockHash); } } } diff --git a/src/Nethermind/Nethermind.Blockchain/PrecompileCachedCodeInfoRepository.cs b/src/Nethermind/Nethermind.Blockchain/PrecompileCachedCodeInfoRepository.cs index 6d1b7e0eb6c..24faeb1ac00 100644 --- a/src/Nethermind/Nethermind.Blockchain/PrecompileCachedCodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.Blockchain/PrecompileCachedCodeInfoRepository.cs @@ -12,6 +12,7 @@ using Nethermind.Evm; using Nethermind.Evm.CodeAnalysis; using Nethermind.Evm.Precompiles; +using Nethermind.Evm.State; using Nethermind.State; namespace Nethermind.Blockchain; diff --git a/src/Nethermind/Nethermind.Consensus/EngineApiVersions.cs b/src/Nethermind/Nethermind.Consensus/EngineApiVersions.cs index 145810a7364..d3c7cde9be7 100644 --- a/src/Nethermind/Nethermind.Consensus/EngineApiVersions.cs +++ b/src/Nethermind/Nethermind.Consensus/EngineApiVersions.cs @@ -9,4 +9,6 @@ public static class EngineApiVersions public const int Shanghai = 2; public const int Cancun = 3; public const int Prague = 4; + public const int Osaka = 5; + public const int Amsterdam = 6; } diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockProductionTransactionsExecutor.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockProductionTransactionsExecutor.cs index 8071dc34907..2521ff7f426 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockProductionTransactionsExecutor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockProductionTransactionsExecutor.cs @@ -30,6 +30,7 @@ public class BlockProductionTransactionsExecutor( ILogManager logManager) : IBlockProductionTransactionsExecutor { + private readonly IBlockAccessListBuilder? _balBuilder = stateProvider as IBlockAccessListBuilder; private readonly ILogger _logger = logManager.GetClassLogger(); protected EventHandler? _transactionProcessed; @@ -77,6 +78,7 @@ public virtual TxReceipt[] ProcessTransactions(Block block, ProcessingOptions pr } } } + _balBuilder?.GeneratedBlockAccessList.IncrementBlockAccessIndex(); block.Header.TxRoot = TxTrie.CalculateRoot(includedTx.AsSpan()); if (blockToProduce is not null) @@ -102,6 +104,7 @@ private TxAction ProcessTransaction( } else { + _balBuilder?.GeneratedBlockAccessList.IncrementBlockAccessIndex(); TransactionResult result = transactionProcessor.ProcessTransaction(currentTx, receiptsTracer, processingOptions, stateProvider); if (result) @@ -111,6 +114,7 @@ private TxAction ProcessTransaction( } else { + _balBuilder?.GeneratedBlockAccessList.RollbackCurrentIndex(); args.Set(TxAction.Skip, result.ErrorDescription!); } } diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockValidationTransactionsExecutor.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockValidationTransactionsExecutor.cs index 31c97c9d263..c74ccc0b74f 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockValidationTransactionsExecutor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockValidationTransactionsExecutor.cs @@ -10,7 +10,6 @@ using Nethermind.Evm; using Nethermind.Evm.State; using Nethermind.Evm.TransactionProcessing; - using Metrics = Nethermind.Evm.Metrics; namespace Nethermind.Consensus.Processing @@ -23,6 +22,8 @@ public class BlockValidationTransactionsExecutor( BlockValidationTransactionsExecutor.ITransactionProcessedEventHandler? transactionProcessedEventHandler = null) : IBlockProcessor.IBlockTransactionsExecutor { + private readonly IBlockAccessListBuilder? _balBuilder = stateProvider as IBlockAccessListBuilder; + public void SetBlockExecutionContext(in BlockExecutionContext blockExecutionContext) { transactionProcessor.SetBlockExecutionContext(in blockExecutionContext); @@ -32,12 +33,27 @@ public TxReceipt[] ProcessTransactions(Block block, ProcessingOptions processing { Metrics.ResetBlockStats(); + long? gasRemaining = _balBuilder?.GasUsed(); + if (gasRemaining is not null) + { + _balBuilder.ValidateBlockAccessList(block.Header, 0, gasRemaining!.Value); + } + for (int i = 0; i < block.Transactions.Length; i++) { + _balBuilder?.GeneratedBlockAccessList.IncrementBlockAccessIndex(); Transaction currentTx = block.Transactions[i]; ProcessTransaction(block, currentTx, i, receiptsTracer, processingOptions); + + if (gasRemaining is not null) + { + gasRemaining -= currentTx.SpentGas; + _balBuilder.ValidateBlockAccessList(block.Header, (ushort)(i + 1), gasRemaining!.Value); + } } - return receiptsTracer.TxReceipts.ToArray(); + _balBuilder?.GeneratedBlockAccessList.IncrementBlockAccessIndex(); + + return [.. receiptsTracer.TxReceipts]; } protected virtual void ProcessTransaction(Block block, Transaction currentTx, int index, BlockReceiptsTracer receiptsTracer, ProcessingOptions processingOptions) diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs index 51d26a545eb..27c5e6b0e57 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs @@ -15,6 +15,7 @@ using Nethermind.Consensus.Validators; using Nethermind.Consensus.Withdrawals; using Nethermind.Core; +using Nethermind.Core.Crypto; using Nethermind.Core.Specs; using Nethermind.Core.Threading; using Nethermind.Crypto; @@ -23,6 +24,7 @@ using Nethermind.Evm.Tracing; using Nethermind.Int256; using Nethermind.Logging; +using Nethermind.Serialization.Rlp; using Nethermind.Specs.Forks; using Nethermind.State; using static Nethermind.Consensus.Processing.IBlockProcessor; @@ -44,8 +46,8 @@ public partial class BlockProcessor( : IBlockProcessor { private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly IBlockAccessListBuilder? _balBuilder = stateProvider as IBlockAccessListBuilder; protected readonly WorldStateMetricsDecorator _stateProvider = new(stateProvider); - private readonly IReceiptsRootCalculator _receiptsRootCalculator = ReceiptsRootCalculator.Instance; /// /// We use a single receipt tracer for all blocks. Internally receipt tracer forwards most of the calls @@ -59,6 +61,13 @@ public partial class BlockProcessor( { if (_logger.IsTrace) _logger.Trace($"Processing block {suggestedBlock.ToString(Block.Format.Short)} ({options})"); + if (_balBuilder is not null && spec.BlockLevelAccessListsEnabled) + { + _balBuilder.TracingEnabled = true; + _balBuilder.GeneratedBlockAccessList.Clear(); + _balBuilder.LoadSuggestedBlockAccessList(suggestedBlock.BlockAccessList, suggestedBlock.GasUsed); + } + ApplyDaoTransition(suggestedBlock); Block block = PrepareBlockForProcessing(suggestedBlock); TxReceipt[] receipts = ProcessBlock(block, blockTracer, options, spec, token); @@ -120,7 +129,7 @@ protected virtual TxReceipt[] ProcessBlock( header.BlobGasUsed = BlobGasCalculator.CalculateBlobGas(block.Transactions); } - header.ReceiptsRoot = _receiptsRootCalculator.GetReceiptsRoot(receipts, spec, block.ReceiptsRoot); + header.ReceiptsRoot = ReceiptsRootCalculator.Instance.GetReceiptsRoot(receipts, spec, block.ReceiptsRoot); ApplyMinerRewards(block, blockTracer, spec); withdrawalProcessor.ProcessWithdrawals(block, spec); @@ -131,6 +140,8 @@ protected virtual TxReceipt[] ProcessBlock( executionRequestsProcessor.ProcessExecutionRequests(block, _stateProvider, receipts, spec); + _balBuilder?.SetBlockAccessList(block, spec); + ReceiptsTracer.EndBlockTrace(); _stateProvider.Commit(spec, commitRoots: true); @@ -147,6 +158,7 @@ protected virtual TxReceipt[] ProcessBlock( header.StateRoot = _stateProvider.StateRoot; } + header.Hash = header.CalculateHash(); return receipts; @@ -223,7 +235,10 @@ protected virtual Block PrepareBlockForProcessing(Block suggestedBlock) headerForProcessing.StateRoot = bh.StateRoot; } - return suggestedBlock.WithReplacedHeader(headerForProcessing); + Block block = suggestedBlock.WithReplacedHeader(headerForProcessing); + block.BlockAccessList = suggestedBlock.BlockAccessList; + + return block; } private void ApplyMinerRewards(Block block, IBlockTracer tracer, IReleaseSpec spec) diff --git a/src/Nethermind/Nethermind.Consensus/Processing/IBlockCachePreWarmer.cs b/src/Nethermind/Nethermind.Consensus/Processing/IBlockCachePreWarmer.cs index 2a371513293..8775f11d72a 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/IBlockCachePreWarmer.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/IBlockCachePreWarmer.cs @@ -7,7 +7,7 @@ using Nethermind.Core; using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; -using Nethermind.State; +using Nethermind.Evm.State; namespace Nethermind.Consensus.Processing; diff --git a/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs b/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs index afb3e871b23..f6c842e0712 100644 --- a/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs +++ b/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs @@ -98,7 +98,7 @@ public Witness GetWitness(BlockHeader parentHeader) public bool TryGetAccount(Address address, out AccountStruct account) { RecordEmptySlots(address); - return ((IWorldState)inner).TryGetAccount(address, out account); + return inner.TryGetAccount(address, out account); } public Hash256 StateRoot => inner.StateRoot; @@ -224,11 +224,22 @@ public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec inner.AddToBalance(address, in balanceChange, spec); } + public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + RecordEmptySlots(address); + inner.AddToBalance(address, in balanceChange, spec, out oldBalance); + } + public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) { RecordEmptySlots(address); return inner.AddToBalanceAndCreateIfNotExists(address, in balanceChange, spec); } + public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + RecordEmptySlots(address); + return inner.AddToBalanceAndCreateIfNotExists(address, in balanceChange, spec, out oldBalance); + } public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) { @@ -236,11 +247,22 @@ public void SubtractFromBalance(Address address, in UInt256 balanceChange, IRele inner.SubtractFromBalance(address, in balanceChange, spec); } + public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + RecordEmptySlots(address); + inner.SubtractFromBalance(address, in balanceChange, spec, out oldBalance); + } + public void IncrementNonce(Address address, UInt256 delta) { RecordEmptySlots(address); inner.IncrementNonce(address, delta); } + public void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) + { + RecordEmptySlots(address); + inner.IncrementNonce(address, delta, out oldNonce); + } public void DecrementNonce(Address address, UInt256 delta) { diff --git a/src/Nethermind/Nethermind.Consensus/Validators/BlockValidator.cs b/src/Nethermind/Nethermind.Consensus/Validators/BlockValidator.cs index f2c9755727b..24df8deca53 100644 --- a/src/Nethermind/Nethermind.Consensus/Validators/BlockValidator.cs +++ b/src/Nethermind/Nethermind.Consensus/Validators/BlockValidator.cs @@ -75,7 +75,8 @@ private bool ValidateBlock(Block block, BlockHeader? parent, out stri ValidateUncles(block, spec, validateHashes, ref errorMessage) && ValidateTxRootMatchesTxs(block, validateHashes, ref errorMessage) && ValidateEip4844Fields(block, spec, ref errorMessage) && - ValidateWithdrawals(block, spec, validateHashes, ref errorMessage); + ValidateWithdrawals(block, spec, validateHashes, ref errorMessage) && + ValidateBlockLevelAccessList(block, spec, ref errorMessage); } private bool ValidateHeader(Block block, BlockHeader? parent, ref string? errorMessage) @@ -212,6 +213,14 @@ public bool ValidateProcessedBlock(Block processedBlock, TxReceipt[] receipts, B error ??= BlockErrorMessages.InvalidRequestsHash(suggestedBlock.Header.RequestsHash, processedBlock.Header.RequestsHash); } + if (processedBlock.Header.BlockAccessListHash != suggestedBlock.Header.BlockAccessListHash) + { + if (_logger.IsWarn) _logger.Warn($"- block access list hash : expected {suggestedBlock.Header.BlockAccessListHash}, got {processedBlock.Header.BlockAccessListHash}"); + error ??= BlockErrorMessages.InvalidBlockLevelAccessListHash(suggestedBlock.Header.BlockAccessListHash, processedBlock.Header.BlockAccessListHash); + if (_logger.IsDebug) _logger.Debug($"Generated block access list:\n{processedBlock.GeneratedBlockAccessList}\nSuggested block access list:\n{processedBlock.BlockAccessList}"); + suggestedBlock.GeneratedBlockAccessList = processedBlock.GeneratedBlockAccessList; + } + if (receipts.Length != processedBlock.Transactions.Length) { if (_logger.IsWarn) _logger.Warn($"- receipt count mismatch: expected {processedBlock.Transactions.Length} receipts to match transaction count, got {receipts.Length}"); @@ -381,6 +390,36 @@ public virtual bool ValidateBodyAgainstHeader(BlockHeader header, BlockBody toBe return true; } + public virtual bool ValidateBlockLevelAccessList(Block block, IReleaseSpec spec, ref string? error) + { + // n.b. block BAL could be null if it doesn't come from engine API eg. RLP tests + + if (!spec.BlockLevelAccessListsEnabled && block.BlockAccessList is not null) + { + error = BlockErrorMessages.BlockLevelAccessListNotEnabled; + + if (_logger.IsWarn) _logger.Warn($"Block level access list must be null in block {block.Hash} when EIP-7928 not activated."); + + return false; + } + + if (block.BlockAccessList is not null) + { + if (!ValidateBlockLevelAccessListHashMatches(block, out Hash256 blockLevelAccessListHash)) + { + error = BlockErrorMessages.InvalidBlockLevelAccessListHash(block.Header.BlockAccessListHash, blockLevelAccessListHash); + if (_logger.IsWarn) _logger.Warn($"Block level access list hash mismatch in block {block.ToString(Block.Format.FullHashAndNumber)}: expected {block.Header.BlockAccessListHash}, got {blockLevelAccessListHash}"); + + return false; + } + } + + error = null; + + return true; + + } + private bool ValidateTxRootMatchesTxs(Block block, bool validateHashes, [NotNullWhen(false)] ref string? errorMessage) { if (validateHashes && !ValidateTxRootMatchesTxs(block.Header, block.Body, out Hash256 txRoot)) @@ -416,5 +455,19 @@ public static bool ValidateWithdrawalsHashMatches(BlockHeader header, BlockBody return (withdrawalsRoot = new WithdrawalTrie(body.Withdrawals).RootHash) == header.WithdrawalsRoot; } + public static bool ValidateBlockLevelAccessListHashMatches(Block block, out Hash256? balRoot) + { + BlockHeader header = block.Header; + if (block.BlockAccessList is null) + { + balRoot = null; + return header.BlockAccessListHash is null; + } + + balRoot = new(ValueKeccak.Compute(block.EncodedBlockAccessList!).Bytes); + + return balRoot == header.BlockAccessListHash; + } + private static string Invalid(Block block) => $"Invalid block {block.ToString(Block.Format.FullHashAndNumber)}:"; } diff --git a/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs b/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs index 61e3f0057b0..c8d9b23a072 100644 --- a/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs +++ b/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs @@ -97,7 +97,8 @@ protected bool Validate(BlockHeader header, BlockHeader? parent, bool && (orphaned || ValidateBlockNumber(header, parent, ref error)) && (orphaned || Validate1559(header, parent, spec, ref error)) && (orphaned || ValidateBlobGasFields(header, parent, spec, ref error)) - && ValidateRequestsHash(header, spec, ref error); + && ValidateRequestsHash(header, spec, ref error) + && ValidateBlockAccessListHash(header, spec, ref error); } public bool ValidateOrphaned(BlockHeader header, [NotNullWhen(false)] out string? error) => @@ -384,5 +385,28 @@ protected virtual bool ValidateBlobGasFields(BlockHeader header, BlockHeader par { return BlobGasCalculator.CalculateExcessBlobGas(parent, spec); } + + private bool ValidateBlockAccessListHash(BlockHeader header, IReleaseSpec spec, ref string? error) + { + if (spec.IsEip7928Enabled) + { + if (header.BlockAccessListHash is null) + { + if (_logger.IsWarn) _logger.Warn("BlockAccessListHash field is not set."); + error = BlockErrorMessages.MissingBlockLevelAccessListHash; + return false; + } + } + else + { + if (header.BlockAccessListHash is not null) + { + if (_logger.IsWarn) _logger.Warn("BlockAccessListHash field should not have value."); + error = BlockErrorMessages.BlockLevelAccessListHashNotEnabled; + return false; + } + } + return true; + } } } diff --git a/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs b/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs index f83c6e3ea1c..af070bccd82 100644 --- a/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs +++ b/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs @@ -61,6 +61,7 @@ public class TestBlockchain : IDisposable public IReadOnlyTxProcessingEnvFactory ReadOnlyTxProcessingEnvFactory => _fromContainer.ReadOnlyTxProcessingEnvFactory; public IShareableTxProcessorSource ShareableTxProcessorSource => _fromContainer.ShareableTxProcessorSource; public IBranchProcessor BranchProcessor => _fromContainer.MainProcessingContext.BranchProcessor; + public IBlockProcessor BlockProcessor => _fromContainer.MainProcessingContext.BlockProcessor; public IBlockchainProcessor BlockchainProcessor => _fromContainer.MainProcessingContext.BlockchainProcessor; public IBlockProcessingQueue BlockProcessingQueue => _fromContainer.MainProcessingContext.BlockProcessingQueue; public IBlockPreprocessorStep BlockPreprocessorStep => _fromContainer.BlockPreprocessorStep; @@ -318,12 +319,14 @@ public Block Build() if (specProvider.GenesisSpec.IsBeaconBlockRootAvailable) { state.CreateAccount(specProvider.GenesisSpec.Eip4788ContractAddress!, 1); + state.InsertCode(Eip4788Constants.BeaconRootsAddress, Eip4788TestConstants.CodeHash, Eip4788TestConstants.Code, specProvider.GenesisSpec); } // Eip2935 - if (specProvider.GenesisSpec.IsBlockHashInStateAvailable) + if (specProvider.GenesisSpec.IsEip2935Enabled) { state.CreateAccount(specProvider.GenesisSpec.Eip2935ContractAddress!, 1); + state.InsertCode(Eip2935Constants.BlockHashHistoryAddress, Eip2935TestConstants.CodeHash, Eip2935TestConstants.Code, specProvider.GenesisSpec); } state.CreateAccount(TestItem.AddressA, testConfiguration.AccountInitialValue); @@ -331,7 +334,7 @@ public Block Build() state.CreateAccount(TestItem.AddressC, testConfiguration.AccountInitialValue); byte[] code = Bytes.FromHexString("0xabcd"); - state.InsertCode(TestItem.AddressA, code, specProvider.GenesisSpec!); + state.InsertCode(TestItem.AddressA, code, specProvider.GenesisSpec); state.Set(new StorageCell(TestItem.AddressA, UInt256.One), Bytes.FromHexString("0xabcdef")); IReleaseSpec? finalSpec = specProvider.GetFinalSpec(); @@ -339,13 +342,13 @@ public Block Build() if (finalSpec?.WithdrawalsEnabled is true) { state.CreateAccount(Eip7002Constants.WithdrawalRequestPredeployAddress, 0, Eip7002TestConstants.Nonce); - state.InsertCode(Eip7002Constants.WithdrawalRequestPredeployAddress, Eip7002TestConstants.CodeHash, Eip7002TestConstants.Code, specProvider.GenesisSpec!); + state.InsertCode(Eip7002Constants.WithdrawalRequestPredeployAddress, Eip7002TestConstants.CodeHash, Eip7002TestConstants.Code, specProvider.GenesisSpec); } if (finalSpec?.ConsolidationRequestsEnabled is true) { state.CreateAccount(Eip7251Constants.ConsolidationRequestPredeployAddress, 0, Eip7251TestConstants.Nonce); - state.InsertCode(Eip7251Constants.ConsolidationRequestPredeployAddress, Eip7251TestConstants.CodeHash, Eip7251TestConstants.Code, specProvider.GenesisSpec!); + state.InsertCode(Eip7251Constants.ConsolidationRequestPredeployAddress, Eip7251TestConstants.CodeHash, Eip7251TestConstants.Code, specProvider.GenesisSpec); } BlockBuilder genesisBlockBuilder = Builders.Build.A.Block.Genesis; @@ -371,6 +374,11 @@ public Block Build() genesisBlockBuilder.WithEmptyRequestsHash(); } + if (specProvider.GenesisSpec.BlockLevelAccessListsEnabled) + { + genesisBlockBuilder.WithBlockAccessListHash(Keccak.OfAnEmptySequenceRlp); + } + Block genesisBlock = genesisBlockBuilder.TestObject; foreach (IGenesisPostProcessor genesisPostProcessor in postProcessors) diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/AccountChangesBuilder.cs b/src/Nethermind/Nethermind.Core.Test/Builders/AccountChangesBuilder.cs new file mode 100644 index 00000000000..67ec751222b --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Builders/AccountChangesBuilder.cs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.BlockAccessLists; +using Nethermind.Int256; + +namespace Nethermind.Core.Test.Builders +{ + public class AccountChangesBuilder : BuilderBase + { + public AccountChangesBuilder() + { + TestObjectInternal = new AccountChanges(Address.Zero); + } + + public AccountChangesBuilder WithAddress(Address address) + { + TestObjectInternal.Address = address; + return this; + } + + public AccountChangesBuilder WithStorageReads(params UInt256[] keys) + { + foreach (UInt256 key in keys) + { + TestObjectInternal.AddStorageRead(key); + } + return this; + } + + + public AccountChangesBuilder WithStorageChanges(UInt256 key, params StorageChange[] storageChanges) + { + SlotChanges slotChanges = TestObjectInternal.GetOrAddSlotChanges(key); + foreach (StorageChange storageChange in storageChanges) + { + slotChanges.Changes.Add(storageChange.BlockAccessIndex, storageChange); + } + return this; + } + + public AccountChangesBuilder WithNonceChanges(params NonceChange[] nonceChanges) + { + foreach (NonceChange nonceChange in nonceChanges) + { + TestObjectInternal.AddNonceChange(nonceChange); + } + return this; + } + + public AccountChangesBuilder WithBalanceChanges(params BalanceChange[] balanceChanges) + { + foreach (BalanceChange balanceChange in balanceChanges) + { + TestObjectInternal.AddBalanceChange(balanceChange); + } + return this; + } + + public AccountChangesBuilder WithCodeChanges(params CodeChange[] codeChanges) + { + foreach (CodeChange codeChange in codeChanges) + { + TestObjectInternal.AddCodeChange(codeChange); + } + return this; + } + } +} diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/BlockAccessListBuilder.cs b/src/Nethermind/Nethermind.Core.Test/Builders/BlockAccessListBuilder.cs new file mode 100644 index 00000000000..1efe2dc83e8 --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Builders/BlockAccessListBuilder.cs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Crypto; +using Nethermind.Int256; + +namespace Nethermind.Core.Test.Builders +{ + public class BlockAccessListBuilder : BuilderBase + { + + public BlockAccessListBuilder() + { + TestObjectInternal = new BlockAccessList(); + } + + public BlockAccessListBuilder WithAccountChanges(params AccountChanges[] accountChanges) + { + TestObjectInternal.AddAccountChanges(accountChanges); + return this; + } + + + public BlockAccessListBuilder WithPrecompileChanges(Hash256 parentHash, ulong timestamp) + { + TestObjectInternal.AddAccountChanges([ + Eip2935Changes(parentHash), + Eip4788Changes(timestamp), + Eip7002Changes, + Eip7251Changes + ]); + return this; + } + + private static AccountChanges Eip2935Changes(Hash256 parentHash) + { + StorageChange parentHashStorageChange = new(0, new UInt256(parentHash.BytesToArray(), isBigEndian: true)); + return Build.An.AccountChanges + .WithAddress(Eip2935Constants.BlockHashHistoryAddress) + .WithStorageChanges(0, parentHashStorageChange) + .TestObject; + } + private static AccountChanges Eip4788Changes(ulong timestamp) + { + UInt256 eip4788Slot1 = timestamp % Eip4788Constants.RingBufferSize; + UInt256 eip4788Slot2 = (timestamp % Eip4788Constants.RingBufferSize) + Eip4788Constants.RingBufferSize; + + return Build.An.AccountChanges + .WithAddress(Eip4788Constants.BeaconRootsAddress) + .WithStorageChanges(eip4788Slot1, [new(0, timestamp)]) + .WithStorageReads(eip4788Slot2) + .TestObject; + } + private readonly AccountChanges Eip7002Changes = Build.An.AccountChanges + .WithAddress(Eip7002Constants.WithdrawalRequestPredeployAddress) + .WithStorageReads(0, 1, 2, 3) + .TestObject; + private readonly AccountChanges Eip7251Changes = Build.An.AccountChanges + .WithAddress(Eip7251Constants.ConsolidationRequestPredeployAddress) + .WithStorageReads(0, 1, 2, 3) + .TestObject; + } +} diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/BlockBuilder.cs b/src/Nethermind/Nethermind.Core.Test/Builders/BlockBuilder.cs index 38ab036671a..da13b14370c 100644 --- a/src/Nethermind/Nethermind.Core.Test/Builders/BlockBuilder.cs +++ b/src/Nethermind/Nethermind.Core.Test/Builders/BlockBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using Nethermind.Blockchain; +using Nethermind.Core.BlockAccessLists; using Nethermind.Core.Crypto; using Nethermind.Core.ExecutionRequest; using Nethermind.Core.Specs; @@ -300,5 +301,23 @@ public BlockBuilder WithEncodedSize(int? encodedSize) TestObjectInternal.EncodedSize = encodedSize; return this; } + + public BlockBuilder WithBlockAccessListHash(Hash256? hash) + { + TestObjectInternal.Header.BlockAccessListHash = hash; + return this; + } + + public BlockBuilder WithBlockAccessList(BlockAccessList? bal) + { + TestObjectInternal.BlockAccessList = bal; + return this; + } + + public BlockBuilder WithEncodedBlockAccessList(byte[]? bal) + { + TestObjectInternal.EncodedBlockAccessList = bal; + return this; + } } } diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/BlockHeaderBuilder.cs b/src/Nethermind/Nethermind.Core.Test/Builders/BlockHeaderBuilder.cs index fbe217d95e6..692ca548dfa 100644 --- a/src/Nethermind/Nethermind.Core.Test/Builders/BlockHeaderBuilder.cs +++ b/src/Nethermind/Nethermind.Core.Test/Builders/BlockHeaderBuilder.cs @@ -212,4 +212,9 @@ public BlockHeaderBuilder WithRequestsHash(Hash256? requestsHash) TestObjectInternal.RequestsHash = requestsHash; return this; } + public BlockHeaderBuilder WithBlockAccessListHash(Hash256? requestsHash) + { + TestObjectInternal.BlockAccessListHash = requestsHash; + return this; + } } diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/BlockTreeBuilder.cs b/src/Nethermind/Nethermind.Core.Test/Builders/BlockTreeBuilder.cs index 655da840187..a55d8c645c2 100644 --- a/src/Nethermind/Nethermind.Core.Test/Builders/BlockTreeBuilder.cs +++ b/src/Nethermind/Nethermind.Core.Test/Builders/BlockTreeBuilder.cs @@ -71,6 +71,7 @@ public BlockTree BlockTree BlockInfoDb, MetadataDb, BadBlockStore, + BlockAccessListStore, ChainLevelInfoRepository, _specProvider, BloomStorage, @@ -111,6 +112,7 @@ public IBlockStore BlockStore public IDb HeadersDb { get; set; } = new TestMemDb(); public IDb BlockNumbersDb { get; set; } = new TestMemDb(); + public IDb BlockAccessListsDb { get; set; } = new TestMemDb(); private IHeaderStore? _headerStore; public IHeaderStore HeaderStore @@ -125,6 +127,19 @@ public IHeaderStore HeaderStore } } + private IBlockAccessListStore? _balStore; + public IBlockAccessListStore BlockAccessListStore + { + get + { + return _balStore ??= new BlockAccessListStore(BlockAccessListsDb); + } + set + { + _balStore = value; + } + } + public IDb BlockInfoDb { get; set; } = new TestMemDb(); public IDb MetadataDb { get; set; } = new TestMemDb(); diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/Build.AccountChanges.cs b/src/Nethermind/Nethermind.Core.Test/Builders/Build.AccountChanges.cs new file mode 100644 index 00000000000..9033ff70e0b --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Builders/Build.AccountChanges.cs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core.Test.Builders +{ + public partial class Build + { + public AccountChangesBuilder AccountChanges => new(); + } +} diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/Build.BlockAccessList.cs b/src/Nethermind/Nethermind.Core.Test/Builders/Build.BlockAccessList.cs new file mode 100644 index 00000000000..47786fd2f76 --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Builders/Build.BlockAccessList.cs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core.Test.Builders +{ + public partial class Build + { + public BlockAccessListBuilder BlockAccessList => new(); + } +} diff --git a/src/Nethermind/Nethermind.Core.Test/Eip2935TestConstants.cs b/src/Nethermind/Nethermind.Core.Test/Eip2935TestConstants.cs new file mode 100644 index 00000000000..189ec1c9136 --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Eip2935TestConstants.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Int256; + +namespace Nethermind.Core.Test; + +public static class Eip2935TestConstants +{ + public static readonly byte[] Code = Bytes.FromHexString("0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500"); + + public static readonly byte[] InitCode = Bytes.FromHexString("0x60538060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500"); + public static readonly ValueHash256 CodeHash = ValueKeccak.Compute(Code); + + public static readonly UInt256 Nonce = 1; +} diff --git a/src/Nethermind/Nethermind.Core.Test/Eip4788TestConstants.cs b/src/Nethermind/Nethermind.Core.Test/Eip4788TestConstants.cs new file mode 100644 index 00000000000..a581508379e --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Eip4788TestConstants.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Int256; + +namespace Nethermind.Core.Test; + +public static class Eip4788TestConstants +{ + public static readonly byte[] Code = Bytes.FromHexString("0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"); + + public static readonly ValueHash256 CodeHash = ValueKeccak.Compute(Code); + + public static readonly UInt256 Nonce = 1; +} diff --git a/src/Nethermind/Nethermind.Core.Test/Encoding/BlockAccessListDecoderTests.cs b/src/Nethermind/Nethermind.Core.Test/Encoding/BlockAccessListDecoderTests.cs new file mode 100644 index 00000000000..35a07f2d3a6 --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Encoding/BlockAccessListDecoderTests.cs @@ -0,0 +1,436 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Int256; +using Nethermind.Serialization.Rlp; +using Nethermind.Serialization.Rlp.Eip7928; +using NUnit.Framework; + +namespace Nethermind.Core.Test.Encoding; + +[TestFixture] +public class BlockAccessListDecoderTests +{ + [TestCaseSource(nameof(BlockAccessListTestSource))] + public void Can_decode_then_encode(string rlp, BlockAccessList expected) + { + BlockAccessList bal = Rlp.Decode(Bytes.FromHexString(rlp)); + + Assert.That(bal, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(bal).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(rlp); + Assert.That(encoded, Is.EqualTo(rlp)); + } + + [Test] + public void Can_decode_then_encode_balance_change() + { + const string rlp = "0xc801861319718811c8"; + Rlp.ValueDecoderContext ctx = new(Bytes.FromHexString(rlp)); + BalanceChange balanceChange = BalanceChangeDecoder.Instance.Decode(ref ctx, RlpBehaviors.None); + BalanceChange expected = new(1, 0x1319718811c8); + Assert.That(balanceChange, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(balanceChange).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(rlp); + Assert.That(encoded, Is.EqualTo(rlp)); + } + + [Test] + public void Can_decode_then_encode_nonce_change() + { + const string rlp = "0xc20101"; + Rlp.ValueDecoderContext ctx = new(Bytes.FromHexString(rlp)); + NonceChange nonceChange = NonceChangeDecoder.Instance.Decode(ref ctx, RlpBehaviors.None); + NonceChange expected = new(1, 1); + Assert.That(nonceChange, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(nonceChange).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(rlp); + Assert.That(encoded, Is.EqualTo(rlp)); + } + + [Test] + public void Can_decode_then_encode_slot_change() + { + StorageChange parentHashStorageChange = new(0, new UInt256(Bytes.FromHexString("0xc382836f81d7e4055a0e280268371e17cc69a531efe2abee082e9b922d6050fd"), isBigEndian: true)); + SlotChanges expected = new(0, new SortedList { { 0, parentHashStorageChange } }); + + string expectedRlp = "0x" + Bytes.ToHexString(Rlp.Encode(expected).Bytes); + + Rlp.ValueDecoderContext ctx = new(Bytes.FromHexString(expectedRlp)); + SlotChanges slotChange = SlotChangesDecoder.Instance.Decode(ref ctx, RlpBehaviors.None); + Assert.That(slotChange, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(slotChange).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(expectedRlp); + Assert.That(encoded, Is.EqualTo(expectedRlp)); + } + + [Test] + public void Can_decode_then_encode_storage_change() + { + StorageChange expected = new(0, new UInt256(Bytes.FromHexString("0xc382836f81d7e4055a0e280268371e17cc69a531efe2abee082e9b922d6050fd"), isBigEndian: true)); + + string expectedRlp = "0x" + Bytes.ToHexString(Rlp.Encode(expected).Bytes); + + Rlp.ValueDecoderContext ctx = new(Bytes.FromHexString(expectedRlp)); + StorageChange storageChange = StorageChangeDecoder.Instance.Decode(ref ctx, RlpBehaviors.None); + Assert.That(storageChange, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(storageChange).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(expectedRlp); + Assert.That(encoded, Is.EqualTo(expectedRlp)); + } + + [Test] + public void Can_decode_then_encode_code_change() + { + const string rlp = "0xc20100"; + + Rlp.ValueDecoderContext ctx = new(Bytes.FromHexString(rlp)); + CodeChange codeChange = CodeChangeDecoder.Instance.Decode(ref ctx, RlpBehaviors.None); + CodeChange expected = new(1, [0x0]); + Assert.That(codeChange, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(codeChange).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(rlp); + Assert.That(encoded, Is.EqualTo(rlp)); + } + + [TestCaseSource(nameof(AccountChangesTestSource))] + public void Can_decode_then_encode_account_change(string rlp, AccountChanges expected) + { + Rlp.ValueDecoderContext ctx = new(Bytes.FromHexString(rlp)); + AccountChanges accountChange = AccountChangesDecoder.Instance.Decode(ref ctx, RlpBehaviors.None); + + Assert.That(accountChange, Is.EqualTo(expected)); + + string encoded = "0x" + Bytes.ToHexString(Rlp.Encode(accountChange).Bytes); + Console.WriteLine(encoded); + Console.WriteLine(rlp); + Assert.That(encoded, Is.EqualTo(rlp)); + } + + [Test] + public void Can_encode_then_decode() + { + StorageChange storageChange = new() + { + BlockAccessIndex = 10, + NewValue = 0xcad + }; + byte[] storageChangeBytes = Rlp.Encode(storageChange, RlpBehaviors.None).Bytes; + StorageChange storageChangeDecoded = Rlp.Decode(storageChangeBytes, RlpBehaviors.None); + Assert.That(storageChange, Is.EqualTo(storageChangeDecoded)); + + var storageChanges = new SortedList { { 10, storageChange } }; + SlotChanges slotChanges = new(0xbad, storageChanges); + byte[] slotChangesBytes = Rlp.Encode(slotChanges, RlpBehaviors.None).Bytes; + SlotChanges slotChangesDecoded = Rlp.Decode(slotChangesBytes, RlpBehaviors.None); + Assert.That(slotChanges, Is.EqualTo(slotChangesDecoded)); + + StorageRead storageRead = new(0xbababa); + StorageRead storageRead2 = new(0xcacaca); + byte[] storageReadBytes = Rlp.Encode(storageRead, RlpBehaviors.None).Bytes; + StorageRead storageReadDecoded = Rlp.Decode(storageReadBytes, RlpBehaviors.None); + Assert.That(storageRead, Is.EqualTo(storageReadDecoded)); + + BalanceChange balanceChange = new(10, 0); + BalanceChange balanceChange2 = new(11, 1); + byte[] balanceChangeBytes = Rlp.Encode(balanceChange, RlpBehaviors.None).Bytes; + BalanceChange balanceChangeDecoded = Rlp.Decode(balanceChangeBytes, RlpBehaviors.None); + Assert.That(balanceChange, Is.EqualTo(balanceChangeDecoded)); + + NonceChange nonceChange = new(10, 0); + NonceChange nonceChange2 = new(11, 0); + byte[] nonceChangeBytes = Rlp.Encode(nonceChange, RlpBehaviors.None).Bytes; + NonceChange nonceChangeDecoded = Rlp.Decode(nonceChangeBytes, RlpBehaviors.None); + Assert.That(nonceChange, Is.EqualTo(nonceChangeDecoded)); + + CodeChange codeChange = new(10, [0, 50]); + byte[] codeChangeBytes = Rlp.Encode(codeChange, RlpBehaviors.None).Bytes; + CodeChange codeChangeDecoded = Rlp.Decode(codeChangeBytes, RlpBehaviors.None); + Assert.That(codeChange, Is.EqualTo(codeChangeDecoded)); + + AccountChanges accountChanges = Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithStorageChanges(slotChanges.Slot, storageChange) + .WithStorageReads(0xbababa, 0xcacaca) + .WithBalanceChanges([balanceChange, balanceChange2]) + .WithNonceChanges([nonceChange, nonceChange2]) + .WithCodeChanges(codeChange) + .TestObject; + byte[] accountChangesBytes = Rlp.Encode(accountChanges, RlpBehaviors.None).Bytes; + AccountChanges accountChangesDecoded = Rlp.Decode(accountChangesBytes, RlpBehaviors.None); + Assert.That(accountChanges, Is.EqualTo(accountChangesDecoded)); + + SortedDictionary accountChangesDict = new() + { + { accountChanges.Address, accountChanges } + }; + + BlockAccessList blockAccessList = new(accountChangesDict); + byte[] blockAccessListBytes = Rlp.Encode(blockAccessList, RlpBehaviors.None).Bytes; + BlockAccessList blockAccessListDecoded = Rlp.Decode(blockAccessListBytes, RlpBehaviors.None); + Assert.That(blockAccessList, Is.EqualTo(blockAccessListDecoded)); + } + + [Test] + public void Decoding_block_access_list_with_unsorted_account_changes_throws() + { + AccountChanges accountChangesA = Build.An.AccountChanges.WithAddress(TestItem.AddressA).TestObject; + AccountChanges accountChangesB = Build.An.AccountChanges.WithAddress(TestItem.AddressB).TestObject; + SortedDictionary accountChanges = new(DescendingComparer
()) + { + { accountChangesA.Address, accountChangesA }, + { accountChangesB.Address, accountChangesB } + }; + + BlockAccessList blockAccessList = new(accountChanges); + byte[] encoded = Rlp.Encode(blockAccessList, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Account changes were in incorrect order.")); + } + + [Test] + public void Decoding_account_changes_with_unsorted_storage_changes_throws() + { + UInt256 slot1 = UInt256.One; + UInt256 slot2 = new(2); + SortedList storageChanges = new(DescendingComparer()) + { + { slot1, new SlotChanges(slot1) }, + { slot2, new SlotChanges(slot2) } + }; + AccountChanges accountChanges = new( + TestItem.AddressA, + storageChanges, + [], + [], + [], + []); + + byte[] encoded = Rlp.Encode(accountChanges, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Storage changes were in incorrect order.")); + } + + [Test] + public void Decoding_account_changes_with_unsorted_storage_reads_throws() + { + SortedSet storageReads = new(DescendingComparer()) + { + new(UInt256.One), + new(new UInt256(2)) + }; + AccountChanges accountChanges = new( + TestItem.AddressA, + [], + storageReads, + [], + [], + []); + + byte[] encoded = Rlp.Encode(accountChanges, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Storage reads were in incorrect order.")); + } + + [Test] + public void Decoding_account_changes_with_unsorted_balance_changes_throws() + { + SortedList balanceChanges = new(DescendingComparer()) + { + { 1, new(1, UInt256.One) }, + { 2, new(2, UInt256.Zero) } + }; + AccountChanges accountChanges = new( + TestItem.AddressA, + [], + [], + balanceChanges, + [], + []); + + byte[] encoded = Rlp.Encode(accountChanges, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Balance changes were in incorrect order.")); + } + + [Test] + public void Decoding_account_changes_with_unsorted_nonce_changes_throws() + { + SortedList nonceChanges = new(DescendingComparer()) + { + { 1, new(1, 1) }, + { 2, new(2, 2) } + }; + AccountChanges accountChanges = new( + TestItem.AddressA, + [], + [], + [], + nonceChanges, + []); + + byte[] encoded = Rlp.Encode(accountChanges, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Nonce changes were in incorrect order.")); + } + + [Test] + public void Decoding_account_changes_with_unsorted_code_changes_throws() + { + SortedList codeChanges = new(DescendingComparer()) + { + { 1, new(1, [0x01]) }, + { 2, new(2, [0x02]) } + }; + AccountChanges accountChanges = new( + TestItem.AddressA, + [], + [], + [], + [], + codeChanges); + + byte[] encoded = Rlp.Encode(accountChanges, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Code changes were in incorrect order.")); + } + + [Test] + public void Decoding_slot_changes_with_unsorted_storage_changes_throws() + { + SortedList storageChanges = new(DescendingComparer()) + { + { 1, new(1, UInt256.One) }, + { 2, new(2, UInt256.Zero) } + }; + SlotChanges slotChanges = new(UInt256.One, storageChanges); + byte[] encoded = Rlp.Encode(slotChanges, RlpBehaviors.None).Bytes; + + Assert.That( + () => Rlp.Decode(encoded, RlpBehaviors.None), + Throws.TypeOf().With.Message.EqualTo("Storage changes were in incorrect order. index=1, lastIndex=2")); + } + + private static IEnumerable BlockAccessListTestSource + { + get + { + StorageChange parentHashStorageChange = new(0, new UInt256(Bytes.FromHexString("0xc382836f81d7e4055a0e280268371e17cc69a531efe2abee082e9b922d6050fd"), isBigEndian: true)); + StorageChange timestampStorageChange = new(0, 0xc); + SortedDictionary expectedAccountChanges = new() + { + { + Eip7002Constants.WithdrawalRequestPredeployAddress, + Build.An.AccountChanges + .WithAddress(Eip7002Constants.WithdrawalRequestPredeployAddress) + .WithStorageReads(0, 1, 2, 3) + .TestObject + }, + { + Eip7251Constants.ConsolidationRequestPredeployAddress, + Build.An.AccountChanges + .WithAddress(Eip7251Constants.ConsolidationRequestPredeployAddress) + .WithStorageReads(0, 1, 2, 3) + .TestObject + }, + { + Eip2935Constants.BlockHashHistoryAddress, + Build.An.AccountChanges + .WithAddress(Eip2935Constants.BlockHashHistoryAddress) + .WithStorageChanges(0, parentHashStorageChange) + .TestObject + }, + { + Eip4788Constants.BeaconRootsAddress, + Build.An.AccountChanges + .WithAddress(Eip4788Constants.BeaconRootsAddress) + .WithStorageChanges(0xc, timestampStorageChange) + .WithStorageReads(0x200b) + .TestObject + }, + { + new("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba"), + Build.An.AccountChanges + .WithAddress(new("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba")) + .WithBalanceChanges([new(1, 0x1319718811c8)]) + .TestObject + }, + { + new("0xaccc7d92b051544a255b8a899071040739bada75"), + Build.An.AccountChanges + .WithAddress(new("0xaccc7d92b051544a255b8a899071040739bada75")) + .WithBalanceChanges([new(1, new UInt256(Bytes.FromHexString("0x3635c99aac6d15af9c"), isBigEndian: true))]) + .WithNonceChanges([new(1, 1)]) + .TestObject + }, + { + new("0xd9c0e57d447779673b236c7423aeab84e931f3ba"), + Build.An.AccountChanges + .WithAddress(new("0xd9c0e57d447779673b236c7423aeab84e931f3ba")) + .WithBalanceChanges([new(1, 0x64)]) + .TestObject + }, + }; + BlockAccessList expected = new(expectedAccountChanges); + string balanceChangesRlp = "0x" + Bytes.ToHexString(Rlp.Encode(expected).Bytes); + yield return new TestCaseData(balanceChangesRlp, expected) + { TestName = "balance_changes" }; + } + } + + private static IEnumerable AccountChangesTestSource + { + get + { + AccountChanges storageReadsExpected = Build.An.AccountChanges + .WithAddress(Eip7002Constants.WithdrawalRequestPredeployAddress) + .WithStorageReads(0, 1, 2, 3) + .TestObject; + string storageReadsRlp = "0x" + Bytes.ToHexString(Rlp.Encode(storageReadsExpected).Bytes); + yield return new TestCaseData(storageReadsRlp, storageReadsExpected) + { TestName = "storage_reads" }; + + AccountChanges storageChangesExpected = Build.An.AccountChanges + .WithAddress(Eip2935Constants.BlockHashHistoryAddress) + .WithStorageChanges( + 0, + [new(0, new(Bytes.FromHexString("0xc382836f81d7e4055a0e280268371e17cc69a531efe2abee082e9b922d6050fd"), isBigEndian: true))]) + .TestObject; + string storageChangesRlp = "0x" + Bytes.ToHexString(Rlp.Encode(storageChangesExpected).Bytes); + yield return new TestCaseData(storageChangesRlp, storageChangesExpected) + { TestName = "storage_changes" }; + } + } + + private static IComparer DescendingComparer() where T : IComparable + => Comparer.Create((left, right) => right.CompareTo(left)); +} diff --git a/src/Nethermind/Nethermind.Core.Test/TestWorldStateFactory.cs b/src/Nethermind/Nethermind.Core.Test/TestWorldStateFactory.cs index 00664323a12..ea89bae9ab6 100644 --- a/src/Nethermind/Nethermind.Core.Test/TestWorldStateFactory.cs +++ b/src/Nethermind/Nethermind.Core.Test/TestWorldStateFactory.cs @@ -16,7 +16,7 @@ namespace Nethermind.Core.Test; public static class TestWorldStateFactory { - public static IWorldState CreateForTest(IDbProvider? dbProvider = null, ILogManager? logManager = null) + public static IWorldState CreateForTest(IDbProvider? dbProvider = null, ILogManager? logManager = null, bool parallel = true) { PruningConfig pruningConfig = new PruningConfig(); TestFinalizedStateProvider finalizedStateProvider = new TestFinalizedStateProvider(pruningConfig.PruningBoundary); @@ -30,7 +30,8 @@ public static IWorldState CreateForTest(IDbProvider? dbProvider = null, ILogMana pruningConfig, LimboLogs.Instance); finalizedStateProvider.TrieStore = trieStore; - return new WorldState(new TrieStoreScopeProvider(trieStore, dbProvider.CodeDb, logManager), logManager); + IWorldState innerWorldState = new WorldState(new TrieStoreScopeProvider(trieStore, dbProvider.CodeDb, logManager), logManager); + return parallel ? new ParallelWorldState(innerWorldState) : innerWorldState; } public static (IWorldState, IStateReader) CreateForTestWithStateReader(IDbProvider? dbProvider = null, ILogManager? logManager = null) diff --git a/src/Nethermind/Nethermind.Core/Block.cs b/src/Nethermind/Nethermind.Core/Block.cs index 5fd94383cdb..ed80682d589 100644 --- a/src/Nethermind/Nethermind.Core/Block.cs +++ b/src/Nethermind/Nethermind.Core/Block.cs @@ -12,25 +12,29 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Int256; +using Nethermind.Core.BlockAccessLists; namespace Nethermind.Core; [DebuggerDisplay("{Hash} ({Number})")] public class Block { - public Block(BlockHeader header, BlockBody body) + public Block(BlockHeader header, BlockBody body, BlockAccessList? bal = null) { Header = header ?? throw new ArgumentNullException(nameof(header)); Body = body ?? throw new ArgumentNullException(nameof(body)); + BlockAccessList = bal; } public Block(BlockHeader header, IEnumerable transactions, IEnumerable uncles, - IEnumerable? withdrawals = null) + IEnumerable? withdrawals = null, + BlockAccessList? blockAccessList = null) { Header = header ?? throw new ArgumentNullException(nameof(header)); Body = new(transactions.ToArray(), uncles.ToArray(), withdrawals?.ToArray()); + BlockAccessList = blockAccessList; } public Block(BlockHeader header) : this( @@ -42,11 +46,11 @@ public Block(BlockHeader header) : this( ) { } - public virtual Block WithReplacedHeader(BlockHeader newHeader) => new(newHeader, Body); + public virtual Block WithReplacedHeader(BlockHeader newHeader) => new(newHeader, Body, BlockAccessList); - public Block WithReplacedBody(BlockBody newBody) => new(Header, newBody); + public Block WithReplacedBody(BlockBody newBody) => new(Header, newBody, BlockAccessList); - public Block WithReplacedBodyCloned(BlockBody newBody) => new(Header.Clone(), newBody); + public Block WithReplacedBodyCloned(BlockBody newBody) => new(Header.Clone(), newBody, BlockAccessList); public BlockHeader Header { get; } @@ -116,6 +120,14 @@ public Transaction[] Transactions public Hash256? ParentBeaconBlockRoot => Header.ParentBeaconBlockRoot; // do not add setter here public Hash256? RequestsHash => Header.RequestsHash; // do not add setter here + public Hash256? BlockAccessListHash => Header.BlockAccessListHash; // do not add setter here + + // suggested BAL from network + [JsonIgnore] + public BlockAccessList? BlockAccessList { get; set; } + + [JsonIgnore] + public BlockAccessList? GeneratedBlockAccessList { get; set; } [JsonIgnore] public byte[][]? ExecutionRequests { get; set; } @@ -126,6 +138,9 @@ public Transaction[] Transactions [JsonIgnore] public int? EncodedSize { get; set; } + [JsonIgnore] + public byte[]? EncodedBlockAccessList { get; set; } + /// /// Pre-encoded transaction bytes in SkipTypedWrapping format (as received from CL). /// Used to avoid re-encoding transactions when storing blocks. diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/AccountChanges.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/AccountChanges.cs new file mode 100644 index 00000000000..f738902dc15 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/AccountChanges.cs @@ -0,0 +1,234 @@ + +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using Nethermind.Int256; +using Nethermind.Serialization.Json; + +namespace Nethermind.Core.BlockAccessLists; + +public class AccountChanges : IEquatable +{ + [JsonConverter(typeof(AddressConverter))] + public Address Address { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList StorageChanges => _storageChanges.Values; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IReadOnlyCollection StorageReads => _storageReads; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList BalanceChanges => _balanceChanges.Values; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList NonceChanges => _nonceChanges.Values; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList CodeChanges => _codeChanges.Values; + + private readonly SortedList _storageChanges; + private readonly SortedSet _storageReads; + private readonly SortedList _balanceChanges; + private readonly SortedList _nonceChanges; + private readonly SortedList _codeChanges; + + public AccountChanges() + { + Address = Address.Zero; + _storageChanges = []; + _storageReads = []; + _balanceChanges = []; + _nonceChanges = []; + _codeChanges = []; + } + + public AccountChanges(Address address) + { + Address = address; + _storageChanges = []; + _storageReads = []; + _balanceChanges = []; + _nonceChanges = []; + _codeChanges = []; + } + + public AccountChanges(Address address, SortedList storageChanges, SortedSet storageReads, SortedList balanceChanges, SortedList nonceChanges, SortedList codeChanges) + { + Address = address; + _storageChanges = storageChanges; + _storageReads = storageReads; + _balanceChanges = balanceChanges; + _nonceChanges = nonceChanges; + _codeChanges = codeChanges; + } + + public bool Equals(AccountChanges? other) => + other is not null && + Address == other.Address && + StorageChanges.SequenceEqual(other.StorageChanges) && + StorageReads.SequenceEqual(other.StorageReads) && + BalanceChanges.SequenceEqual(other.BalanceChanges) && + NonceChanges.SequenceEqual(other.NonceChanges) && + CodeChanges.SequenceEqual(other.CodeChanges); + + public override bool Equals(object? obj) => + obj is AccountChanges other && Equals(other); + public override int GetHashCode() => + Address.GetHashCode(); + + public static bool operator ==(AccountChanges left, AccountChanges right) => + left.Equals(right); + + public static bool operator !=(AccountChanges left, AccountChanges right) => + !(left == right); + + // n.b. implies that length of changes is zero + public bool HasStorageChange(UInt256 key) + => _storageChanges.ContainsKey(key); + + public bool TryGetSlotChanges(UInt256 key, [NotNullWhen(true)] out SlotChanges? slotChanges) + => _storageChanges.TryGetValue(key, out slotChanges); + + public void ClearEmptySlotChangesAndAddRead(UInt256 key) + { + if (TryGetSlotChanges(key, out SlotChanges? slotChanges) && slotChanges.Changes.Count == 0) + { + _storageChanges.Remove(key); + _storageReads.Add(new(key)); + } + } + + public SlotChanges GetOrAddSlotChanges(UInt256 key) + { + if (!_storageChanges.TryGetValue(key, out SlotChanges? existing)) + { + SlotChanges slotChanges = new(key); + _storageChanges.Add(key, slotChanges); + return slotChanges; + } + return existing; + } + + public IEnumerable SlotChangesAtIndex(ushort index) + { + foreach (SlotChanges slotChanges in StorageChanges) + { + if (slotChanges.Changes.TryGetValue(index, out StorageChange storageChange)) + { + yield return new(slotChanges.Slot, new SortedList() { { index, storageChange } }); + } + } + } + + public void AddStorageRead(UInt256 key) + => _storageReads.Add(new(key)); + + public void RemoveStorageRead(UInt256 key) + => _storageReads.Remove(new(key)); + + public void SelfDestruct() + { + foreach (UInt256 key in _storageChanges.Keys) + { + AddStorageRead(key); + } + + _storageChanges.Clear(); + _nonceChanges.Clear(); + _codeChanges.Clear(); + } + + public void AddBalanceChange(BalanceChange balanceChange) + => _balanceChanges.Add(balanceChange.BlockAccessIndex, balanceChange); + + public bool PopBalanceChange(ushort index, [NotNullWhen(true)] out BalanceChange? balanceChange) + { + balanceChange = null; + if (PopChange(_balanceChanges, index, out BalanceChange change)) + { + balanceChange = change; + return true; + } + return false; + } + + public BalanceChange? BalanceChangeAtIndex(ushort index) + => _balanceChanges.TryGetValue(index, out BalanceChange balanceChange) ? balanceChange : null; + + public void AddNonceChange(NonceChange nonceChange) + => _nonceChanges.Add(nonceChange.BlockAccessIndex, nonceChange); + + public bool PopNonceChange(ushort index, [NotNullWhen(true)] out NonceChange? nonceChange) + { + nonceChange = null; + if (PopChange(_nonceChanges, index, out NonceChange change)) + { + nonceChange = change; + return true; + } + return false; + } + + public NonceChange? NonceChangeAtIndex(ushort index) + => _nonceChanges.TryGetValue(index, out NonceChange nonceChange) ? nonceChange : null; + + public void AddCodeChange(CodeChange codeChange) + => _codeChanges.Add(codeChange.BlockAccessIndex, codeChange); + + public bool PopCodeChange(ushort index, [NotNullWhen(true)] out CodeChange? codeChange) + { + codeChange = null; + if (PopChange(_codeChanges, index, out CodeChange change)) + { + codeChange = change; + return true; + } + return false; + } + + public CodeChange? CodeChangeAtIndex(ushort index) + => _codeChanges.TryGetValue(index, out CodeChange codeChange) ? codeChange : null; + + public override string ToString() + { + StringBuilder sb = new(); + sb.Append(Address); + if (BalanceChanges.Count > 0) + sb.Append($" balance=[{string.Join(", ", BalanceChanges)}]"); + if (NonceChanges.Count > 0) + sb.Append($" nonce=[{string.Join(", ", NonceChanges)}]"); + if (CodeChanges.Count > 0) + sb.Append($" code=[{string.Join(", ", CodeChanges)}]"); + if (StorageChanges.Count > 0) + sb.Append($" storage=[{string.Join(", ", StorageChanges)}]"); + if (StorageReads.Count > 0) + sb.Append($" reads=[{string.Join(", ", StorageReads)}]"); + return sb.ToString(); + } + + private static bool PopChange(SortedList changes, ushort index, [NotNullWhen(true)] out T? change) where T : IIndexedChange + { + change = default; + + if (changes.Count == 0) + return false; + + KeyValuePair lastChange = changes.Last(); + + if (lastChange.Key == index) + { + changes.RemoveAt(changes.Count - 1); + change = lastChange.Value; + return true; + } + + return false; + } +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/BalanceChange.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/BalanceChange.cs new file mode 100644 index 00000000000..93c1b991fef --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/BalanceChange.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Int256; + +namespace Nethermind.Core.BlockAccessLists; + +public readonly record struct BalanceChange(ushort BlockAccessIndex, UInt256 PostBalance) : IIndexedChange +{ + public override string ToString() => $"{BlockAccessIndex}:{PostBalance}"; +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/BlockAccessList.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/BlockAccessList.cs new file mode 100644 index 00000000000..0a3633e61a1 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/BlockAccessList.cs @@ -0,0 +1,463 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using Nethermind.Int256; + +namespace Nethermind.Core.BlockAccessLists; + +public class BlockAccessList : IEquatable, IJournal +{ + [JsonIgnore] + public ushort Index = 0; + public IEnumerable AccountChanges => _accountChanges.Values; + + private readonly SortedDictionary _accountChanges; + private readonly Stack _changes; + + public BlockAccessList() + { + _accountChanges = []; + _changes = new(); + } + + public BlockAccessList(SortedDictionary accountChanges) + { + _accountChanges = accountChanges; + _changes = new(); + } + + public bool Equals(BlockAccessList? other) => + other is not null && _accountChanges.SequenceEqual(other._accountChanges); + + public override bool Equals(object? obj) => + obj is BlockAccessList other && Equals(other); + + public override int GetHashCode() => + _accountChanges.Count.GetHashCode(); + + public static bool operator ==(BlockAccessList left, BlockAccessList right) => + left.Equals(right); + + public static bool operator !=(BlockAccessList left, BlockAccessList right) => + !(left == right); + + public AccountChanges? GetAccountChanges(Address address) => _accountChanges.TryGetValue(address, out AccountChanges? value) ? value : null; + + public void IncrementBlockAccessIndex() + { + _changes.Clear(); + Index++; + } + + public void RollbackCurrentIndex() + { + Restore(0); + _changes.Clear(); + Index--; + } + + public void Clear() + { + _accountChanges.Clear(); + _changes.Clear(); + Index = 0; + } + + public void AddBalanceChange(Address address, UInt256 before, UInt256 after) + { + bool isZeroBalanceChange = before == after; + if (address == Address.SystemUser && isZeroBalanceChange) + { + return; + } + + AccountChanges accountChanges = GetOrAddAccountChanges(address); + + // don't add zero balance transfers, but add empty account changes + if (isZeroBalanceChange) + { + return; + } + + bool changedDuringTx = HasBalanceChangedDuringTx(address, before, after); + accountChanges.PopBalanceChange(Index, out BalanceChange? oldBalanceChange); + + _changes.Push(new() + { + Address = address, + Type = ChangeType.BalanceChange, + PreviousValue = oldBalanceChange, + PreTxBalance = before, + BlockAccessIndex = Index + }); + + if (changedDuringTx) + { + accountChanges.AddBalanceChange(new(Index, after)); + } + } + + public void AddCodeChange(Address address, byte[] before, ReadOnlyMemory after) + { + AccountChanges accountChanges = GetOrAddAccountChanges(address); + + if (before.AsSpan().SequenceEqual(after.Span)) + { + return; + } + + bool changedDuringTx = HasCodeChangedDuringTx(accountChanges.Address, before, after.Span); + accountChanges.PopCodeChange(Index, out CodeChange? oldCodeChange); + _changes.Push(new() + { + Address = address, + Type = ChangeType.CodeChange, + PreviousValue = oldCodeChange, + PreTxCode = before + }); + + if (changedDuringTx) + { + accountChanges.AddCodeChange(new(Index, after.ToArray())); + } + } + + public void AddNonceChange(Address address, ulong newNonce) + { + if (newNonce == 0) + { + return; + } + + AccountChanges accountChanges = GetOrAddAccountChanges(address); + + accountChanges.PopNonceChange(Index, out NonceChange? oldNonceChange); + _changes.Push(new() + { + Address = address, + Type = ChangeType.NonceChange, + PreviousValue = oldNonceChange + }); + + accountChanges.AddNonceChange(new(Index, newNonce)); + } + + public void AddAccountRead(Address address) + { + if (!_accountChanges.ContainsKey(address)) + { + _accountChanges.Add(address, new(address)); + } + } + + public void AddStorageChange(Address address, UInt256 key, UInt256 before, UInt256 after) + { + AccountChanges accountChanges = GetOrAddAccountChanges(address); + + if (before != after) + { + StorageChange(accountChanges, key, before, after); + } + } + + public void AddStorageChange(in StorageCell storageCell, UInt256 before, UInt256 after) + => AddStorageChange(storageCell.Address, storageCell.Index, before, after); + + public void AddStorageRead(in StorageCell storageCell) => + AddStorageRead(storageCell.Address, storageCell.Index); + + public void AddStorageRead(Address address, UInt256 key) + { + AccountChanges accountChanges = GetOrAddAccountChanges(address); + + if (!accountChanges.HasStorageChange(key)) + { + accountChanges.AddStorageRead(key); + } + } + + public void DeleteAccount(Address address, UInt256 oldBalance) + { + AccountChanges accountChanges = GetOrAddAccountChanges(address); + accountChanges.SelfDestruct(); + AddBalanceChange(address, oldBalance, 0); + } + + private void StorageChange(AccountChanges accountChanges, in UInt256 key, in UInt256 before, in UInt256 after) + { + SlotChanges slotChanges = accountChanges.GetOrAddSlotChanges(key); + + bool changedDuringTx = HasStorageChangedDuringTx(accountChanges.Address, key, before, after); + slotChanges.TryPopStorageChange(Index, out StorageChange? oldStorageChange); + + _changes.Push(new() + { + Address = accountChanges.Address, + BlockAccessIndex = Index, + Slot = key, + Type = ChangeType.StorageChange, + PreviousValue = oldStorageChange, + PreTxStorage = before + }); + + if (changedDuringTx) + { + slotChanges.Changes.Add(Index, new(Index, after)); + accountChanges.RemoveStorageRead(key); + } + else + { + accountChanges.ClearEmptySlotChangesAndAddRead(key); + } + } + + public int TakeSnapshot() + => _changes.Count; + + public void Restore(int snapshot) + { + snapshot = int.Max(0, snapshot); + while (_changes.Count > snapshot) + { + Change change = _changes.Pop(); + AccountChanges accountChanges = _accountChanges[change.Address]; + switch (change.Type) + { + case ChangeType.BalanceChange: + BalanceChange? previousBalance = change.PreviousValue is null ? null : (BalanceChange)change.PreviousValue; + + // balance could have gone back to pre-tx value + // so would already be empty + accountChanges.PopBalanceChange(change.BlockAccessIndex, out _); // todo: this index must be the same? + if (previousBalance is not null) + { + accountChanges.AddBalanceChange(previousBalance.Value); + } + break; + case ChangeType.CodeChange: + CodeChange? previousCode = change.PreviousValue is null ? null : (CodeChange)change.PreviousValue; + + accountChanges.PopCodeChange(Index, out _); + if (previousCode is not null) + { + accountChanges.AddCodeChange(previousCode.Value); + } + break; + case ChangeType.NonceChange: + NonceChange? previousNonce = change.PreviousValue is null ? null : (NonceChange)change.PreviousValue; + + accountChanges.PopNonceChange(Index, out _); + if (previousNonce is not null) + { + accountChanges.AddNonceChange(previousNonce.Value); + } + break; + case ChangeType.StorageChange: + StorageChange? previousStorage = change.PreviousValue is null ? null : (StorageChange)change.PreviousValue; + SlotChanges slotChanges = accountChanges.GetOrAddSlotChanges(change.Slot!.Value); + + slotChanges.TryPopStorageChange(Index, out _); + if (previousStorage is not null) + { + slotChanges.Changes.Add(previousStorage.Value.BlockAccessIndex, previousStorage.Value); + accountChanges.RemoveStorageRead(change.Slot.Value); + } + + accountChanges.ClearEmptySlotChangesAndAddRead(change.Slot!.Value); + break; + } + } + } + + public IEnumerable GetChangesAtIndex(ushort index) + { + foreach (AccountChanges accountChanges in AccountChanges) + { + bool isPostExecutionSystemContract = + accountChanges.Address == Eip7002Constants.WithdrawalRequestPredeployAddress || + accountChanges.Address == Eip7251Constants.ConsolidationRequestPredeployAddress; + + yield return + new( + accountChanges.Address, + accountChanges.BalanceChangeAtIndex(index), + accountChanges.NonceChangeAtIndex(index), + accountChanges.CodeChangeAtIndex(index), + accountChanges.SlotChangesAtIndex(index), + isPostExecutionSystemContract ? 0 : accountChanges.StorageReads.Count + ); + } + } + + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine($"BlockAccessList (Index={Index}, Accounts={_accountChanges.Count})"); + foreach (AccountChanges ac in _accountChanges.Values) + { + sb.AppendLine($" {ac}"); + } + return sb.ToString(); + } + + // for testing + internal void AddAccountChanges(params AccountChanges[] accountChanges) + { + foreach (AccountChanges change in accountChanges) + { + _accountChanges.Add(change.Address, change); + } + } + + private bool HasBalanceChangedDuringTx(Address address, UInt256 beforeInstr, UInt256 afterInstr) + { + AccountChanges accountChanges = _accountChanges[address]; + IList balanceChanges = accountChanges.BalanceChanges; + int count = balanceChanges.Count; + + if (count == 0) + { + // first balance change of block + // return balance prior to this instruction + return beforeInstr != afterInstr; + } + + for (int i = count - 1; i >= 0; i--) + { + BalanceChange balanceChange = balanceChanges[i]; + if (balanceChange.BlockAccessIndex != Index) + { + // balance changed in previous tx in block + return balanceChange.PostBalance != afterInstr; + } + } + + // balance only changed within this transaction + foreach (Change change in _changes) + { + if (change.Type == ChangeType.BalanceChange && change.Address == address && change.PreviousValue is null) + { + // first change of this transaction & block + return change.PreTxBalance!.Value != afterInstr; + } + } + + // should never happen + throw new InvalidOperationException("Error calculating pre tx balance"); + } + + private bool HasStorageChangedDuringTx(Address address, UInt256 key, in UInt256 beforeInstr, in UInt256 afterInstr) + { + AccountChanges accountChanges = _accountChanges[address]; + + if (!accountChanges.TryGetSlotChanges(key, out SlotChanges? slotChanges) || slotChanges.Changes.Count == 0) + { + // first storage change of block + // return storage prior to this instruction + return beforeInstr != afterInstr; + } + + IList values = slotChanges.Changes.Values; + for (int i = values.Count - 1; i >= 0; i--) + { + StorageChange storageChange = values[i]; + if (storageChange.BlockAccessIndex != Index) + { + // storage changed in previous tx in block + return storageChange.NewValue != afterInstr; + } + } + + // storage only changed within this transaction + foreach (Change change in _changes) + { + if ( + change.Type == ChangeType.StorageChange && + change.Address == address && + change.Slot == key && + change.PreviousValue is null) + { + // first change of this transaction & block + return change.PreTxStorage is null || change.PreTxStorage != afterInstr; + } + } + + // should never happen + throw new InvalidOperationException("Error calculating pre tx storage"); + } + + private bool HasCodeChangedDuringTx(Address address, in ReadOnlySpan beforeInstr, in ReadOnlySpan afterInstr) + { + AccountChanges accountChanges = _accountChanges[address]; + IList codeChanges = accountChanges.CodeChanges; + int count = codeChanges.Count; + + if (count == 0) + { + // first code change of block + // return code prior to this instruction + return !beforeInstr.SequenceEqual(afterInstr); + } + + for (int i = count - 1; i >= 0; i--) + { + CodeChange codeChange = codeChanges[i]; + if (codeChange.BlockAccessIndex != Index) + { + // code changed in previous tx in block + return !codeChange.NewCode.AsSpan().SequenceEqual(afterInstr); + } + } + + // storage only changed within this transaction + foreach (Change change in _changes) + { + if (change.Type == ChangeType.CodeChange && change.Address == address && change.PreviousValue is null) + { + // first change of this transaction & block + return change.PreTxCode is null || !change.PreTxCode.AsSpan().SequenceEqual(afterInstr); + } + } + + // should never happen + throw new InvalidOperationException("Error calculating pre tx code"); + } + + private AccountChanges GetOrAddAccountChanges(Address address) + { + if (!_accountChanges.TryGetValue(address, out AccountChanges? existing)) + { + AccountChanges accountChanges = new(address); + _accountChanges.Add(address, accountChanges); + return accountChanges; + } + return existing; + } + + private enum ChangeType + { + BalanceChange = 0, + CodeChange = 1, + NonceChange = 2, + StorageChange = 3 + } + + private readonly struct Change + { + public Address Address { get; init; } + public UInt256? Slot { get; init; } + public ChangeType Type { get; init; } + public IIndexedChange? PreviousValue { get; init; } + public UInt256? PreTxBalance { get; init; } + public UInt256? PreTxStorage { get; init; } + public byte[]? PreTxCode { get; init; } + public ushort BlockAccessIndex { get; init; } + } +} + +public record struct ChangeAtIndex(Address Address, BalanceChange? BalanceChange, NonceChange? NonceChange, CodeChange? CodeChange, IEnumerable SlotChanges, int Reads); diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/CodeChange.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/CodeChange.cs new file mode 100644 index 00000000000..37ea3846f7f --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/CodeChange.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq; +using System.Text.Json.Serialization; +using Nethermind.Serialization.Json; + +namespace Nethermind.Core.BlockAccessLists; + +public readonly record struct CodeChange(ushort BlockAccessIndex, [property: JsonConverter(typeof(ByteArrayConverter))] byte[] NewCode) : IIndexedChange +{ + public bool Equals(CodeChange other) => + BlockAccessIndex == other.BlockAccessIndex && + CompareByteArrays(NewCode, other.NewCode); + + public override int GetHashCode() => + HashCode.Combine(BlockAccessIndex, NewCode); + + private static bool CompareByteArrays(byte[]? left, byte[]? right) => + ReferenceEquals(left, right) || (left is not null && right is not null && left.SequenceEqual(right)); + + public override string ToString() => $"{BlockAccessIndex}:0x{Convert.ToHexString(NewCode ?? [])}"; +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/IIndexedChange.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/IIndexedChange.cs new file mode 100644 index 00000000000..c99b5da2b91 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/IIndexedChange.cs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core.BlockAccessLists; + +public interface IIndexedChange +{ + public ushort BlockAccessIndex { get; init; } +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/NonceChange.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/NonceChange.cs new file mode 100644 index 00000000000..12abe6cdb5e --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/NonceChange.cs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core.BlockAccessLists; + +public readonly record struct NonceChange(ushort BlockAccessIndex, ulong NewNonce) : IIndexedChange +{ + public override string ToString() => $"{BlockAccessIndex}:{NewNonce}"; +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/SlotChanges.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/SlotChanges.cs new file mode 100644 index 00000000000..4a3d45d7625 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/SlotChanges.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Nethermind.Int256; + +namespace Nethermind.Core.BlockAccessLists; + +public record SlotChanges(UInt256 Slot, SortedList Changes) +{ + public SlotChanges(UInt256 slot) : this(slot, []) { } + + public virtual bool Equals(SlotChanges? other) => + other is not null && + Slot.Equals(other.Slot) && + Changes.SequenceEqual(other.Changes); + + public override int GetHashCode() => + HashCode.Combine(Slot, Changes); + + public override string ToString() => $"{Slot}:[{string.Join(", ", Changes.Values)}]"; + + public bool TryPopStorageChange(ushort index, [NotNullWhen(true)] out StorageChange? storageChange) + { + storageChange = null; + + if (Changes.Count == 0) + return false; + + StorageChange lastChange = Changes.Values.Last(); + + if (lastChange.BlockAccessIndex == index) + { + Changes.RemoveAt(Changes.Count - 1); + storageChange = lastChange; + return true; + } + + return false; + } +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/StorageChange.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/StorageChange.cs new file mode 100644 index 00000000000..c0548e7f1ff --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/StorageChange.cs @@ -0,0 +1,33 @@ + +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Int256; + +namespace Nethermind.Core.BlockAccessLists; + +public readonly struct StorageChange(ushort blockAccessIndex, UInt256 newValue) : IEquatable, IIndexedChange +{ + public ushort BlockAccessIndex { get; init; } = blockAccessIndex; + + public UInt256 NewValue { get; init; } = newValue; + + public readonly bool Equals(StorageChange other) => + BlockAccessIndex == other.BlockAccessIndex && + NewValue.Equals(other.NewValue); + + public override readonly bool Equals(object? obj) => + obj is StorageChange other && Equals(other); + + public override readonly int GetHashCode() => + HashCode.Combine(BlockAccessIndex, NewValue); + + public static bool operator ==(StorageChange left, StorageChange right) => + left.Equals(right); + + public static bool operator !=(StorageChange left, StorageChange right) => + !(left == right); + + public override readonly string ToString() => $"{BlockAccessIndex}:{NewValue}"; +} diff --git a/src/Nethermind/Nethermind.Core/BlockAccessLists/StorageRead.cs b/src/Nethermind/Nethermind.Core/BlockAccessLists/StorageRead.cs new file mode 100644 index 00000000000..53c64143e66 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/BlockAccessLists/StorageRead.cs @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Int256; + +namespace Nethermind.Core.BlockAccessLists; + +public readonly record struct StorageRead(UInt256 Key) : IComparable +{ + public int CompareTo(StorageRead other) + => Key.CompareTo(other.Key); + + public override string ToString() => Key.ToString(); +} diff --git a/src/Nethermind/Nethermind.Core/BlockHeader.cs b/src/Nethermind/Nethermind.Core/BlockHeader.cs index 92bcc5d1eab..a352395a5d0 100644 --- a/src/Nethermind/Nethermind.Core/BlockHeader.cs +++ b/src/Nethermind/Nethermind.Core/BlockHeader.cs @@ -73,13 +73,15 @@ public BlockHeader( public Hash256? WithdrawalsRoot { get; set; } public Hash256? ParentBeaconBlockRoot { get; set; } public Hash256? RequestsHash { get; set; } + public Hash256? BlockAccessListHash { get; set; } public ulong? BlobGasUsed { get; set; } public ulong? ExcessBlobGas { get; set; } public bool HasBody => (TxRoot is not null && TxRoot != Keccak.EmptyTreeHash) || (UnclesHash is not null && UnclesHash != Keccak.OfAnEmptySequenceRlp) - || (WithdrawalsRoot is not null && WithdrawalsRoot != Keccak.EmptyTreeHash); + || (WithdrawalsRoot is not null && WithdrawalsRoot != Keccak.EmptyTreeHash) + || (BlockAccessListHash is not null && BlockAccessListHash != Keccak.OfAnEmptySequenceRlp); - public bool HasTransactions => (TxRoot is not null && TxRoot != Keccak.EmptyTreeHash); + public bool HasTransactions => TxRoot is not null && TxRoot != Keccak.EmptyTreeHash; public bool IsPostMerge { get; set; } @@ -121,6 +123,10 @@ public string ToString(string indent) { builder.AppendLine($"{indent}RequestsHash: {RequestsHash}"); } + if (BlockAccessListHash is not null) + { + builder.AppendLine($"{indent}BlockAccessListHash: {BlockAccessListHash}"); + } return builder.ToString(); } diff --git a/src/Nethermind/Nethermind.Core/Eip4788Constants.cs b/src/Nethermind/Nethermind.Core/Eip4788Constants.cs index af10e162a95..2995bceea8a 100644 --- a/src/Nethermind/Nethermind.Core/Eip4788Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip4788Constants.cs @@ -14,4 +14,9 @@ public static class Eip4788Constants /// Gets the BEACON_ROOTS_ADDRESS parameter. /// public static readonly Address BeaconRootsAddress = new("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02"); + + /// + /// The HISTORY_SERVE_WINDOW parameter. + /// + public static readonly ulong RingBufferSize = 8191; } diff --git a/src/Nethermind/Nethermind.Core/Eip7928Constants.cs b/src/Nethermind/Nethermind.Core/Eip7928Constants.cs new file mode 100644 index 00000000000..88ac07ef199 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Eip7928Constants.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core; + +public static class Eip7928Constants +{ + // Max buffer lengths for RLP decoding + + // max number of transactions per block + public const int MaxTxs = ushort.MaxValue; + + // max number of slots changed / read in one account + public const int MaxSlots = 1_000_000; + + // max number of accounts accessed per block + public const int MaxAccounts = 1_000_000; + + // max code size in bytes + public const int MaxCodeSize = 1_000_000; +} diff --git a/src/Nethermind/Nethermind.Blockchain/BlockchainException.cs b/src/Nethermind/Nethermind.Core/Exceptions/BlockchainException.cs similarity index 92% rename from src/Nethermind/Nethermind.Blockchain/BlockchainException.cs rename to src/Nethermind/Nethermind.Core/Exceptions/BlockchainException.cs index 928423b5619..33d37f4ee60 100644 --- a/src/Nethermind/Nethermind.Blockchain/BlockchainException.cs +++ b/src/Nethermind/Nethermind.Core/Exceptions/BlockchainException.cs @@ -3,7 +3,7 @@ using System; -namespace Nethermind.Blockchain +namespace Nethermind.Core { public class BlockchainException : Exception { diff --git a/src/Nethermind/Nethermind.Blockchain/InvalidBlockException.cs b/src/Nethermind/Nethermind.Core/Exceptions/InvalidBlockException.cs similarity index 89% rename from src/Nethermind/Nethermind.Blockchain/InvalidBlockException.cs rename to src/Nethermind/Nethermind.Core/Exceptions/InvalidBlockException.cs index 80cd68a1b28..b04c6a5e027 100644 --- a/src/Nethermind/Nethermind.Blockchain/InvalidBlockException.cs +++ b/src/Nethermind/Nethermind.Core/Exceptions/InvalidBlockException.cs @@ -2,9 +2,8 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using Nethermind.Core; -namespace Nethermind.Blockchain; +namespace Nethermind.Core; public class InvalidBlockException(BlockHeader block, string message, Exception? innerException = null) : BlockchainException(message, innerException) diff --git a/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs b/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs index 8cb8db2d83c..bacbd809bee 100644 --- a/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs +++ b/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs @@ -150,6 +150,21 @@ public static string InvalidDepositEventLayout(string error) => public static string ExceededBlockSizeLimit(int limit) => $"ExceededBlockSizeLimit: Exceeded block size limit of {limit} bytes."; + public const string MissingBlockLevelAccessList = "MissingBlockLevelAccessList: Must be present in block body."; + + public const string MissingBlockLevelAccessListHash = "MissingBlockLevelAccessListHash: Must be present in block header."; + + public const string InvalidBlockLevelAccessList = + $"InvalidBlockLevelAccessList: Unable to decode."; + + public const string BlockLevelAccessListNotEnabled = + "BlockLevelAccessListNotEnabled: Block body cannot have block level access list."; + + public const string BlockLevelAccessListHashNotEnabled = + "BlockLevelAccessListHashNotEnabled: Block header cannot have block level access list hash."; + public static string InvalidBlockLevelAccessListHash(Hash256 expected, Hash256 actual) => + $"InvalidBlockLevelAccessListHash: Expected {expected}, got {actual}"; + public static string ReceiptCountMismatch(int expectedCount, int actualCount) => $"ReceiptCountMismatch: Expected {expectedCount} receipts to match transaction count, but got {actualCount}."; } diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 52847a1644f..dba86a0a977 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -448,6 +448,12 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec public bool IsRip7728Enabled { get; } /// + /// EIP-7928: Block-Level Access Lists + /// + public bool IsEip7928Enabled { get; } + + bool BlockLevelAccessListsEnabled => IsEip7928Enabled; + /// Precomputed gas cost and refund constants derived from this spec. /// Values are cached per spec instance (singletons per fork) to avoid /// repeated interface dispatch on the EVM opcode hot path. diff --git a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs index cec1d4ba89a..c6ae89c019c 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -114,5 +114,6 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec public virtual bool IsEip7939Enabled => spec.IsEip7939Enabled; public virtual bool IsEip7907Enabled => spec.IsEip7907Enabled; public virtual bool IsRip7728Enabled => spec.IsRip7728Enabled; + public virtual bool IsEip7928Enabled => spec.IsEip7928Enabled; public SpecGasCosts GasCosts => spec.GasCosts; } diff --git a/src/Nethermind/Nethermind.Db.Rocks/Config/DbConfig.cs b/src/Nethermind/Nethermind.Db.Rocks/Config/DbConfig.cs index 42c998de8e2..894d9ab201f 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/Config/DbConfig.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/Config/DbConfig.cs @@ -88,6 +88,8 @@ public class DbConfig : IDbConfig public string BadBlocksDbRocksDbOptions { get; set; } = ""; public string? BadBlocksDbAdditionalRocksDbOptions { get; set; } + public string BlockAccessListsDbRocksDbOptions { get; set; } = ""; + public string? BlockAccessListsDbAdditionalRocksDbOptions { get; set; } public string BlobTransactionsDbRocksDbOptions { get; set; } = "block_based_table_factory.block_cache=32000000;"; diff --git a/src/Nethermind/Nethermind.Db.Rocks/Config/IDbConfig.cs b/src/Nethermind/Nethermind.Db.Rocks/Config/IDbConfig.cs index 1debd3ebfd5..1d202a41603 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/Config/IDbConfig.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/Config/IDbConfig.cs @@ -38,6 +38,9 @@ public interface IDbConfig : IConfig string BadBlocksDbRocksDbOptions { get; set; } string? BadBlocksDbAdditionalRocksDbOptions { get; set; } + string BlockAccessListsDbRocksDbOptions { get; set; } + string? BlockAccessListsDbAdditionalRocksDbOptions { get; set; } + string BlobTransactionsDbRocksDbOptions { get; set; } string? BlobTransactionsDbAdditionalRocksDbOptions { get; set; } diff --git a/src/Nethermind/Nethermind.Db/DbNames.cs b/src/Nethermind/Nethermind.Db/DbNames.cs index ddc51ad4855..dbb124ceb68 100644 --- a/src/Nethermind/Nethermind.Db/DbNames.cs +++ b/src/Nethermind/Nethermind.Db/DbNames.cs @@ -12,6 +12,7 @@ public static class DbNames public const string Blocks = "blocks"; public const string Headers = "headers"; public const string BlockNumbers = "blockNumbers"; + public const string BlockAccessLists = "blockAccessLists"; public const string Receipts = "receipts"; public const string BlockInfos = "blockInfos"; public const string BadBlocks = "badBlocks"; diff --git a/src/Nethermind/Nethermind.Db/IDbProvider.cs b/src/Nethermind/Nethermind.Db/IDbProvider.cs index 038d70c3842..ec0db852d09 100644 --- a/src/Nethermind/Nethermind.Db/IDbProvider.cs +++ b/src/Nethermind/Nethermind.Db/IDbProvider.cs @@ -15,6 +15,7 @@ public interface IDbProvider : IDisposable public IDb BlockNumbersDb => GetDb(DbNames.BlockNumbers); public IDb BlockInfosDb => GetDb(DbNames.BlockInfos); public IDb BadBlocksDb => GetDb(DbNames.BadBlocks); + public IDb BlockAccessListDb => GetDb(DbNames.BlockAccessLists); // BloomDB progress / config (does not contain blooms - they are kept in bloom storage) public IDb BloomDb => GetDb(DbNames.Bloom); diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs new file mode 100644 index 00000000000..c847e0c17fd --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs @@ -0,0 +1,931 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Nethermind.Blockchain.Tracing; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Precompiles; +using Nethermind.Core.Test; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Evm.State; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.Int256; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +[TestFixture] +public class Eip7928Tests() : VirtualMachineTestsBase +{ + protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; + protected override ulong Timestamp => MainnetSpecProvider.AmsterdamBlockTimestamp; + + private static readonly EthereumEcdsa _ecdsa = new(0); + private static readonly UInt256 _accountBalance = 10.Ether; + private static readonly UInt256 _testAccountBalance = 1.Ether; + private static readonly long _gasLimit = 100000; + private static readonly Address _testAddress = ContractAddress.From(TestItem.AddressA, 0); + private static readonly Address _callTargetAddress = TestItem.AddressC; + private static readonly Address _delegationTargetAddress = TestItem.AddressD; + private static readonly UInt256 _delegationSlot = 10; + private static readonly byte[] _delegatedCode = Prepare.EvmCode + .PushData(_delegationSlot) + .Op(Instruction.SLOAD) + .Done; + + [TestCaseSource(nameof(CodeTestSource))] + public async Task Constructs_BAL_when_processing_code( + IEnumerable expected, + byte[] code, + byte[]? extraCode, + bool revert) + { + InitWorldState(TestState, extraCode); + ParallelWorldState worldState = TestState as ParallelWorldState; + worldState.TracingEnabled = true; + + UInt256 value = _testAccountBalance; + + Transaction createTx = Build.A.Transaction + .WithCode(code) + .WithGasLimit(_gasLimit) + .WithValue(value) + .SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + Block block = Build.A.Block.TestObject; + + _processor.SetBlockExecutionContext(new BlockExecutionContext(block.Header, Amsterdam.Instance)); + CallOutputTracer callOutputTracer = new(); + TransactionResult res = _processor.Execute(createTx, callOutputTracer); + BlockAccessList bal = worldState.GeneratedBlockAccessList; + UInt256 gasUsed = new((ulong)callOutputTracer.GasSpent); + + UInt256 newBalance = _accountBalance - gasUsed; + if (!revert) + { + newBalance -= value; + } + AccountChanges accountChangesA = Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithBalanceChanges([new(0, newBalance)]) + .WithNonceChanges([new(0, 1)]).TestObject; + AccountChanges accountChangesZero = Build.An.AccountChanges.WithBalanceChanges([new(0, gasUsed)]).TestObject; + + using (Assert.EnterMultipleScope()) + { + Assert.That(res.TransactionExecuted); + Assert.That(bal.GetAccountChanges(TestItem.AddressA), Is.EqualTo(accountChangesA)); + Assert.That(bal.GetAccountChanges(Address.Zero), Is.EqualTo(accountChangesZero)); + Assert.That(bal.AccountChanges.Count(), Is.EqualTo(expected.Count() + 2)); + } + + foreach (AccountChanges expectedAccountChanges in expected) + { + AccountChanges actual = bal.GetAccountChanges(expectedAccountChanges.Address); + Assert.That(actual, Is.EqualTo(expectedAccountChanges)); + } + } + + [TestCaseSource(nameof(ExceptionTestSource))] + public async Task Constructs_BAL_when_processing_code_exception( + IEnumerable expected, + byte[] code, + byte[]? extraCode, + long executionGas, + EvmExceptionType expectedException) + { + InitWorldState(TestState, extraCode); + ParallelWorldState worldState = TestState as ParallelWorldState; + worldState.TracingEnabled = true; + + Transaction templateTx = Build.A.Transaction + .WithCode(code) + .WithGasLimit(0) + .WithValue(_testAccountBalance) + .TestObject; + long intrinsicGas = IntrinsicGasCalculator.Calculate(templateTx, Amsterdam.Instance).MinimalGas; + long gasLimit = intrinsicGas + executionGas; + + Transaction createTx = Build.A.Transaction + .WithCode(code) + .WithGasLimit(gasLimit) + .WithValue(_testAccountBalance) + .SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + Block block = Build.A.Block.TestObject; + + _processor.SetBlockExecutionContext(new BlockExecutionContext(block.Header, Amsterdam.Instance)); + CallOutputTracer callOutputTracer = new(); + TransactionResult res = _processor.Execute(createTx, callOutputTracer); + BlockAccessList bal = worldState.GeneratedBlockAccessList; + UInt256 gasUsed = new((ulong)callOutputTracer.GasSpent); + + AccountChanges accountChangesA = Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithBalanceChanges([new(0, _accountBalance - gasUsed)]) + .WithNonceChanges([new(0, 1)]).TestObject; + AccountChanges accountChangesZero = Build.An.AccountChanges.WithBalanceChanges([new(0, gasUsed)]).TestObject; + + using (Assert.EnterMultipleScope()) + { + Assert.That(res.EvmExceptionType, Is.EqualTo(expectedException)); + Assert.That(bal.GetAccountChanges(TestItem.AddressA), Is.EqualTo(accountChangesA)); + Assert.That(bal.GetAccountChanges(Address.Zero), Is.EqualTo(accountChangesZero)); + Assert.That(bal.AccountChanges.Count(), Is.EqualTo(expected.Count() + 2)); + } + + foreach (AccountChanges expectedAccountChanges in expected) + { + AccountChanges actual = bal.GetAccountChanges(expectedAccountChanges.Address); + Assert.That(actual, Is.EqualTo(expectedAccountChanges)); + } + } + + private void InitWorldState(IWorldState worldState, byte[]? extraCode = null) + { + worldState.CreateAccount(TestItem.AddressA, _accountBalance); + + worldState.CreateAccount(Eip2935Constants.BlockHashHistoryAddress, 0, Eip2935TestConstants.Nonce); + worldState.InsertCode(Eip2935Constants.BlockHashHistoryAddress, Eip2935TestConstants.CodeHash, Eip2935TestConstants.Code, SpecProvider.GenesisSpec); + + worldState.CreateAccount(Eip4788Constants.BeaconRootsAddress, 0, Eip4788TestConstants.Nonce); + worldState.InsertCode(Eip4788Constants.BeaconRootsAddress, Eip4788TestConstants.CodeHash, Eip4788TestConstants.Code, SpecProvider.GenesisSpec); + + worldState.CreateAccount(Eip7002Constants.WithdrawalRequestPredeployAddress, 0, Eip7002TestConstants.Nonce); + worldState.InsertCode(Eip7002Constants.WithdrawalRequestPredeployAddress, Eip7002TestConstants.CodeHash, Eip7002TestConstants.Code, SpecProvider.GenesisSpec); + + worldState.CreateAccount(Eip7251Constants.ConsolidationRequestPredeployAddress, 0, Eip7251TestConstants.Nonce); + worldState.InsertCode(Eip7251Constants.ConsolidationRequestPredeployAddress, Eip7251TestConstants.CodeHash, Eip7251TestConstants.Code, SpecProvider.GenesisSpec); + + worldState.CreateAccount(_delegationTargetAddress, 0); + worldState.InsertCode(_delegationTargetAddress, ValueKeccak.Compute(_delegatedCode), _delegatedCode, SpecProvider.GenesisSpec); + + worldState.CreateAccount(_callTargetAddress, 0); + if (extraCode is not null) + { + ValueHash256 codeHash = ValueKeccak.Compute(extraCode); + worldState.InsertCode(_callTargetAddress, codeHash, extraCode, SpecProvider.GenesisSpec); + } + else + { + byte[] delegationCode = [.. Eip7702Constants.DelegationHeader, .. _delegationTargetAddress.Bytes]; + worldState.InsertCode(_callTargetAddress, ValueKeccak.Compute(delegationCode), delegationCode, SpecProvider.GenesisSpec); + } + + worldState.Commit(SpecProvider.GenesisSpec); + worldState.CommitTree(0); + worldState.RecalculateStateRoot(); + } + + private static IEnumerable CodeTestSource + { + get + { + IEnumerable changes; + UInt256 slot = _delegationSlot; + byte[] code = Prepare.EvmCode + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + + AccountChanges readAccount = Build.An.AccountChanges + .WithAddress(_testAddress) + .WithStorageReads(slot) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .TestObject; + changes = [readAccount]; + yield return new TestCaseData(changes, code, null, false) { TestName = "storage_read" }; + + code = Prepare.EvmCode + .PushData(slot) + .PushData(slot) + .Op(Instruction.SSTORE) + .Done; + changes = [Build.An.AccountChanges + .WithAddress(_testAddress) + .WithStorageChanges(slot, [new(0, slot)]) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .TestObject]; + yield return new TestCaseData(changes, code, null, false) { TestName = "storage_write" }; + + code = Prepare.EvmCode + .PushData(slot) + .PushData(slot) + .Op(Instruction.SSTORE) + .PushData(0) + .PushData(slot) + .Op(Instruction.SSTORE) + .Done; + changes = [readAccount]; + yield return new TestCaseData(changes, code, null, false) { TestName = "storage_write_return_to_original" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.BALANCE) + .Done; + AccountChanges testAccount = Build.An.AccountChanges + .WithAddress(_testAddress) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .TestObject; + AccountChanges emptyBAccount = new(TestItem.AddressB); + changes = [testAccount, emptyBAccount]; + yield return new TestCaseData(changes, code, null, false) { TestName = "balance" }; + + code = Prepare.EvmCode + .PushData(0) + .PushData(0) + .PushData(0) + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODECOPY) + .Done; + changes = [testAccount, emptyBAccount]; + yield return new TestCaseData(changes, code, null, false) { TestName = "extcodecopy" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODEHASH) + .Done; + changes = [testAccount, emptyBAccount]; + yield return new TestCaseData(changes, code, null, false) { TestName = "extcodehash" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODESIZE) + .Done; + changes = [testAccount, emptyBAccount]; + yield return new TestCaseData(changes, code, null, false) { TestName = "extcodesize" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.SELFDESTRUCT) + .Done; + changes = [new(_testAddress), Build.An.AccountChanges.WithAddress(TestItem.AddressB).WithBalanceChanges([new(0, _testAccountBalance)]).TestObject]; + yield return new TestCaseData(changes, code, null, false) { TestName = "selfdestruct" }; + + code = Prepare.EvmCode + .PushData(2) + .PushData(slot) + .Op(Instruction.SSTORE) + .PushData(slot) + .Op(Instruction.SLOAD) + .Op(Instruction.POP) + .Create( + Prepare.EvmCode + .ForInitOf(Prepare.EvmCode.Op(Instruction.STOP).Done) + .Done, + 0) + .Op(Instruction.POP) + .CallWithValue(TestItem.AddressB, 20_000, 1) + .Op(Instruction.POP) + .PushData(0) + .PushData(0) + .Op(Instruction.REVERT) + .Done; + // revert should convert storage load to read, nonce and balance changes revert + changes = [Build.An.AccountChanges + .WithAddress(_testAddress) + .WithStorageReads(slot) + .TestObject]; + yield return new TestCaseData(changes, code, null, true) { TestName = "revert" }; + + UInt256 changedValue = 2; + byte[] revertToPreviousCode = Prepare.EvmCode + .PushData(0) + .PushData(slot) + .Op(Instruction.SSTORE) + .PushData(0) + .PushData(0) + .Op(Instruction.REVERT) + .Done; + code = Prepare.EvmCode + .PushData(changedValue) + .PushData(slot) + .Op(Instruction.SSTORE) + .DelegateCall(_callTargetAddress, 20_000) + .Done; + changes = [new(_callTargetAddress), Build.An.AccountChanges + .WithAddress(_testAddress) + .WithStorageChanges(slot, [new(0, changedValue)]) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .TestObject]; + yield return new TestCaseData(changes, code, revertToPreviousCode, false) + { TestName = "revert_with_return_to_original" }; + + code = Prepare.EvmCode + .Call(_callTargetAddress, 20_000) + .Done; + changes = [ + testAccount, + Build.An.AccountChanges + .WithAddress(_callTargetAddress) + .WithStorageReads(_delegationSlot) + .TestObject, + new AccountChanges(_delegationTargetAddress) + ]; + yield return new TestCaseData(changes, code, null, false) { TestName = "delegated_account" }; + + UInt256 callValue = 10_000; + byte[] callTargetCode = Prepare.EvmCode + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + code = Prepare.EvmCode + .CallWithValue(_callTargetAddress, 20_000, callValue) + .Done; + changes = [ + Build.An.AccountChanges + .WithAddress(_testAddress) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance - callValue)]) + .TestObject, + Build.An.AccountChanges + .WithAddress(_callTargetAddress) + .WithStorageReads(slot) + .WithBalanceChanges([new(0, callValue)]) + .TestObject + ]; + yield return new TestCaseData(changes, code, callTargetCode, false) { TestName = "call" }; + + byte[] returnValueCode = Prepare.EvmCode + .PushData(0) + .PushData(0) + .PushData(0) + .PushData(0) + .Op(Instruction.CALLVALUE) + .Op(Instruction.CALLER) + .PushData(20_000) + .Op(Instruction.CALL) + .Done; + code = Prepare.EvmCode + .CallWithValue(_callTargetAddress, 20_000, 1.GWei) + .Done; + changes = [testAccount, new(_callTargetAddress)]; + yield return new TestCaseData(changes, code, returnValueCode, false) { TestName = "balance_change_return_to_original" }; + + code = Prepare.EvmCode + .CallCode(_callTargetAddress, 20_000) + .Done; + changes = [ + Build.An.AccountChanges + .WithAddress(_testAddress) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .WithStorageReads(slot) + .TestObject, + new AccountChanges(_callTargetAddress) + ]; + // storage read happens in test account context + yield return new TestCaseData(changes, code, callTargetCode, false) { TestName = "callcode" }; + + code = Prepare.EvmCode + .DelegateCall(_callTargetAddress, 20_000) + .Done; + changes = [ + Build.An.AccountChanges + .WithAddress(_testAddress) + .WithNonceChanges([new(0, 1)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .WithStorageReads(slot) + .TestObject, + new AccountChanges(_callTargetAddress) + ]; + // storage read happens in test account context + yield return new TestCaseData(changes, code, callTargetCode, false) { TestName = "delegatecall" }; + + code = Prepare.EvmCode + .StaticCall(_callTargetAddress, 20_000) + .Done; + changes = [ + testAccount, + Build.An.AccountChanges + .WithAddress(_callTargetAddress) + .WithStorageReads(slot) + .TestObject + ]; + yield return new TestCaseData(changes, code, callTargetCode, false) { TestName = "staticcall" }; + + byte[] createdRuntimeCode = Prepare.EvmCode + .Op(Instruction.STOP) + .Done; + byte[] createInitCode = Prepare.EvmCode + .PushData(slot) + .Op(Instruction.SLOAD) + .ForInitOf(createdRuntimeCode) + .Done; + Address createdAddress = ContractAddress.From(_testAddress, 1); + code = Prepare.EvmCode + .Create(createInitCode, 0) + .Done; + changes = [ + Build.An.AccountChanges + .WithAddress(_testAddress) + .WithNonceChanges([new(0, 2)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .TestObject, + Build.An.AccountChanges + .WithAddress(createdAddress) + .WithNonceChanges([new(0, 1)]) + .WithStorageReads(slot) + .WithCodeChanges([new(0, createdRuntimeCode)]) + .TestObject + ]; + yield return new TestCaseData(changes, code, null, false) { TestName = "create" }; + + byte[] create2Salt = new byte[32]; + create2Salt[^1] = 1; + Address createdAddress2 = ContractAddress.From(_testAddress, create2Salt, createInitCode); + code = Prepare.EvmCode + .Create2(createInitCode, create2Salt, 0) + .Done; + changes = [ + Build.An.AccountChanges + .WithAddress(_testAddress) + .WithNonceChanges([new(0, 2)]) + .WithBalanceChanges([new(0, _testAccountBalance)]) + .TestObject, + Build.An.AccountChanges + .WithAddress(createdAddress2) + .WithNonceChanges([new(0, 1)]) + .WithStorageReads(slot) + .WithCodeChanges([new(0, createdRuntimeCode)]) + .TestObject + ]; + yield return new TestCaseData(changes, code, null, false) { TestName = "create2" }; + + code = Prepare.EvmCode + .CallWithInput(PrecompiledAddresses.Identity, 20_000, [1, 2, 3, 4]) + .Done; + changes = [testAccount, new(PrecompiledAddresses.Identity)]; + yield return new TestCaseData(changes, code, null, false) { TestName = "precompile" }; + + code = Prepare.EvmCode + .Call(TestItem.AddressB, 20_000) + .Done; + changes = [testAccount, new(TestItem.AddressB)]; + yield return new TestCaseData(changes, code, null, false) { TestName = "zero_value_call" }; + } + } + + private static IEnumerable ExceptionTestSource + { + get + { + IEnumerable changes; + byte[] code; + UInt256 slot = _delegationSlot; + AccountChanges testAccount = new(_testAddress); + AccountChanges addressB = new(TestItem.AddressB); + AccountChanges callTarget = new(_callTargetAddress); + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.BALANCE) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.ColdAccountAccess - 1, EvmExceptionType.OutOfGas) + { TestName = "balance_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.BALANCE) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, addressB]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "balance_oog_post_state_access" }; + + code = Prepare.EvmCode + .Op(Instruction.SELFBALANCE) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.SelfBalance, EvmExceptionType.OutOfGas) + { TestName = "selfbalance_oog_post_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODESIZE) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.ColdAccountAccess - 1, EvmExceptionType.OutOfGas) + { TestName = "extcodesize_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODESIZE) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, addressB]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "extcodesize_oog_post_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODEHASH) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.ColdAccountAccess - 1, EvmExceptionType.OutOfGas) + { TestName = "extcodehash_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODEHASH) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, addressB]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "extcodehash_oog_post_state_access" }; + + code = Prepare.EvmCode + .PushData(32) + .PushData(0) + .PushData(0) + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODECOPY) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.Memory, + EvmExceptionType.OutOfGas) + { TestName = "extcodecopy_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(32) + .PushData(0) + .PushData(0) + .PushData(TestItem.AddressB) + .Op(Instruction.EXTCODECOPY) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, addressB]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.ColdAccountAccess + GasCostOf.Memory * 2 + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "extcodecopy_oog_post_state_access" }; + + byte[] callTargetCode = Prepare.EvmCode.Op(Instruction.STOP).Done; + code = Prepare.EvmCode + .Call(_callTargetAddress, 0) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess - 1, + EvmExceptionType.OutOfGas) + { TestName = "call_oog_pre_state_access" }; + + code = Prepare.EvmCode + .Call(_callTargetAddress, 0) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, callTarget]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "call_oog_post_state_access" }; + + code = Prepare.EvmCode + .CallCode(_callTargetAddress, 0) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess - 1, + EvmExceptionType.OutOfGas) + { TestName = "callcode_oog_pre_state_access" }; + + code = Prepare.EvmCode + .CallCode(_callTargetAddress, 0) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, callTarget]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "callcode_oog_post_state_access" }; + + code = Prepare.EvmCode + .DelegateCall(_callTargetAddress, 0) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess - 1, + EvmExceptionType.OutOfGas) + { TestName = "delegatecall_oog_pre_state_access" }; + + code = Prepare.EvmCode + .DelegateCall(_callTargetAddress, 0) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, callTarget]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "delegatecall_oog_post_state_access" }; + + code = Prepare.EvmCode + .StaticCall(_callTargetAddress, 0) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess - 1, + EvmExceptionType.OutOfGas) + { TestName = "staticcall_oog_pre_state_access" }; + + code = Prepare.EvmCode + .StaticCall(_callTargetAddress, 0) + .PushData(slot) + .Op(Instruction.SLOAD) + .Done; + changes = [testAccount, callTarget]; + yield return new TestCaseData( + changes, + code, + callTargetCode, + GasCostOf.ColdAccountAccess + GasCostOf.ColdSLoad - 1, + EvmExceptionType.OutOfGas) + { TestName = "staticcall_oog_post_state_access" }; + + code = Prepare.EvmCode + .PushData(32) + .PushData(0) + .PushData(0) + .Op(Instruction.CREATE) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.Create + GasCostOf.InitCodeWord + GasCostOf.Memory - 1, + EvmExceptionType.OutOfGas) + { TestName = "create_oog_pre_state_access" }; + + byte[] create2Salt = new byte[32]; + create2Salt[^1] = 1; + code = Prepare.EvmCode + .PushData(32) + .PushData(0) + .PushData(0) + .PushData(create2Salt) + .Op(Instruction.CREATE2) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.Create + GasCostOf.InitCodeWord + GasCostOf.Sha3Word + GasCostOf.Memory - 1, + EvmExceptionType.OutOfGas) + { TestName = "create2_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(slot) + .Op(Instruction.SLOAD) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.ColdSLoad - 1, EvmExceptionType.OutOfGas) + { TestName = "sload_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(slot) + .Op(Instruction.SLOAD) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [Build.An.AccountChanges.WithAddress(_testAddress).WithStorageReads(slot).TestObject]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.ColdSLoad + GasCostOf.VeryLow + GasCostOf.Memory - 1, + EvmExceptionType.OutOfGas) + { TestName = "sload_oog_post_state_access" }; + + code = Prepare.EvmCode + .PushData(6) + .PushData(slot) + .Op(Instruction.SSTORE) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.CallStipend - 1, + EvmExceptionType.OutOfGas) + { TestName = "sstore_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(6) + .PushData(slot) + .Op(Instruction.SSTORE) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [Build.An.AccountChanges.WithAddress(_testAddress).WithStorageReads(slot).TestObject]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.ColdSLoad + GasCostOf.SSet - 1, + EvmExceptionType.OutOfGas) + { TestName = "sstore_oog_post_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.SELFDESTRUCT) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.SelfDestructEip150 + GasCostOf.ColdAccountAccess + GasCostOf.VeryLow - 1, + EvmExceptionType.OutOfGas) + { TestName = "selfdestruct_oog_pre_state_access" }; + + code = Prepare.EvmCode + .PushData(TestItem.AddressB) + .Op(Instruction.SELFDESTRUCT) + .PushData(0) + .PushData(0) + .Op(Instruction.MSTORE) + .Done; + changes = [testAccount, addressB]; + yield return new TestCaseData( + changes, + code, + null, + GasCostOf.SelfDestructEip150 + GasCostOf.ColdAccountAccess + GasCostOf.VeryLow, + EvmExceptionType.OutOfGas) + { TestName = "selfdestruct_oog_post_state_access" }; + + code = Prepare.EvmCode.Op(Instruction.BALANCE).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "balance_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.EXTCODESIZE).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "extcodesize_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.EXTCODEHASH).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "extcodehash_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.EXTCODECOPY).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "extcodecopy_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.SLOAD).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "sload_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.SSTORE).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.CallStipend + 1, EvmExceptionType.StackUnderflow) + { TestName = "sstore_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.CALL).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "call_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.CALLCODE).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "callcode_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.DELEGATECALL).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "delegatecall_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.STATICCALL).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "staticcall_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.CREATE).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "create_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.CREATE2).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.VeryLow, EvmExceptionType.StackUnderflow) + { TestName = "create2_stack_underflow" }; + + code = Prepare.EvmCode.Op(Instruction.SELFDESTRUCT).Done; + changes = [testAccount]; + yield return new TestCaseData(changes, code, null, GasCostOf.SelfDestructEip150, EvmExceptionType.StackUnderflow) + { TestName = "selfdestruct_stack_underflow" }; + } + } +} diff --git a/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs b/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs index 7fba83d4ac4..e1fc2cba125 100644 --- a/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs @@ -10,7 +10,6 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Specs; using Nethermind.Evm.CodeAnalysis; -using Nethermind.Evm.EvmObjectFormat; using Nethermind.Evm.State; namespace Nethermind.Evm; @@ -46,6 +45,7 @@ public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IRe delegationAddress = null; if (vmSpec.IsPrecompile(codeSource)) { + _worldState.AddAccountRead(codeSource); return _localPrecompiles[codeSource]; } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 3f1de4e1bfc..9415278537f 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -28,8 +28,8 @@ public struct EthereumGasPolicy : IGasPolicy public static void Consume(ref EthereumGasPolicy gas, long cost) => gas.Value -= cost; - public static void ConsumeSelfDestructGas(ref EthereumGasPolicy gas) - => Consume(ref gas, GasCostOf.SelfDestructEip150); + public static bool ConsumeSelfDestructGas(ref EthereumGasPolicy gas) + => UpdateGas(ref gas, GasCostOf.SelfDestructEip150); /// /// Consume gas for code deposit. For standard Ethereum, this is equivalent to Consume. diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index a0e6809c894..e3e783a719d 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -47,7 +47,7 @@ public interface IGasPolicy where TSelf : struct, IGasPolicy /// Consume gas for SelfDestruct operation. /// /// The gas state to update. - static abstract void ConsumeSelfDestructGas(ref TSelf gas); + static abstract bool ConsumeSelfDestructGas(ref TSelf gas); /// /// Consume gas for code deposit during CREATE/CREATE2. diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index b556ca5efd8..b78d46e543f 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -132,13 +132,9 @@ public static EvmExceptionType InstructionCall( if (!TGasPolicy.UpdateMemoryCost(ref gas, in a, result, vm.VmState)) goto OutOfGas; + vm.WorldState.AddAccountRead(address); + CodeInfo codeInfo = vm.CodeInfoRepository .GetCachedCodeInfo(address, followDelegation: false, spec, out _); @@ -196,6 +199,10 @@ public static EvmExceptionType InstructionExtCodeCopy( vm.TxTracer.ReportMemoryChange(a, in slice); } } + else + { + vm.WorldState.AddAccountRead(address); + } return EvmExceptionType.None; // Jump forward to be unpredicted by the branch predictor. @@ -241,6 +248,8 @@ public static EvmExceptionType InstructionExtCodeSize( if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address)) goto OutOfGas; + vm.WorldState.AddAccountRead(address); + // Attempt a peephole optimization when tracing is not active and code is available. ReadOnlySpan codeSection = vm.VmState.Env.CodeInfo.CodeSpan; if (!TTracingInst.IsActive && programCounter < codeSection.Length) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index cfb7ad7a384..c2a370e2159 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -219,7 +219,8 @@ private static EvmExceptionType InstructionSelfDestruct(VirtualMachi // If Shanghai DDoS protection is active, charge the appropriate gas cost. if (spec.UseShanghaiDDosProtection) { - TGasPolicy.ConsumeSelfDestructGas(ref gas); + if (!TGasPolicy.ConsumeSelfDestructGas(ref gas)) + goto OutOfGas; } // Pop the inheritor address from the stack; signal underflow if missing. diff --git a/src/Nethermind/Nethermind.Evm/Nethermind.Evm.csproj b/src/Nethermind/Nethermind.Evm/Nethermind.Evm.csproj index 968541ac1aa..37f9f5a0895 100644 --- a/src/Nethermind/Nethermind.Evm/Nethermind.Evm.csproj +++ b/src/Nethermind/Nethermind.Evm/Nethermind.Evm.csproj @@ -9,5 +9,6 @@ + diff --git a/src/Nethermind/Nethermind.Evm/State/IBlockAccessListBuilder.cs b/src/Nethermind/Nethermind.Evm/State/IBlockAccessListBuilder.cs new file mode 100644 index 00000000000..aa0fc2a491a --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/State/IBlockAccessListBuilder.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Specs; + +namespace Nethermind.Evm.State; + +public interface IBlockAccessListBuilder +{ + public bool TracingEnabled { get; set; } + public BlockAccessList GeneratedBlockAccessList { get; set; } + public void AddAccountRead(Address address); + public void LoadSuggestedBlockAccessList(BlockAccessList suggested, long gasUsed); + public long GasUsed(); + public void ValidateBlockAccessList(BlockHeader block, ushort index, long gasRemaining); + public void SetBlockAccessList(Block block, IReleaseSpec spec); +} diff --git a/src/Nethermind/Nethermind.State/IPreBlockCaches.cs b/src/Nethermind/Nethermind.Evm/State/IPreBlockCaches.cs similarity index 86% rename from src/Nethermind/Nethermind.State/IPreBlockCaches.cs rename to src/Nethermind/Nethermind.Evm/State/IPreBlockCaches.cs index 1526663b148..2e29278e9c6 100644 --- a/src/Nethermind/Nethermind.State/IPreBlockCaches.cs +++ b/src/Nethermind/Nethermind.Evm/State/IPreBlockCaches.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.State; +namespace Nethermind.Evm.State; public interface IPreBlockCaches { diff --git a/src/Nethermind/Nethermind.Evm/State/IWorldState.cs b/src/Nethermind/Nethermind.Evm/State/IWorldState.cs index 7e1bcd71fa4..5e41e159898 100644 --- a/src/Nethermind/Nethermind.Evm/State/IWorldState.cs +++ b/src/Nethermind/Nethermind.Evm/State/IWorldState.cs @@ -113,18 +113,16 @@ public interface IWorldState : IJournal, IReadOnlyStateProvider /// Note: This is different from whether the account has its hash updated bool InsertCode(Address address, in ValueHash256 codeHash, ReadOnlyMemory code, IReleaseSpec spec, bool isGenesis = false); - void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec); + void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance); - bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec); + bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance); - void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec); + void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance); - void IncrementNonce(Address address, UInt256 delta); + void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce); void DecrementNonce(Address address, UInt256 delta); - void IncrementNonce(Address address) => IncrementNonce(address, UInt256.One); - void DecrementNonce(Address address) => DecrementNonce(address, UInt256.One); void SetNonce(Address address, in UInt256 nonce); diff --git a/src/Nethermind/Nethermind.Evm/State/IWorldStateExtensions.cs b/src/Nethermind/Nethermind.Evm/State/IWorldStateExtensions.cs index c0e73a6a311..19bdad43a59 100644 --- a/src/Nethermind/Nethermind.Evm/State/IWorldStateExtensions.cs +++ b/src/Nethermind/Nethermind.Evm/State/IWorldStateExtensions.cs @@ -6,6 +6,7 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Specs; using Nethermind.Evm.Tracing.State; +using Nethermind.Int256; namespace Nethermind.Evm.State; @@ -19,7 +20,23 @@ public static void InsertCode(this IWorldState worldState, Address address, Read } public static void Commit(this IWorldState worldState, IReleaseSpec releaseSpec, bool isGenesis = false, bool commitRoots = true) - { - worldState.Commit(releaseSpec, NullStateTracer.Instance, isGenesis, commitRoots); - } + => worldState.Commit(releaseSpec, NullStateTracer.Instance, isGenesis, commitRoots); + + public static void AddToBalance(this IWorldState worldState, Address address, in UInt256 balanceChange, IReleaseSpec spec) + => worldState.AddToBalance(address, balanceChange, spec, out _); + + public static bool AddToBalanceAndCreateIfNotExists(this IWorldState worldState, Address address, in UInt256 balanceChange, IReleaseSpec spec) + => worldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out _); + + public static void SubtractFromBalance(this IWorldState worldState, Address address, in UInt256 balanceChange, IReleaseSpec spec) + => worldState.SubtractFromBalance(address, balanceChange, spec, out _); + + public static void IncrementNonce(this IWorldState worldState, Address address, UInt256 delta) + => worldState.IncrementNonce(address, delta, out _); + + public static void IncrementNonce(this IWorldState worldState, Address address) + => worldState.IncrementNonce(address, UInt256.One); + + public static void AddAccountRead(this IWorldState worldState, Address address) + => (worldState as IBlockAccessListBuilder)?.AddAccountRead(address); } diff --git a/src/Nethermind/Nethermind.State/PreBlockCaches.cs b/src/Nethermind/Nethermind.Evm/State/PreBlockCaches.cs similarity index 98% rename from src/Nethermind/Nethermind.State/PreBlockCaches.cs rename to src/Nethermind/Nethermind.Evm/State/PreBlockCaches.cs index e7f8bf29073..13d35af0501 100644 --- a/src/Nethermind/Nethermind.State/PreBlockCaches.cs +++ b/src/Nethermind/Nethermind.Evm/State/PreBlockCaches.cs @@ -10,7 +10,7 @@ using CollectionExtensions = Nethermind.Core.Collections.CollectionExtensions; -namespace Nethermind.State; +namespace Nethermind.Evm.State; public class PreBlockCaches { diff --git a/src/Nethermind/Nethermind.Evm/State/Snapshot.cs b/src/Nethermind/Nethermind.Evm/State/Snapshot.cs index 7f5a123bc6d..50234115347 100644 --- a/src/Nethermind/Nethermind.Evm/State/Snapshot.cs +++ b/src/Nethermind/Nethermind.Evm/State/Snapshot.cs @@ -7,9 +7,23 @@ namespace Nethermind.Evm.State; /// Stores state and storage snapshots (as the change index that we can revert to) /// At the beginning and after each commit the snapshot is set to the value of EmptyPosition /// -public readonly struct Snapshot(in Snapshot.Storage storageSnapshot, int stateSnapshot) +public readonly struct Snapshot { - public static readonly Snapshot Empty = new(Storage.Empty, EmptyPosition); + public Snapshot(in Storage storageSnapshot, int stateSnapshot, int balSnapshot) + { + StorageSnapshot = storageSnapshot; + StateSnapshot = stateSnapshot; + BlockAccessListSnapshot = balSnapshot; + } + + public Snapshot(in Storage storageSnapshot, int stateSnapshot) + { + StorageSnapshot = storageSnapshot; + StateSnapshot = stateSnapshot; + BlockAccessListSnapshot = EmptyPosition; + } + + public static readonly Snapshot Empty = new(Storage.Empty, EmptyPosition, EmptyPosition); /// /// Tracks snapshot positions for Persistent and Transient storage @@ -22,8 +36,9 @@ public readonly struct Storage(int storageSnapshot, int transientStorageSnapshot public int TransientStorageSnapshot { get; } = transientStorageSnapshot; } - public Storage StorageSnapshot { get; } = storageSnapshot; - public int StateSnapshot { get; } = stateSnapshot; + public Storage StorageSnapshot { get; } + public int StateSnapshot { get; } + public int BlockAccessListSnapshot { get; } public const int EmptyPosition = -1; } diff --git a/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs b/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs new file mode 100644 index 00000000000..8787b4f6e5d --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Core.Eip2930; +using Nethermind.Core.Specs; +using Nethermind.Evm.Tracing.State; +using Nethermind.Int256; + +namespace Nethermind.Evm.State; + +public class WrappedWorldState(IWorldState innerWorldState) : IWorldState +{ + protected IWorldState _innerWorldState = innerWorldState; + public bool IsInScope => _innerWorldState.IsInScope; + public IWorldStateScopeProvider ScopeProvider => _innerWorldState.ScopeProvider; + + public Hash256 StateRoot => _innerWorldState.StateRoot; + + public virtual bool AccountExists(Address address) + => _innerWorldState.AccountExists(address); + + public virtual void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => _innerWorldState.AddToBalance(address, balanceChange, spec); + + public virtual void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + => _innerWorldState.AddToBalance(address, balanceChange, spec, out oldBalance); + + public virtual bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => _innerWorldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec); + + public virtual bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + => _innerWorldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out oldBalance); + + public virtual IDisposable BeginScope(BlockHeader? baseBlock) + => _innerWorldState.BeginScope(baseBlock); + + public virtual void ClearStorage(Address address) + => _innerWorldState.ClearStorage(address); + + public virtual void Commit(IReleaseSpec releaseSpec, IWorldStateTracer tracer, bool isGenesis = false, bool commitRoots = true) + => _innerWorldState.Commit(releaseSpec, tracer, isGenesis, commitRoots); + + public virtual void CommitTree(long blockNumber) + => _innerWorldState.CommitTree(blockNumber); + + public virtual void CreateAccount(Address address, in UInt256 balance, in UInt256 nonce = default) + => _innerWorldState.CreateAccount(address, balance, nonce); + + public virtual void CreateAccountIfNotExists(Address address, in UInt256 balance, in UInt256 nonce = default) + => _innerWorldState.CreateAccountIfNotExists(address, balance, nonce); + + public virtual void CreateEmptyAccountIfDeleted(Address address) => + _innerWorldState.CreateEmptyAccountIfDeleted(address); + + public virtual void DecrementNonce(Address address, UInt256 delta) + => _innerWorldState.DecrementNonce(address, delta); + + public virtual void DeleteAccount(Address address) + => _innerWorldState.DeleteAccount(address); + + public virtual ReadOnlySpan Get(in StorageCell storageCell) + => _innerWorldState.Get(storageCell); + + public ArrayPoolList? GetAccountChanges() + => _innerWorldState.GetAccountChanges(); + + public virtual ref readonly UInt256 GetBalance(Address address) + => ref _innerWorldState.GetBalance(address); + + public virtual byte[]? GetCode(Address address) + => _innerWorldState.GetCode(address); + + public byte[]? GetCode(in ValueHash256 codeHash) + => _innerWorldState.GetCode(codeHash); + + public virtual ref readonly ValueHash256 GetCodeHash(Address address) + => ref _innerWorldState.GetCodeHash(address); + + public byte[] GetOriginal(in StorageCell storageCell) + => _innerWorldState.GetOriginal(storageCell); + + public ReadOnlySpan GetTransientState(in StorageCell storageCell) + => _innerWorldState.GetTransientState(storageCell); + + public bool HasStateForBlock(BlockHeader? baseBlock) + => _innerWorldState.HasStateForBlock(baseBlock); + + public virtual void IncrementNonce(Address address, UInt256 delta) + => _innerWorldState.IncrementNonce(address, delta); + + public virtual void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) + => _innerWorldState.IncrementNonce(address, delta, out oldNonce); + + public virtual bool InsertCode(Address address, in ValueHash256 codeHash, ReadOnlyMemory code, IReleaseSpec spec, bool isGenesis = false) + => _innerWorldState.InsertCode(address, codeHash, code, spec, isGenesis); + + public virtual bool IsContract(Address address) + => _innerWorldState.IsContract(address); + + public virtual bool IsDeadAccount(Address address) + => _innerWorldState.IsDeadAccount(address); + + public virtual void RecalculateStateRoot() + => _innerWorldState.RecalculateStateRoot(); + + public virtual void Reset(bool resetBlockChanges = true) + => _innerWorldState.Reset(resetBlockChanges); + + public void ResetTransient() + => _innerWorldState.ResetTransient(); + + public virtual void Restore(Snapshot snapshot) + => _innerWorldState.Restore(snapshot); + + public virtual void Set(in StorageCell storageCell, byte[] newValue) + => _innerWorldState.Set(storageCell, newValue); + + public virtual void SetNonce(Address address, in UInt256 nonce) + => _innerWorldState.SetNonce(address, nonce); + + public void SetTransientState(in StorageCell storageCell, byte[] newValue) + => _innerWorldState.SetTransientState(storageCell, newValue); + + public virtual void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => _innerWorldState.SubtractFromBalance(address, balanceChange, spec); + + public virtual void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + => _innerWorldState.SubtractFromBalance(address, balanceChange, spec, out oldBalance); + + public virtual Snapshot TakeSnapshot(bool newTransactionStart = false) + => _innerWorldState.TakeSnapshot(newTransactionStart); + + public virtual bool TryGetAccount(Address address, out AccountStruct account) + => _innerWorldState.TryGetAccount(address, out account); + + public void WarmUp(AccessList? accessList) + => _innerWorldState.WarmUp(accessList); + + public void WarmUp(Address address) + => _innerWorldState.WarmUp(address); +} diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 2705c6a119d..f3f40468a98 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -66,6 +66,7 @@ public abstract class TransactionProcessorBase : ITransactionProcess private SystemTransactionProcessor? _systemTransactionProcessor; private readonly ITransactionProcessor.IBlobBaseFeeCalculator _blobBaseFeeCalculator; private readonly ILogManager _logManager; + private readonly IBlockAccessListBuilder? _balBuilder; [Flags] protected enum ExecutionOptions @@ -126,6 +127,7 @@ protected TransactionProcessorBase( WorldState = worldState; VirtualMachine = virtualMachine; _codeInfoRepository = codeInfoRepository; + _balBuilder = worldState as IBlockAccessListBuilder; _blobBaseFeeCalculator = blobBaseFeeCalculator; Ecdsa = new EthereumEcdsa(specProvider.ChainId); @@ -303,7 +305,8 @@ private int ProcessDelegations(Transaction tx, IReleaseSpec spec, in StackAccess { Address authority = (authTuple.Authority ??= Ecdsa.RecoverAddress(authTuple))!; - if (!IsValidForExecution(authTuple, accessTracker, out string? error)) + AuthorizationTupleResult authorizationResult = IsValidForExecution(authTuple, accessTracker, spec, out string? error); + if (authorizationResult != AuthorizationTupleResult.Valid) { if (Logger.IsDebug) Logger.Debug($"Delegation {authTuple} is invalid with error: {error}"); } @@ -324,52 +327,63 @@ private int ProcessDelegations(Transaction tx, IReleaseSpec spec, in StackAccess } return refunds; + } - bool IsValidForExecution( - AuthorizationTuple authorizationTuple, - StackAccessTracker accessTracker, - [NotNullWhen(false)] out string? error) - { - if (authorizationTuple.ChainId != 0 && SpecProvider.ChainId != authorizationTuple.ChainId) - { - error = $"Chain id ({authorizationTuple.ChainId}) does not match."; - return false; - } + private enum AuthorizationTupleResult + { + Valid, + IncorrectNonce, + InvalidNonce, + InvalidChainId, + InvalidSignature, + InvalidAsCodeDeployed + } - if (authorizationTuple.Nonce == ulong.MaxValue) - { - error = $"Nonce ({authorizationTuple.Nonce}) must be less than 2**64 - 1."; - return false; - } + private AuthorizationTupleResult IsValidForExecution( + AuthorizationTuple authorizationTuple, + in StackAccessTracker accessTracker, + IReleaseSpec spec, + [NotNullWhen(false)] out string? error) + { + if (authorizationTuple.ChainId != 0 && SpecProvider.ChainId != authorizationTuple.ChainId) + { + error = $"Chain id ({authorizationTuple.ChainId}) does not match."; + return AuthorizationTupleResult.InvalidChainId; + } - UInt256 s = new(authorizationTuple.AuthoritySignature.SAsSpan, isBigEndian: true); - if (authorizationTuple.Authority is null - || s > Secp256K1Curve.HalfN - //V minus the offset can only be 1 or 0 since eip-155 does not apply to Setcode signatures - || authorizationTuple.AuthoritySignature.V - Signature.VOffset > 1) - { - error = "Bad signature."; - return false; - } + if (authorizationTuple.Nonce == ulong.MaxValue) + { + error = $"Nonce ({authorizationTuple.Nonce}) must be less than 2**64 - 1."; + return AuthorizationTupleResult.InvalidNonce; + } - accessTracker.WarmUp(authorizationTuple.Authority); + UInt256 s = new(authorizationTuple.AuthoritySignature.SAsSpan, isBigEndian: true); + if (authorizationTuple.Authority is null + || s > Secp256K1Curve.HalfN + //V minus the offset can only be 1 or 0 since eip-155 does not apply to Setcode signatures + || authorizationTuple.AuthoritySignature.V - Signature.VOffset > 1) + { + error = "Bad signature."; + return AuthorizationTupleResult.InvalidSignature; + } - if (WorldState.HasCode(authorizationTuple.Authority) && !_codeInfoRepository.TryGetDelegation(authorizationTuple.Authority, spec, out _)) - { - error = $"Authority ({authorizationTuple.Authority}) has code deployed."; - return false; - } + accessTracker.WarmUp(authorizationTuple.Authority); - UInt256 authNonce = WorldState.GetNonce(authorizationTuple.Authority); - if (authNonce != authorizationTuple.Nonce) - { - error = $"Skipping tuple in authorization_list because nonce is set to {authorizationTuple.Nonce}, but authority ({authorizationTuple.Authority}) has {authNonce}."; - return false; - } + if (WorldState.HasCode(authorizationTuple.Authority) && !_codeInfoRepository.TryGetDelegation(authorizationTuple.Authority, spec, out _)) + { + error = $"Authority ({authorizationTuple.Authority}) has code deployed."; + return AuthorizationTupleResult.InvalidAsCodeDeployed; + } - error = null; - return true; + UInt256 authNonce = WorldState.GetNonce(authorizationTuple.Authority); + if (authNonce != authorizationTuple.Nonce) + { + error = $"Skipping tuple in authorization_list because nonce is set to {authorizationTuple.Nonce}, but authority ({authorizationTuple.Authority}) has {authNonce}."; + return AuthorizationTupleResult.IncorrectNonce; } + + error = null; + return AuthorizationTupleResult.Valid; } protected virtual IReleaseSpec GetSpec(BlockHeader header) => VirtualMachine.BlockExecutionContext.Spec; diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 281ce7853d0..af12910203f 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -158,6 +158,9 @@ public TransactionSubstate ExecuteTransaction( _txTracer = txTracer; _worldState = worldState; + // Reset Parity touch bug state to prevent cross-transaction leakage. + _parityTouchBugAccount.ShouldDelete = false; + // Prepare the specification and opcode mapping based on the current block header. IReleaseSpec spec = BlockExecutionContext.Spec; PrepareOpcodes(spec); diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index 56a7e47b0ac..becaa4985ce 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -34,6 +34,8 @@ using Nethermind.Consensus; using Nethermind.Evm.State; using Nethermind.State.OverridableEnv; +using Nethermind.Blockchain.Headers; +using Nethermind.Core.BlockAccessLists; using Nethermind.Consensus.Stateless; namespace Nethermind.Facade @@ -52,6 +54,7 @@ public class BlockchainBridge( IEthereumEcdsa ecdsa, ITimestamper timestamper, ILogFinder logFinder, + IBlockAccessListStore balStore, ISpecProvider specProvider, IBlocksConfig blocksConfig, IMiningConfig miningConfig) @@ -424,6 +427,13 @@ public IEnumerable FindLogs(LogFilter filter, CancellationToken cance return logFinder.FindLogs(filter, cancellationToken); } + public BlockAccessList? GetBlockAccessList(Hash256 blockHash) + => balStore.Get(blockHash); + + // for testing + public void DeleteBlockAccessList(Hash256 blockHash) + => balStore.Delete(blockHash); + private static string? ConstructError(TransactionResult txResult, string? tracerError) { var error = txResult switch diff --git a/src/Nethermind/Nethermind.Facade/Eth/BlockForRpc.cs b/src/Nethermind/Nethermind.Facade/Eth/BlockForRpc.cs index d8f0b0b6531..3ce299d63ea 100644 --- a/src/Nethermind/Nethermind.Facade/Eth/BlockForRpc.cs +++ b/src/Nethermind/Nethermind.Facade/Eth/BlockForRpc.cs @@ -12,6 +12,7 @@ using System.Text.Json.Serialization; using System.Runtime.CompilerServices; using Nethermind.Facade.Eth.RpcTransaction; +using Nethermind.Core.BlockAccessLists; namespace Nethermind.Facade.Eth; @@ -90,6 +91,8 @@ public BlockForRpc(Block block, bool includeFullTransactionData, ISpecProvider s Withdrawals = block.Withdrawals; WithdrawalsRoot = block.Header.WithdrawalsRoot; RequestsHash = block.Header.RequestsHash; + BlockAccessListHash = block.Header.BlockAccessListHash; + BlockAccessList = block.BlockAccessList; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -147,6 +150,12 @@ public BlockForRpc(Block block, bool includeFullTransactionData, ISpecProvider s [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Hash256? RequestsHash { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Hash256? BlockAccessListHash { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BlockAccessList? BlockAccessList { get; set; } + private static object[] GetTransactionHashes(Transaction[] transactions) { if (transactions.Length == 0) return Array.Empty(); diff --git a/src/Nethermind/Nethermind.Facade/IBlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/IBlockchainBridge.cs index bcad2e48476..979d128b1f0 100644 --- a/src/Nethermind/Nethermind.Facade/IBlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/IBlockchainBridge.cs @@ -15,6 +15,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using Block = Nethermind.Core.Block; +using Nethermind.Core.BlockAccessLists; using Nethermind.Consensus.Stateless; namespace Nethermind.Facade @@ -53,5 +54,8 @@ public interface IBlockchainBridge : ILogFinder bool HasStateForBlock(BlockHeader? baseBlock); Witness GenerateExecutionWitness(BlockHeader parent, Block block); + + BlockAccessList? GetBlockAccessList(Hash256 blockHash); + void DeleteBlockAccessList(Hash256 blockHash); } } diff --git a/src/Nethermind/Nethermind.Facade/Simulate/SimulateReadOnlyBlocksProcessingEnvFactory.cs b/src/Nethermind/Nethermind.Facade/Simulate/SimulateReadOnlyBlocksProcessingEnvFactory.cs index 43593715743..92c709fd12c 100644 --- a/src/Nethermind/Nethermind.Facade/Simulate/SimulateReadOnlyBlocksProcessingEnvFactory.cs +++ b/src/Nethermind/Nethermind.Facade/Simulate/SimulateReadOnlyBlocksProcessingEnvFactory.cs @@ -39,7 +39,10 @@ public ISimulateReadOnlyBlocksProcessingEnv Create() IHeaderStore mainHeaderStore = new HeaderStore(editableDbProvider.HeadersDb, editableDbProvider.BlockNumbersDb); SimulateDictionaryHeaderStore tmpHeaderStore = new(mainHeaderStore); - BlockTree tempBlockTree = CreateTempBlockTree(editableDbProvider, specProvider, logManager, editableDbProvider, tmpHeaderStore); + + IBlockAccessListStore mainBalStore = new BlockAccessListStore(editableDbProvider.BlockAccessListDb); + + BlockTree tempBlockTree = CreateTempBlockTree(editableDbProvider, specProvider, logManager, editableDbProvider, tmpHeaderStore, mainBalStore); BlockTreeOverlay overrideBlockTree = new BlockTreeOverlay(baseBlockTree, tempBlockTree); ILifetimeScope envLifetimeScope = rootLifetimeScope.BeginLifetimeScope((builder) => builder @@ -71,7 +74,8 @@ private static BlockTree CreateTempBlockTree( ISpecProvider? specProvider, ILogManager logManager, IReadOnlyDbProvider editableDbProvider, - SimulateDictionaryHeaderStore tmpHeaderStore) + SimulateDictionaryHeaderStore tmpHeaderStore, + IBlockAccessListStore tmpBalStore) { IBlockStore mainBlockStore = new BlockStore(editableDbProvider.BlocksDb); const int badBlocksStored = 1; @@ -84,6 +88,7 @@ private static BlockTree CreateTempBlockTree( editableDbProvider.BlockInfosDb, editableDbProvider.MetadataDb, badBlockStore, + tmpBalStore, new ChainLevelInfoRepository(readOnlyDbProvider.BlockInfosDb), specProvider, NullBloomStorage.Instance, diff --git a/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs index d6b2dd8d9b8..29f308e0161 100644 --- a/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs +++ b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs @@ -91,7 +91,7 @@ private Block CreateBlock(EngineModuleTests.MergeTestBlockchain chain) Hash256 prevRandao = Keccak.Zero; - Hash256 expectedBlockHash = new("0xf96547f16f2d140931e4a026b15a5490538d5479518bbd46338bbada948b403a"); + Hash256 expectedBlockHash = new("0x5444e83525cda76e1de787ad6a2b81918efdaf0f7ab18b446cf59f57a9479d42"); string stateRoot = "0xa272b2f949e4a0e411c9b45542bd5d0ef3c311b5f26c4ed6b7a8d4f605a91154"; return new( diff --git a/src/Nethermind/Nethermind.Hive/HiveRunner.cs b/src/Nethermind/Nethermind.Hive/HiveRunner.cs index c24148b7045..87c02aae00d 100644 --- a/src/Nethermind/Nethermind.Hive/HiveRunner.cs +++ b/src/Nethermind/Nethermind.Hive/HiveRunner.cs @@ -85,11 +85,22 @@ private void ListEnvironmentVariables() "HIVE_CHAIN_ID", "HIVE_BOOTNODE", "HIVE_TESTNET", "HIVE_NODETYPE", "HIVE_FORK_HOMESTEAD", "HIVE_FORK_DAO_BLOCK", "HIVE_FORK_DAO_VOTE", "HIVE_FORK_TANGERINE", "HIVE_FORK_SPURIOUS", "HIVE_FORK_METROPOLIS", "HIVE_FORK_BYZANTIUM", "HIVE_FORK_CONSTANTINOPLE", "HIVE_FORK_PETERSBURG", - "HIVE_MINER", "HIVE_MINER_EXTRA", "HIVE_FORK_BERLIN", "HIVE_FORK_LONDON" + "HIVE_MINER", "HIVE_MINER_EXTRA", "HIVE_FORK_BERLIN", "HIVE_FORK_LONDON", + "HIVE_MERGE_BLOCK_ID", "HIVE_SHANGHAI_TIMESTAMP", "HIVE_CANCUN_TIMESTAMP", + "HIVE_CANCUN_BLOB_TARGET", "HIVE_CANCUN_BLOB_MAX", "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION", + "HIVE_PRAGUE_TIMESTAMP", "HIVE_PRAGUE_BLOB_TARGET", "HIVE_PRAGUE_BLOB_MAX", + "HIVE_PRAGUE_BLOB_BASE_FEE_UPDATE_FRACTION", "HIVE_OSAKA_TIMESTAMP", "HIVE_OSAKA_BLOB_TARGET", + "HIVE_OSAKA_BLOB_MAX", "HIVE_OSAKA_BLOB_BASE_FEE_UPDATE_FRACTION", "HIVE_AMSTERDAM_TIMESTAMP", + "HIVE_AMSTERDAM_BLOB_TARGET", "HIVE_AMSTERDAM_BLOB_MAX", "HIVE_AMSTERDAM_BLOB_BASE_FEE_UPDATE_FRACTION", + "HIVE_BPO1_TIMESTAMP", "HIVE_BPO1_BLOB_TARGET", "HIVE_BPO1_BLOB_MAX", "HIVE_BPO1_BLOB_BASE_FEE_UPDATE_FRACTION", + "HIVE_BPO2_TIMESTAMP", "HIVE_BPO2_BLOB_TARGET", "HIVE_BPO2_BLOB_MAX", "HIVE_BPO2_BLOB_BASE_FEE_UPDATE_FRACTION", + "HIVE_BPO3_TIMESTAMP", "HIVE_BPO3_BLOB_TARGET", "HIVE_BPO3_BLOB_MAX", "HIVE_BPO3_BLOB_BASE_FEE_UPDATE_FRACTION", + "HIVE_BPO4_TIMESTAMP", "HIVE_BPO4_BLOB_TARGET", "HIVE_BPO4_BLOB_MAX", "HIVE_BPO4_BLOB_BASE_FEE_UPDATE_FRACTION", + "HIVE_BPO5_TIMESTAMP", "HIVE_BPO5_BLOB_TARGET", "HIVE_BPO5_BLOB_MAX", "HIVE_BPO5_BLOB_BASE_FEE_UPDATE_FRACTION" }; foreach (string variableName in variableNames) { - if (_logger.IsInfo) _logger.Info($"{variableName}: {Environment.GetEnvironmentVariable(variableName)}"); + if (_logger.IsInfo) _logger.Info($"{variableName}: {Environment.GetEnvironmentVariable(variableName) ?? "null"}"); } } diff --git a/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs b/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs index 72f88faa2eb..0c712e4abcc 100644 --- a/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs @@ -52,6 +52,7 @@ protected override void Load(ContainerBuilder builder) .AddScoped() .AddSingleton() .AddScoped() + .AddDecorator() .AddScoped() .AddScoped() .AddSingleton() diff --git a/src/Nethermind/Nethermind.Init/Modules/BlockTreeModule.cs b/src/Nethermind/Nethermind.Init/Modules/BlockTreeModule.cs index 139dabadad2..56f4eb3edac 100644 --- a/src/Nethermind/Nethermind.Init/Modules/BlockTreeModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/BlockTreeModule.cs @@ -36,6 +36,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddSingleton() .AddSingleton(CreateBadBlockStore) + .AddSingleton(CreateBalStore) .AddSingleton() .AddSingleton() .AddSingleton((ecdsa, specProvider, receiptConfig) => @@ -83,8 +84,9 @@ private IFileStoreFactory CreateBloomStorageFileStoreFactory(IComponentContext c Bloom.ByteLength); } - private IBadBlockStore CreateBadBlockStore([KeyFilter(DbNames.BadBlocks)] IDb badBlockDb, IInitConfig initConfig) - { - return new BadBlockStore(badBlockDb, initConfig.BadBlocksStored ?? 100); - } + private IBadBlockStore CreateBadBlockStore([KeyFilter(DbNames.BadBlocks)] IDb badBlockDb, IInitConfig initConfig) => + new BadBlockStore(badBlockDb, initConfig.BadBlocksStored ?? 100); + + private IBlockAccessListStore CreateBalStore([KeyFilter(DbNames.BlockAccessLists)] IDb balDb) => + new BlockAccessListStore(balDb); } diff --git a/src/Nethermind/Nethermind.Init/Modules/DbModule.cs b/src/Nethermind/Nethermind.Init/Modules/DbModule.cs index 0caa67fe530..a9d7a764e25 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DbModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DbModule.cs @@ -66,6 +66,7 @@ protected override void Load(ContainerBuilder builder) .AddDatabase(DbNames.Headers) .AddDatabase(DbNames.BlockInfos) .AddDatabase(DbNames.Bloom) + .AddDatabase(DbNames.BlockAccessLists) .AddColumnDatabase(DbNames.Receipts) .AddColumnDatabase(DbNames.BlobTransactions) diff --git a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.cs b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.cs index 9726592562b..dbfaa44bb6b 100644 --- a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.cs +++ b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.cs @@ -1602,6 +1602,77 @@ public async Task Eth_sign_recover_correct_address() Assert.That(recovered, Is.EqualTo(new Address(keyAddress))); } + [Test] + public async Task Eth_get_block_access_list_by_hash() + { + using Context ctx = await Context.CreateWithAmsterdamEnabled(); + Hash256 blockHash = ctx.Test.BlockTree.Head!.Hash!; + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByHash", blockHash); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"result\":{\"accountChanges\":[{\"address\":\"0x00000961ef480eb55e80d19ad83579a64c007002\",\"storageChanges\":[],\"storageReads\":[{\"key\":\"0x0\"},{\"key\":\"0x1\"},{\"key\":\"0x2\"},{\"key\":\"0x3\"}],\"balanceChanges\":[],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x0000bbddc7ce488642fb579f8b00f3a590007251\",\"storageChanges\":[],\"storageReads\":[{\"key\":\"0x0\"},{\"key\":\"0x1\"},{\"key\":\"0x2\"},{\"key\":\"0x3\"}],\"balanceChanges\":[],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x0000f90827f1c53a10cb7a02335b175320002935\",\"storageChanges\":[{\"slot\":\"0x2\",\"changes\":{\"0\":{\"blockAccessIndex\":0,\"newValue\":\"0x179d875956f4d9ec8d54fc9f973a3ce5800b4f0a836a5f89cfc2842359d02b9e\"}}}],\"storageReads\":[],\"balanceChanges\":[],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x475674cb523a0a2736b7f7534390288fce16982c\",\"storageChanges\":[],\"storageReads\":[],\"balanceChanges\":[{\"blockAccessIndex\":1,\"postBalance\":\"0xa410\"},{\"blockAccessIndex\":2,\"postBalance\":\"0xf618\"}],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358\",\"storageChanges\":[],\"storageReads\":[],\"balanceChanges\":[{\"blockAccessIndex\":1,\"postBalance\":\"0x3635c9adc5dea00002\"},{\"blockAccessIndex\":2,\"postBalance\":\"0x3635c9adc5dea00003\"}],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0xb7705ae4c6f81b66cdb323c65f4e8133690fc099\",\"storageChanges\":[],\"storageReads\":[],\"balanceChanges\":[{\"blockAccessIndex\":1,\"postBalance\":\"0x3635c9adc5de9f5bee\"},{\"blockAccessIndex\":2,\"postBalance\":\"0x3635c9adc5de9f09e5\"}],\"nonceChanges\":[{\"blockAccessIndex\":1,\"newNonce\":\"0x2\"},{\"blockAccessIndex\":2,\"newNonce\":\"0x3\"}],\"codeChanges\":[]}]},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_hash_not_found() + { + using Context ctx = await Context.CreateWithAmsterdamEnabled(); + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByHash", Hash256.Zero); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Cannot return block access list, block not found.\"},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_hash_unavailable_before_fork() + { + using Context ctx = await Context.Create(); // Amsterdam disabled + Hash256 blockHash = ctx.Test.BlockTree.Head!.Hash!; + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByHash", blockHash); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":4445,\"message\":\"Cannot return block access list for block from before Amsterdam fork.\"},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_hash_pruned() + { + using Context ctx = await Context.CreateWithAmsterdamEnabled(); + Hash256 blockHash = ctx.Test.BlockTree.Head!.Hash!; + ctx.Test.Bridge.DeleteBlockAccessList(blockHash); + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByHash", blockHash); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":4444,\"message\":\"Cannot return pruned historical block access list.\"},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_number() + { + using Context ctx = await Context.CreateWithAmsterdamEnabled(); + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByNumber", 3); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"result\":{\"accountChanges\":[{\"address\":\"0x00000961ef480eb55e80d19ad83579a64c007002\",\"storageChanges\":[],\"storageReads\":[{\"key\":\"0x0\"},{\"key\":\"0x1\"},{\"key\":\"0x2\"},{\"key\":\"0x3\"}],\"balanceChanges\":[],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x0000bbddc7ce488642fb579f8b00f3a590007251\",\"storageChanges\":[],\"storageReads\":[{\"key\":\"0x0\"},{\"key\":\"0x1\"},{\"key\":\"0x2\"},{\"key\":\"0x3\"}],\"balanceChanges\":[],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x0000f90827f1c53a10cb7a02335b175320002935\",\"storageChanges\":[{\"slot\":\"0x2\",\"changes\":{\"0\":{\"blockAccessIndex\":0,\"newValue\":\"0x179d875956f4d9ec8d54fc9f973a3ce5800b4f0a836a5f89cfc2842359d02b9e\"}}}],\"storageReads\":[],\"balanceChanges\":[],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x475674cb523a0a2736b7f7534390288fce16982c\",\"storageChanges\":[],\"storageReads\":[],\"balanceChanges\":[{\"blockAccessIndex\":1,\"postBalance\":\"0xa410\"},{\"blockAccessIndex\":2,\"postBalance\":\"0xf618\"}],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358\",\"storageChanges\":[],\"storageReads\":[],\"balanceChanges\":[{\"blockAccessIndex\":1,\"postBalance\":\"0x3635c9adc5dea00002\"},{\"blockAccessIndex\":2,\"postBalance\":\"0x3635c9adc5dea00003\"}],\"nonceChanges\":[],\"codeChanges\":[]},{\"address\":\"0xb7705ae4c6f81b66cdb323c65f4e8133690fc099\",\"storageChanges\":[],\"storageReads\":[],\"balanceChanges\":[{\"blockAccessIndex\":1,\"postBalance\":\"0x3635c9adc5de9f5bee\"},{\"blockAccessIndex\":2,\"postBalance\":\"0x3635c9adc5de9f09e5\"}],\"nonceChanges\":[{\"blockAccessIndex\":1,\"newNonce\":\"0x2\"},{\"blockAccessIndex\":2,\"newNonce\":\"0x3\"}],\"codeChanges\":[]}]},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_number_not_found() + { + using Context ctx = await Context.CreateWithAmsterdamEnabled(); + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByNumber", 100); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Cannot return block access list, block not found.\"},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_number_unavailable_before_fork() + { + using Context ctx = await Context.Create(); // Amsterdam disabled + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByNumber", 3); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":4445,\"message\":\"Cannot return block access list for block from before Amsterdam fork.\"},\"id\":67}")); + } + + [Test] + public async Task Eth_get_block_access_list_by_number_pruned() + { + const long number = 3; + using Context ctx = await Context.CreateWithAmsterdamEnabled(); + Hash256 blockHash = ctx.Test.BlockTree.FindLevel(number)!.BlockInfos[0].BlockHash; + ctx.Test.Bridge.DeleteBlockAccessList(blockHash); + string serialized = await ctx.Test.TestEthRpc("eth_getBlockAccessListByNumber", 3); + Assert.That(serialized, Is.EqualTo("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":4444,\"message\":\"Cannot return pruned historical block access list.\"},\"id\":67}")); + } + public class AllowNullAuthorizationTuple : AuthorizationTuple { public AllowNullAuthorizationTuple(ulong chainId, Address? codeAddress, ulong nonce, Signature? sig) @@ -1687,6 +1758,12 @@ public static async Task CreateWithCancunEnabled() return await Create(specProvider); } + public static async Task CreateWithAmsterdamEnabled() + { + OverridableReleaseSpec releaseSpec = new(Amsterdam.Instance); + TestSpecProvider specProvider = new(releaseSpec); + return await Create(specProvider); + } public static async Task CreateWithAncientBarriers(long blockNumber) { diff --git a/src/Nethermind/Nethermind.JsonRpc/ErrorCodes.cs b/src/Nethermind/Nethermind.JsonRpc/ErrorCodes.cs index 82123f9ca08..383cd8e5833 100644 --- a/src/Nethermind/Nethermind.JsonRpc/ErrorCodes.cs +++ b/src/Nethermind/Nethermind.JsonRpc/ErrorCodes.cs @@ -107,6 +107,11 @@ public static class ErrorCodes /// public const int PrunedHistoryUnavailable = 4444; + /// + /// Data is not available due to eip not being enabled yet + /// + public const int UnavailableBeforeFork = 4445; + /// /// Default error code /// diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/BadBlock.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/BadBlock.cs index de2af695562..9e7a6d1eabc 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/BadBlock.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/BadBlock.cs @@ -6,6 +6,8 @@ using Nethermind.Serialization.Rlp; using Nethermind.Core.Crypto; using Nethermind.Facade.Eth; +using System.Text.Json.Serialization; +using Nethermind.Core.BlockAccessLists; namespace Nethermind.JsonRpc.Modules.Eth; @@ -14,4 +16,7 @@ public class BadBlock(Block block, bool includeFullTransactionData, ISpecProvide public BlockForRpc Block { get; } = new BlockForRpc(block, includeFullTransactionData, specProvider); public Hash256 Hash { get; } = block.Header.Hash; public byte[] Rlp { get; } = blockDecoder.Encode(block).Bytes; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BlockAccessList? GeneratedBlockAccessList { get; } = block.GeneratedBlockAccessList; } diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs index 32b049105f5..1316af6a5b4 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs @@ -5,6 +5,7 @@ using Nethermind.Blockchain.Find; using Nethermind.Blockchain.Receipts; using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; @@ -846,6 +847,31 @@ public ResultWrapper eth_config() } } + public ResultWrapper eth_getBlockAccessListByHash(Hash256 blockHash) + => GetBlockAccessList(blockHash, null); + + public ResultWrapper eth_getBlockAccessListByNumber(long blockNumber) + => GetBlockAccessList(null, blockNumber); + + private ResultWrapper GetBlockAccessList(Hash256? blockHash, long? blockNumber) + { + Block block = blockHash is null ? _blockFinder.FindBlock(blockNumber.Value) : _blockFinder.FindBlock(blockHash); + if (block is null) + { + return ResultWrapper.Fail("Cannot return block access list, block not found."); + } + else if (block.BlockAccessListHash is null) + { + return ResultWrapper.Fail("Cannot return block access list for block from before Amsterdam fork.", ErrorCodes.UnavailableBeforeFork); + } + + BlockAccessList? bal = blockchainBridge.GetBlockAccessList(block.Hash); + + return bal is null ? + ResultWrapper.Fail("Cannot return pruned historical block access list.", ErrorCodes.PrunedHistoryUnavailable) + : ResultWrapper.Success(bal); + } + private CancellationTokenSource BuildTimeoutCancellationTokenSource() => _rpcConfig.BuildTimeoutCancellationToken(); diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/IEthRpcModule.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/IEthRpcModule.cs index 28acb3a8353..6e1d4279f43 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/IEthRpcModule.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/IEthRpcModule.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Nethermind.Blockchain.Find; using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; using Nethermind.Core.Crypto; using Nethermind.Evm; using Nethermind.Facade.Eth; @@ -296,5 +297,11 @@ ResultWrapper eth_getProof( [JsonRpcMethod(IsImplemented = true, Description = "Provides configuration data for the current and next fork", IsSharable = true)] ResultWrapper eth_config(); + + [JsonRpcMethod(Description = "Retrieves block access list for a block by hash.")] + ResultWrapper eth_getBlockAccessListByHash(Hash256 blockHash); + + [JsonRpcMethod(Description = "Retrieves block access list for a block by number.")] + ResultWrapper eth_getBlockAccessListByNumber(long number); } } diff --git a/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs b/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs index 07dd1df0b51..bc7eff5be98 100644 --- a/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs +++ b/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs @@ -41,6 +41,7 @@ protected override MergeTestBlockchain CreateBaseBlockchain( => new MergeAuRaTestBlockchain(mergeConfig); protected override Hash256 ExpectedBlockHash => new("0x990d377b67dbffee4a60db6f189ae479ffb406e8abea16af55e0469b8524cf46"); + private const string _auraWithdrawalContractAddress = "0xbabe2bed00000000000000000000000000000003"; [TestCaseSource(nameof(GetWithdrawalValidationValues))] public override Task forkchoiceUpdatedV2_should_validate_withdrawals((IReleaseSpec Spec, @@ -52,10 +53,10 @@ int ErrorCode => base.forkchoiceUpdatedV2_should_validate_withdrawals(input); [TestCase( - "0xd6ac1db7fee77f895121329b5949ddfb5258c952868a3707bb1104d8f219df2e", - "0x912ef8152bf54f81762e26d2a4f0793fb2a0a55b7ce5a9ff7f6879df6d94a6b3", - "0x26b9598dd31cd520c6dcaf4f6fa13e279b4fa1f94d150357290df0e944f53115", - "0x0117c925cabe3c1d")] + "0x76d560a6eb4ea2dd6232cc7feb6f61393d9e955baffaf3a84b1c53d3dd0746b8", + "0x2277308bcf798426afd925e4f67303af26c83f5c1832644c4de93893d41bdbf5", + "0xae4bd586033cba409a0c6e58c4a0808ef798747cd55a781b972e6e00b9427af8", + "0xf0954948630d827c")] public override Task Should_process_block_as_expected_V4(string latestValidHash, string blockHash, string stateRoot, string payloadId) => base.Should_process_block_as_expected_V4(latestValidHash, blockHash, stateRoot, payloadId); @@ -74,6 +75,92 @@ public override Task Should_process_block_as_expected_V2(string latestValidHash, public override Task processing_block_should_serialize_valid_responses(string blockHash, string latestValidHash, string payloadId) => base.processing_block_should_serialize_valid_responses(blockHash, latestValidHash, payloadId); + [TestCase( + "0x9bd79e1ac72667844a969b9584977d3f196082c31e5002eda0a7bd4da8d0e4ce", + "0x176dff396d76839a9de72be37aa09076bff2b1c78908ae0f107036ea2de7c1c4", + "0xdc17608835fdd8511c87c8fd36214735bb880a109124664756919e737674e070", + "0x77a3fa6067dde61a", + _auraWithdrawalContractAddress)] + public override async Task Should_process_block_as_expected_V6(string latestValidHash, string blockHash, string stateRoot, string payloadId, string? auraWithdrawalContractAddress) + => await base.Should_process_block_as_expected_V6(latestValidHash, blockHash, stateRoot, payloadId, auraWithdrawalContractAddress); + + [TestCase( + "0xef18d85fde297e54998c706132658bdb8db2f43da55bc6cc42222b2758000ecc", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xdf1b2c48064fe2c6a431b32b76422d419fe4e3e744a2720d011bd134c5590e63")] + public override Task NewPayloadV5_accepts_valid_BAL(string blockHash, string receiptsRoot, string stateRoot) + => NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + + [TestCase( + "0xc9e08f341474c4af262a47a18c37b95ab0d9cfd96d780ac6c2dd7d1362c43f04", + "0xb6b4dddb39c5f23402fbc7e0e0ad387e24ea8b8d6e13b9e9f5f972ff064a82f6", + "0x05a7a8b6afb54d9195d3c78c7b11febd7173ba93982a6d9cb981646fd4d723e0", + "0xbc48a7a2b823d3a089a3d6fe46205e0ce1b642563fed1a8255005f64f4b5acac", + _auraWithdrawalContractAddress)] + public override Task NewPayloadV5_rejects_invalid_BAL_after_processing(string blockHash, string stateRoot, string invalidBalHash, string expectedBalHash, string? auraWithdrawalContractAddress) + => base.NewPayloadV5_rejects_invalid_BAL_after_processing(blockHash, stateRoot, invalidBalHash, expectedBalHash, auraWithdrawalContractAddress); + + [TestCase( + "0xc4ffe5a6af2fb1d97b9a58c4123040d44015fc9ca6e360baef75bc131cebfeb5", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public override Task NewPayloadV5_rejects_invalid_BAL_with_incorrect_changes_early(string blockHash, string receiptsRoot, string stateRoot) + => NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list contained incorrect changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 3.", + withIncorrectChange: true, + auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + + [TestCase( + "0x222582c7d7ef2f2e90ff4d689499847da742021839fd5aca1af5d4bc6f135ada", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public override Task NewPayloadV5_rejects_invalid_BAL_with_missing_changes_early(string blockHash, string receiptsRoot, string stateRoot) + => NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list missing account changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 2.", + withMissingChange: true, + auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + + [TestCase( + "0x78b7ff406febf51d4256d5e89ad00ee76903c768252eb2e1f5ef6c7f12da4fd8", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public override Task NewPayloadV5_rejects_invalid_BAL_with_surplus_changes_early(string blockHash, string receiptsRoot, string stateRoot) + => NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list contained surplus changes for 0x65942aaf2c32a1aca4f14e82e94fce91960893a2 at index 2.", + withSurplusChange: true, + auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + + [TestCase( + "0x65b0294a1727c92b65874c2505dbba3c290866d07f5a3ca4449cb283450ca692", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public override Task NewPayloadV5_rejects_invalid_BAL_with_surplus_reads_early(string blockHash, string receiptsRoot, string stateRoot) + => NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list contained invalid storage reads.", + withSurplusReads: true, + auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + + [Test] + [TestCase(_auraWithdrawalContractAddress)] + public override async Task GetPayloadV6_builds_block_with_BAL(string? auraWithdrawalContractAddress) + => await base.GetPayloadV6_builds_block_with_BAL(auraWithdrawalContractAddress); + [Test] [TestCase( "0xa66ec67b117f57388da53271f00c22a68e6c297b564f67c5904e6f2662881875", @@ -153,7 +240,7 @@ protected override ChainSpec CreateChainSpec() baseChainSpec.EngineChainSpecParametersProvider = new TestChainSpecParametersProvider( new AuRaChainSpecEngineParameters { - WithdrawalContractAddress = new("0xbabe2bed00000000000000000000000000000003"), + WithdrawalContractAddress = new(_auraWithdrawalContractAddress), StepDuration = { { 0, 3 } } }); baseChainSpec.Parameters = new ChainParameters(); diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V1.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V1.cs index 0ae849ebdc2..82e7a65ccef 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V1.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V1.cs @@ -1535,7 +1535,7 @@ public async Task Should_return_ClientVersionV1() [Test] public async Task Should_return_capabilities() { - using MergeTestBlockchain chain = await CreateBlockchain(Osaka.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(Amsterdam.Instance); IEngineRpcModule rpcModule = chain.EngineRpcModule; IOrderedEnumerable expected = typeof(IEngineRpcModule).GetMethods() .Select(static m => m.Name) diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V3.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V3.cs index 98f60c5d103..7ace5aa2f24 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V3.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V3.cs @@ -377,23 +377,26 @@ public async Task NewPayloadV3_should_verify_blob_versioned_hashes_again .Success(new PayloadStatusV1() { Status = FurtherValidationStatus }))); return (chain, new EngineRpcModule( - Substitute.For>(), - Substitute.For>(), - Substitute.For>(), - Substitute.For>(), - Substitute.For>(), - newPayloadHandlerMock, - Substitute.For(), - Substitute.For, IEnumerable>>(), - Substitute.For(), - Substitute.For>(), - Substitute.For, IEnumerable>>(), - Substitute.For>>(), - Substitute.For?>>(), - Substitute.For(), - chain.SpecProvider, - new GCKeeper(NoGCStrategy.Instance, chain.LogManager), - Substitute.For())); + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + newPayloadHandlerMock, + Substitute.For(), + Substitute.For, IEnumerable>>(), + Substitute.For(), + Substitute.For>(), + Substitute.For, IEnumerable>>(), + Substitute.For>>(), + Substitute.For?>>(), + Substitute.For, IEnumerable>>(), + Substitute.For(), + Substitute.For(), + chain.SpecProvider, + new GCKeeper(NoGCStrategy.Instance, chain.LogManager), + Substitute.For())); } diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V4.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V4.cs index fe19df159ed..c672d14f5d4 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V4.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V4.cs @@ -23,10 +23,10 @@ namespace Nethermind.Merge.Plugin.Test; public partial class EngineModuleTests { [TestCase( - "0x0b2e24d802b4664b6f64f87a3e9498566080b2e9f8adf6e00896f89e4cc8a83b", - "0xf0444d3c2e8d725d7884803ff71b62a680fe9f96765a9f01b9c4e47dacb2f3b0", - "0x73cecfc66bc1c8545aa3521e21be51c31bd2054badeeaa781f5fd5b871883f35", - "0xd814b750baa05a39")] + "0x32fc756d56a1897bf4d53ec72a854743786b5edbd8c0feec05b245e7cc124eea", + "0xc1ecac4884c36982061392807b9307fb0d701a05d9d9fd7dc7e82d2ec96cf9af", + "0x8ffb712de6b72f59def7b84f361e6c23519f7f8674d7e6552e23617a996d8ed3", + "0x0f0b18188ed90425")] public virtual async Task Should_process_block_as_expected_V4(string latestValidHash, string blockHash, string stateRoot, string payloadId) { diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs new file mode 100644 index 00000000000..00627a2d225 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs @@ -0,0 +1,687 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using Nethermind.Consensus.Producers; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Extensions; +using Nethermind.JsonRpc; +using Nethermind.Merge.Plugin.Data; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using NUnit.Framework; +using Nethermind.Serialization.Rlp; +using Nethermind.Core.Test.Builders; +using Nethermind.Core.Crypto; +using Nethermind.Evm; +using System.Collections.Generic; +using System.Linq; +using Nethermind.State; +using Nethermind.TxPool; +using Nethermind.Int256; +using Nethermind.JsonRpc.Test; +using System; +using Nethermind.Core.Test; + +namespace Nethermind.Merge.Plugin.Test; + +public partial class EngineModuleTests +{ + + [TestCase( + "0x54dfc1d0ee589508c694f51cbea0816a2b665c8521294b549589389290751669", + "0x8597fff183c2055d2429240b5deb70af64121e1f2299a495305f718aed536f7c", + "0xa5d7c276147e583751c86570cca74327cb2e46aeca349d42d5e647f00ff372d6", + "0xf5ab30d4c8440c85", + null)] + public virtual async Task Should_process_block_as_expected_V6(string latestValidHash, string blockHash, + string stateRoot, string payloadId, string? auraWithdrawalContractAddress) + { + using MergeTestBlockchain chain = + await CreateBlockchain(Amsterdam.Instance); + IEngineRpcModule rpc = chain.EngineRpcModule; + Hash256 startingHead = chain.BlockTree.HeadHash; + Hash256 prevRandao = Keccak.Zero; + Address feeRecipient = TestItem.AddressC; + ulong timestamp = Timestamper.UnixTime.Seconds; + var fcuState = new + { + headBlockHash = startingHead.ToString(), + safeBlockHash = startingHead.ToString(), + finalizedBlockHash = Keccak.Zero.ToString() + }; + Withdrawal[] withdrawals = []; + var payloadAttrs = new + { + timestamp = timestamp.ToHexString(true), + prevRandao = prevRandao.ToString(), + suggestedFeeRecipient = feeRecipient.ToString(), + withdrawals, + parentBeaconBLockRoot = Keccak.Zero + }; + string?[] @params = new string?[] + { + chain.JsonSerializer.Serialize(fcuState), chain.JsonSerializer.Serialize(payloadAttrs) + }; + string expectedPayloadId = payloadId; + + string response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV3", @params!); + JsonRpcSuccessResponse? successResponse = chain.JsonSerializer.Deserialize(response); + + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new ForkchoiceUpdatedV1Result + { + PayloadId = expectedPayloadId, + PayloadStatus = new PayloadStatusV1 + { + LatestValidHash = new(latestValidHash), + Status = PayloadStatus.Valid, + ValidationError = null + } + } + }))); + } + + BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList.WithPrecompileChanges(startingHead, timestamp); + if (auraWithdrawalContractAddress is not null) + { + expectedBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress)), new(Address.SystemUser)]); + } + + Hash256 expectedBlockHash = new(blockHash); + Block block = new( + new( + startingHead, + Keccak.OfAnEmptySequenceRlp, + feeRecipient, + UInt256.Zero, + 1, + chain.BlockTree.Head!.GasLimit, + timestamp, + Bytes.FromHexString("0x4e65746865726d696e64") // Nethermind + ) + { + BlobGasUsed = 0, + ExcessBlobGas = 0, + BaseFeePerGas = 0, + Bloom = Bloom.Empty, + GasUsed = 0, + Hash = expectedBlockHash, + MixHash = prevRandao, + ParentBeaconBlockRoot = Keccak.Zero, + ReceiptsRoot = chain.BlockTree.Head!.ReceiptsRoot!, + StateRoot = new(stateRoot), + }, + [], + [], + withdrawals, + expectedBalBuilder.TestObject); + GetPayloadV6Result expectedPayload = new(block, UInt256.Zero, new BlobsBundleV2(block), executionRequests: [], shouldOverrideBuilder: false); + + response = await RpcTest.TestSerializedRequest(rpc, "engine_getPayloadV6", expectedPayloadId); + successResponse = chain.JsonSerializer.Deserialize(response); + + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = expectedPayload + }))); + } + + response = await RpcTest.TestSerializedRequest(rpc, "engine_newPayloadV5", + chain.JsonSerializer.Serialize(ExecutionPayloadV4.Create(block)), "[]", Keccak.Zero.ToString(true), "[]"); + successResponse = chain.JsonSerializer.Deserialize(response); + + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new PayloadStatusV1 + { + LatestValidHash = expectedBlockHash, + Status = PayloadStatus.Valid, + ValidationError = null + } + }))); + } + + fcuState = new + { + headBlockHash = expectedBlockHash.ToString(true), + safeBlockHash = expectedBlockHash.ToString(true), + finalizedBlockHash = startingHead.ToString(true) + }; + @params = new[] { chain.JsonSerializer.Serialize(fcuState), null }; + + response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV3", @params!); + successResponse = chain.JsonSerializer.Deserialize(response); + + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new ForkchoiceUpdatedV1Result + { + PayloadId = null, + PayloadStatus = new PayloadStatusV1 + { + LatestValidHash = expectedBlockHash, + Status = PayloadStatus.Valid, + ValidationError = null + } + } + }))); + } + } + + + [TestCase( + "0x1e11df85db9df143816b319b33ad72dc488d63baa6d3d477b72803b352897ef4", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public virtual async Task NewPayloadV5_accepts_valid_BAL(string blockHash, string receiptsRoot, string stateRoot) + => await NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + null); + + [TestCase( + "0xfeff419072d4141abc1fed0fd15360e8a521a5544c0c6f4413cd84f0d07a5fb5", + "0xee19f9b94832e8855eee01f304f9479d15a4e690ef63145094a726006bc6d1b2", + "0xb1cce0e7c7315eb50afe128ad81a92b9c0cab67c6c1eb7170ad69811d53eb42c", + "0x6455e7ed6d666a3e421f97ffadaf1bbc18be8ca752bfa9cdb5ff4863ff3db38d", + null)] + public virtual async Task NewPayloadV5_rejects_invalid_BAL_after_processing(string blockHash, string stateRoot, string invalidBalHash, string expectedBalHash, string? auraWithdrawalContractAddress) + { + using MergeTestBlockchain chain = + await CreateBlockchain(Amsterdam.Instance); + IEngineRpcModule rpc = chain.EngineRpcModule; + + const ulong timestamp = 1000000; + Hash256 parentHash = new(chain.BlockTree.HeadHash); + + BlockAccessListBuilder invalidBalBuilder = Build.A.BlockAccessList + .WithPrecompileChanges(parentHash, timestamp) + .WithAccountChanges([new(TestItem.AddressA)]); // additional address + if (auraWithdrawalContractAddress is not null) + { + invalidBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress)), new(Address.SystemUser)]); + } + BlockAccessList invalidBal = invalidBalBuilder.TestObject; + + Block block = new( + new( + parentHash, + Keccak.OfAnEmptySequenceRlp, + TestItem.AddressC, + UInt256.Zero, + 1, + chain.BlockTree.Head!.GasLimit, + timestamp, + [] + ) + { + BlobGasUsed = 0, + ExcessBlobGas = 0, + BaseFeePerGas = 0, + Bloom = Bloom.Empty, + GasUsed = 0, + Hash = new(blockHash), + MixHash = Keccak.Zero, + ParentBeaconBlockRoot = Keccak.Zero, + ReceiptsRoot = Keccak.EmptyTreeHash, + StateRoot = new(stateRoot), + }, + [], + [], + [], + invalidBal); + + string response = await RpcTest.TestSerializedRequest(rpc, "engine_newPayloadV5", + chain.JsonSerializer.Serialize(ExecutionPayloadV4.Create(block)), "[]", Keccak.Zero.ToString(true), "[]"); + JsonRpcSuccessResponse successResponse = chain.JsonSerializer.Deserialize(response); + + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new PayloadStatusV1 + { + LatestValidHash = Keccak.Zero, + Status = PayloadStatus.Invalid, + ValidationError = $"InvalidBlockLevelAccessListHash: Expected {expectedBalHash}, got {invalidBalHash}" + } + }))); + } + } + + [TestCase( + "0x00c884d490708f36fd9b8f8b666a27b06d60ed3abc267d3416619e1b4a5eaa1a", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_incorrect_changes_early(string blockHash, string receiptsRoot, string stateRoot) + => await NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list contained incorrect changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 3.", + withIncorrectChange: true); + + [TestCase( + "0x969025cfc580665697a9fb224547ada9a792d9673f8f5f376caed043e5595c26", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_missing_changes_early(string blockHash, string receiptsRoot, string stateRoot) + => await NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list missing account changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 2.", + withMissingChange: true); + + [TestCase( + "0xfd090a339659d2ca17dfdcf8550d5667c1e30f5aa49af1f074d4bda8110005ff", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_surplus_changes_early(string blockHash, string receiptsRoot, string stateRoot) + => await NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list contained surplus changes for 0x65942aaf2c32a1aca4f14e82e94fce91960893a2 at index 2.", + withSurplusChange: true); + + [TestCase( + "0x4a599cb247bcf4b2565a4dbeb1f4c55ad849cbac5f810cdd79878898c86088e1", + "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", + "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] + public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_surplus_reads_early(string blockHash, string receiptsRoot, string stateRoot) + => await NewPayloadV5( + blockHash, + receiptsRoot, + stateRoot, + "InvalidBlockLevelAccessList: Suggested block-level access list contained invalid storage reads.", + withSurplusReads: true); + + [Test] + [TestCase(null)] + public virtual async Task GetPayloadV6_builds_block_with_BAL(string? auraWithdrawalContractAddress) + { + ulong timestamp = 12; + TestSpecProvider specProvider = new(Amsterdam.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(specProvider); + + Block genesis = chain.BlockFinder.FindGenesisBlock()!; + PayloadAttributes payloadAttributes = new() + { + Timestamp = timestamp, + PrevRandao = genesis.Header.Random!, + SuggestedFeeRecipient = Address.Zero, + ParentBeaconBlockRoot = Keccak.Zero, + Withdrawals = [] + }; + + Transaction tx = Build.A.Transaction + .WithValue(1) + .WithTo(TestItem.AddressB) + .SignedAndResolved(chain.EthereumEcdsa, TestItem.PrivateKeyA) + .TestObject; + + AcceptTxResult txPoolRes = chain.TxPool.SubmitTx(tx, TxHandlingOptions.None); + Assert.That(txPoolRes, Is.EqualTo(AcceptTxResult.Accepted)); + + ForkchoiceStateV1 fcuState = new(genesis.Hash!, genesis.Hash!, genesis.Hash!); + + ResultWrapper fcuResponse = await chain.EngineRpcModule.engine_forkchoiceUpdatedV3(fcuState, payloadAttributes); + Assert.That(fcuResponse.Result.ResultType, Is.EqualTo(ResultType.Success)); + + await Task.Delay(1000); + + ResultWrapper getPayloadResult = + await chain.EngineRpcModule.engine_getPayloadV6(Bytes.FromHexString(fcuResponse.Data.PayloadId!)); + GetPayloadV6Result res = getPayloadResult.Data!; + Assert.That(res.ExecutionPayload.BlockAccessList, Is.Not.Null); + BlockAccessList bal = Rlp.Decode(new Rlp(res.ExecutionPayload.BlockAccessList)); + + BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList + .WithAccountChanges([ + Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithBalanceChanges([new(1, new UInt256(Bytes.FromHexString("0x3635c9adc5de9fadf7"), isBigEndian: true))]) + .WithNonceChanges([new(1, 1)]) + .TestObject, + Build.An.AccountChanges + .WithAddress(TestItem.AddressB) + .WithBalanceChanges([new(1, new UInt256(Bytes.FromHexString("0x3635c9adc5dea00001"), isBigEndian: true))]) + .TestObject, + Build.An.AccountChanges + .WithAddress(Address.Zero) + .WithBalanceChanges([new(1, 0x5208)]) + .TestObject, + ]) + .WithPrecompileChanges(genesis.Header.Hash!, timestamp); + + if (auraWithdrawalContractAddress is not null) + { + expectedBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress))]); + } + + BlockAccessList expected = expectedBalBuilder.TestObject; + Assert.That(bal, Is.EqualTo(expected)); + } + + [Test] + public virtual async Task GetPayloadBodiesHashV2_returns_correctly() + { + TestSpecProvider specProvider = new(Amsterdam.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(specProvider); + + List blockHashes = []; + for (var i = 1; i < 5; i++) + { + ExecutionPayloadV3 payload = await AddNewBlockV6(chain.EngineRpcModule, chain, 1); + blockHashes.Add(payload.BlockHash); + } + + ResultWrapper> response = await chain.EngineRpcModule.engine_getPayloadBodiesByHashV2([ + blockHashes.ElementAt(1), + blockHashes.ElementAt(2), + Hash256.Zero + ]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(response.Result.ResultType, Is.EqualTo(ResultType.Success)); + Assert.That(response.Data.Count, Is.EqualTo(3)); + Assert.That(response.Data.ElementAt(2), Is.Null); + } + } + + [Test] + public virtual async Task GetPayloadBodiesByRangeV2_returns_correctly() + { + TestSpecProvider specProvider = new(Amsterdam.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(specProvider); + + for (var i = 1; i < 5; i++) + { + await AddNewBlockV6(chain.EngineRpcModule, chain, 1); + } + + ResultWrapper> response = await chain.EngineRpcModule.engine_getPayloadBodiesByRangeV2(1, 6); + + using (Assert.EnterMultipleScope()) + { + Assert.That(response.Result.ResultType, Is.EqualTo(ResultType.Success)); + Assert.That(response.Data.Count, Is.EqualTo(4)); // cutoff at head + } + } + + private async Task AddNewBlockV6(IEngineRpcModule rpcModule, MergeTestBlockchain chain, int transactionCount = 0) + { + Transaction[] txs = BuildTransactions(chain, chain.BlockTree.Head!.Hash!, TestItem.PrivateKeyA, TestItem.AddressB, (uint)transactionCount, 0, out _, out _, 0); + chain.AddTransactions(txs); + + PayloadAttributes payloadAttributes = new() + { + Timestamp = chain.BlockTree.Head!.Timestamp + 1, + PrevRandao = TestItem.KeccakH, + SuggestedFeeRecipient = TestItem.AddressF, + Withdrawals = [], + ParentBeaconBlockRoot = TestItem.KeccakE + }; + Hash256 currentHeadHash = chain.BlockTree.HeadHash; + ForkchoiceStateV1 forkchoiceState = new(currentHeadHash, currentHeadHash, currentHeadHash); + + Task blockImprovementWait = chain.WaitForImprovedBlock(); + + string payloadId = (await rpcModule.engine_forkchoiceUpdatedV3(forkchoiceState, payloadAttributes)).Data.PayloadId!; + + await blockImprovementWait; + + ResultWrapper payloadResult = await rpcModule.engine_getPayloadV6(Bytes.FromHexString(payloadId)); + using (Assert.EnterMultipleScope()) + { + Assert.That(payloadResult.Result, Is.EqualTo(Result.Success)); + Assert.That(payloadResult.Data, Is.Not.Null); + } + + GetPayloadV6Result payload = payloadResult.Data; + await rpcModule.engine_newPayloadV5(payload.ExecutionPayload, payload.BlobsBundle.Blobs, TestItem.KeccakE, []); + + ForkchoiceStateV1 newForkchoiceState = new(payload.ExecutionPayload.BlockHash, payload.ExecutionPayload.BlockHash, payload.ExecutionPayload.BlockHash); + await rpcModule.engine_forkchoiceUpdatedV3(newForkchoiceState, null); + + return payload.ExecutionPayload; + } + + protected async Task NewPayloadV5( + string blockHash, + string receiptsRoot, + string stateRoot, + string? expectedError = null, + bool withIncorrectChange = false, + bool withSurplusChange = false, + bool withMissingChange = false, + bool withSurplusReads = false, + string? auraWithdrawalContractAddress = null) + { + using MergeTestBlockchain chain = + await CreateBlockchain(Amsterdam.Instance); + IEngineRpcModule rpc = chain.EngineRpcModule; + + const long gasUsed = 167340; + const long gasUsedBeforeFinal = 92100; + const ulong gasPrice = 2; + const long gasLimit = 100000; + const ulong timestamp = 1000000; + Hash256 parentHash = new(chain.BlockTree.HeadHash); + + Transaction tx = Build.A.Transaction + .WithTo(TestItem.AddressB) + .WithSenderAddress(TestItem.AddressA) + .WithValue(0) + .WithGasPrice(gasPrice) + .WithGasLimit(gasLimit) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Transaction tx2 = Build.A.Transaction + .WithTo(null) + .WithSenderAddress(TestItem.AddressA) + .WithValue(0) + .WithNonce(1) + .WithGasPrice(gasPrice) + .WithGasLimit(gasLimit) + .WithCode(Eip2935TestConstants.InitCode) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + // Store followed by revert should undo storage change + byte[] code = Prepare.EvmCode + .PushData(1) + .PushData(1) + .SSTORE() + .Op(Instruction.PUSH0) + .Op(Instruction.PUSH0) + .REVERT() + .Done; + Transaction tx3 = Build.A.Transaction + .WithTo(null) + .WithSenderAddress(TestItem.AddressA) + .WithValue(0) + .WithNonce(2) + .WithGasPrice(gasPrice) + .WithGasLimit(gasLimit) + .WithCode(code) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Withdrawal withdrawal = new() + { + Index = 0, + ValidatorIndex = 0, + Address = TestItem.AddressD, + AmountInGwei = 1 + }; + + Address newContractAddress = ContractAddress.From(TestItem.AddressA, 1); + Address newContractAddress2 = ContractAddress.From(TestItem.AddressA, 2); + + UInt256 eip4788Slot1 = timestamp % Eip4788Constants.RingBufferSize; + UInt256 eip4788Slot2 = (timestamp % Eip4788Constants.RingBufferSize) + Eip4788Constants.RingBufferSize; + + StorageChange parentHashStorageChange = new(0, new UInt256(parentHash.BytesToArray(), isBigEndian: true)); + StorageChange timestampStorageChange = new(0, 0xF4240); + + UInt256 accountBalance = chain.StateReader.GetBalance(chain.BlockTree.Head!.Header, TestItem.AddressA); + UInt256 addressABalance = accountBalance - gasPrice * GasCostOf.Transaction; + UInt256 addressABalance2 = accountBalance - gasPrice * gasUsedBeforeFinal; + UInt256 addressABalance3 = accountBalance - gasPrice * gasUsed; + + AccountChangesBuilder newContractAccount = Build.An.AccountChanges + .WithAddress(newContractAddress) + .WithNonceChanges([new(2, 1)]) + .WithCodeChanges([new(2, Eip2935TestConstants.Code)]); + + if (withIncorrectChange) + { + newContractAccount = newContractAccount.WithBalanceChanges([new(3, 1.GWei)]); // incorrect change + } + + if (withSurplusReads) + { + for (ulong i = 0; i < 100; i++) + { + newContractAccount = newContractAccount.WithStorageReads(new UInt256(i)); + } + } + + BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList + .WithAccountChanges( + Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithBalanceChanges([new(1, addressABalance), new(2, addressABalance2), new(3, addressABalance3)]) + .WithNonceChanges([new(1, 1), new(2, 2), new(3, 3)]) + .TestObject, + new(TestItem.AddressB), + Build.An.AccountChanges + .WithAddress(TestItem.AddressE) + .WithBalanceChanges([new(1, new UInt256(GasCostOf.Transaction * gasPrice)), new(2, new UInt256(gasUsedBeforeFinal * gasPrice)), new(3, new UInt256(gasUsed * gasPrice))]) + .TestObject, + Build.An.AccountChanges + .WithAddress(newContractAddress2) + .WithStorageReads(1) + .TestObject) + .WithPrecompileChanges(parentHash, timestamp); + + if (!withMissingChange) + { + expectedBalBuilder.WithAccountChanges(newContractAccount.TestObject); + } + + if (withSurplusChange) + { + expectedBalBuilder.WithAccountChanges( + Build.An.AccountChanges + .WithAddress(TestItem.AddressF) + .WithNonceChanges([new(2, 5)]) + .TestObject); + } + + if (auraWithdrawalContractAddress is not null) + { + expectedBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress)), new(Address.SystemUser)]); + } + else + { + expectedBalBuilder.WithAccountChanges([Build.An.AccountChanges + .WithAddress(TestItem.AddressD) + .WithBalanceChanges([new(4, 1.GWei)]) + .TestObject]); + } + + Block block = new( + new( + parentHash, + Keccak.OfAnEmptySequenceRlp, + TestItem.AddressE, + UInt256.Zero, + 1, + chain.BlockTree.Head!.GasLimit, + timestamp, + [] + ) + { + BlobGasUsed = 0, + ExcessBlobGas = 0, + BaseFeePerGas = 0, + Bloom = Bloom.Empty, + GasUsed = gasUsed, + Hash = new(blockHash), + MixHash = Keccak.Zero, + ParentBeaconBlockRoot = Keccak.Zero, + ReceiptsRoot = new(receiptsRoot), + StateRoot = new(stateRoot), + }, + [tx, tx2, tx3], + [], + [withdrawal], + expectedBalBuilder.TestObject); + + string response = await RpcTest.TestSerializedRequest(rpc, "engine_newPayloadV5", + chain.JsonSerializer.Serialize(ExecutionPayloadV4.Create(block)), "[]", Keccak.Zero.ToString(true), "[]"); + JsonRpcSuccessResponse successResponse = chain.JsonSerializer.Deserialize(response); + + if (expectedError is null) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new PayloadStatusV1 + { + LatestValidHash = block.Hash, + Status = PayloadStatus.Valid, + ValidationError = null + } + }))); + } + } + else + { + using (Assert.EnterMultipleScope()) + { + Assert.That(successResponse, Is.Not.Null); + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new PayloadStatusV1 + { + LatestValidHash = Keccak.Zero, + Status = PayloadStatus.Invalid, + ValidationError = expectedError + } + }))); + } + } + } +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayload.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayload.cs index 33660a072a2..5f26fd098cd 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayload.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayload.cs @@ -12,6 +12,8 @@ using Nethermind.State.Proofs; using System.Text.Json.Serialization; using Nethermind.Core.ExecutionRequest; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Extensions; namespace Nethermind.Merge.Plugin.Data; @@ -97,6 +99,13 @@ public byte[][] Transactions [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public virtual ulong? ExcessBlobGas { get; set; } + /// + /// Gets or sets as defined in + /// EIP-7928. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public virtual byte[]? BlockAccessList { get; set; } + /// /// Gets or sets as defined in /// EIP-4788. @@ -141,7 +150,7 @@ public virtual BlockDecodingResult TryGetBlock(UInt256? totalDifficulty = null) TransactionDecodingResult transactions = TryGetTransactions(); if (transactions.Error is not null) { - return new BlockDecodingResult(transactions.Error); + return new(transactions.Error); } BlockHeader header = new( @@ -176,10 +185,7 @@ public virtual BlockDecodingResult TryGetBlock(UInt256? totalDifficulty = null) return new BlockDecodingResult(block); } - protected virtual Hash256? BuildWithdrawalsRoot() - { - return Withdrawals is null ? null : new WithdrawalTrie(Withdrawals).RootHash; - } + protected virtual Hash256? BuildWithdrawalsRoot() => Withdrawals is null ? null : new WithdrawalTrie(Withdrawals).RootHash; protected Transaction[]? _transactions = null; @@ -256,7 +262,7 @@ public ValidationResult ValidateParams(IReleaseSpec spec, int version, out strin protected virtual int GetExecutionPayloadVersion() => this switch { - { ExecutionRequests: not null } => 4, + { BlockAccessList: not null } => 4, { BlobGasUsed: not null } or { ExcessBlobGas: not null } or { ParentBeaconBlockRoot: not null } => 3, { Withdrawals: not null } => 2, _ => 1 diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV1Result.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV1Result.cs index 995886ebf05..2083525a504 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV1Result.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV1Result.cs @@ -15,7 +15,7 @@ public ExecutionPayloadBodyV1Result(IReadOnlyList transactions, IRe { ArgumentNullException.ThrowIfNull(transactions); - var t = new byte[transactions.Count][]; + byte[][] t = new byte[transactions.Count][]; for (int i = 0, count = t.Length; i < count; i++) { diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV2Result.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV2Result.cs new file mode 100644 index 00000000000..69584aae088 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadBodyV2Result.cs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using Nethermind.Core; +using Nethermind.Serialization.Rlp; +using System.Text.Json.Serialization; + +namespace Nethermind.Merge.Plugin.Data; + +public class ExecutionPayloadBodyV2Result +{ + public ExecutionPayloadBodyV2Result(IReadOnlyList transactions, IReadOnlyList? withdrawals, byte[]? blockAccessList) + { + ArgumentNullException.ThrowIfNull(transactions); + + byte[][] t = new byte[transactions.Count][]; + + for (int i = 0, count = t.Length; i < count; i++) + { + t[i] = Rlp.Encode(transactions[i], RlpBehaviors.SkipTypedWrapping).Bytes; + } + + Transactions = t; + Withdrawals = withdrawals; + BlockAccessList = blockAccessList; + } + + public IReadOnlyList Transactions { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public IReadOnlyList? Withdrawals { get; set; } + + public byte[]? BlockAccessList { get; set; } +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadV4.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadV4.cs new file mode 100644 index 00000000000..871be5f12dd --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadV4.cs @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Text.Json.Serialization; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Int256; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Merge.Plugin.Data; + +/// +/// Represents an object mapping the ExecutionPayloadV4 structure of the beacon chain spec. +/// +public class ExecutionPayloadV4 : ExecutionPayloadV3, IExecutionPayloadFactory +{ + protected new static TExecutionPayload Create(Block block) where TExecutionPayload : ExecutionPayloadV4, new() + { + TExecutionPayload executionPayload = ExecutionPayloadV3.Create(block); + executionPayload.BlockAccessList = block.EncodedBlockAccessList ?? (block.BlockAccessList is null ? null : Rlp.Encode(block.BlockAccessList).Bytes); + return executionPayload; + } + + public new static ExecutionPayloadV4 Create(Block block) => Create(block); + + public override BlockDecodingResult TryGetBlock(UInt256? totalDifficulty = null) + { + BlockDecodingResult baseResult = base.TryGetBlock(totalDifficulty); + Block? block = baseResult.Block; + if (block is null) + { + return baseResult; + } + + if (BlockAccessList is not null) + { + try + { + block.BlockAccessList = Rlp.Decode(BlockAccessList); + } + catch (RlpException e) + { + return new($"Error decoding block access list: {e}"); + } + } + + block.EncodedBlockAccessList = BlockAccessList; + block.Header.BlockAccessListHash = BlockAccessList is null || BlockAccessList.Length == 0 ? null : new(ValueKeccak.Compute(BlockAccessList).Bytes); + + return baseResult; + } + + public override bool ValidateFork(ISpecProvider specProvider) + => specProvider.GetSpec(BlockNumber, Timestamp).IsEip7928Enabled; + + + /// + /// Gets or sets as defined in + /// EIP-4844. + /// + [JsonRequired] + public sealed override byte[]? BlockAccessList { get; set; } +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV4Result.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV4Result.cs index 083de55ebf8..643f853b0e9 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV4Result.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV4Result.cs @@ -7,7 +7,9 @@ namespace Nethermind.Merge.Plugin.Data; -public class GetPayloadV4Result(Block block, UInt256 blockFees, BlobsBundleV1 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) : GetPayloadV3Result(block, blockFees, blobsBundle, shouldOverrideBuilder) +public class GetPayloadV4Result(Block block, UInt256 blockFees, BlobsBundleV1 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) + : GetPayloadV3Result(block, blockFees, blobsBundle, shouldOverrideBuilder) + where TVersionedExecutionPayload : ExecutionPayloadV3, IExecutionPayloadParams, IExecutionPayloadFactory { public byte[][]? ExecutionRequests { get; } = executionRequests; @@ -20,3 +22,5 @@ public override bool ValidateFork(ISpecProvider specProvider) return spec.IsEip7623Enabled && !spec.IsEip7594Enabled; } } + +public class GetPayloadV4Result(Block block, UInt256 blockFees, BlobsBundleV1 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) : GetPayloadV4Result(block, blockFees, blobsBundle, executionRequests, shouldOverrideBuilder); diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV5Result.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV5Result.cs index 9d47d7439c6..6be3ea386c1 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV5Result.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV5Result.cs @@ -7,7 +7,9 @@ namespace Nethermind.Merge.Plugin.Data; -public class GetPayloadV5Result(Block block, UInt256 blockFees, BlobsBundleV2 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) : GetPayloadV4Result(block, blockFees, null!, executionRequests, shouldOverrideBuilder) +public class GetPayloadV5Result(Block block, UInt256 blockFees, BlobsBundleV2 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) + : GetPayloadV4Result(block, blockFees, null!, executionRequests, shouldOverrideBuilder) + where TVersionedExecutionPayload : ExecutionPayloadV3, IExecutionPayloadParams, IExecutionPayloadFactory { public new BlobsBundleV2 BlobsBundle { get; } = blobsBundle; @@ -16,3 +18,5 @@ public override string ToString() => public override bool ValidateFork(ISpecProvider specProvider) => specProvider.GetSpec(ExecutionPayload.BlockNumber, ExecutionPayload.Timestamp).IsEip7594Enabled; } + +public class GetPayloadV5Result(Block block, UInt256 blockFees, BlobsBundleV2 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) : GetPayloadV5Result(block, blockFees, blobsBundle, executionRequests, shouldOverrideBuilder); diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV6Result.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV6Result.cs new file mode 100644 index 00000000000..655d2140606 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV6Result.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Int256; + +namespace Nethermind.Merge.Plugin.Data; + +public class GetPayloadV6Result(Block block, UInt256 blockFees, BlobsBundleV2 blobsBundle, byte[][] executionRequests, bool shouldOverrideBuilder) + : GetPayloadV5Result(block, blockFees, blobsBundle, executionRequests, shouldOverrideBuilder) +{ + public override string ToString() => + $"{{ExecutionPayload: {ExecutionPayload}, Fees: {BlockValue}, BlobsBundle blobs count: {BlobsBundle.Blobs.Length}, ShouldOverrideBuilder {ShouldOverrideBuilder}, ExecutionRequests count : {ExecutionRequests?.Length}}}"; + + public override bool ValidateFork(ISpecProvider specProvider) => specProvider.GetSpec(ExecutionPayload.BlockNumber, ExecutionPayload.Timestamp).IsEip7928Enabled; +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Amsterdam.cs b/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Amsterdam.cs new file mode 100644 index 00000000000..5b799c32c8f --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Amsterdam.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading.Tasks; +using Nethermind.Consensus; +using Nethermind.Core.Crypto; +using Nethermind.JsonRpc; +using Nethermind.Merge.Plugin.Data; +using Nethermind.Merge.Plugin.Handlers; + +namespace Nethermind.Merge.Plugin; + +public partial class EngineRpcModule : IEngineRpcModule +{ + private readonly IAsyncHandler _getPayloadHandlerV6; + private readonly IHandler, IEnumerable> _executionGetPayloadBodiesByHashV2Handler; + private readonly IGetPayloadBodiesByRangeV2Handler _executionGetPayloadBodiesByRangeV2Handler; + + public Task> engine_getPayloadV6(byte[] payloadId) + => _getPayloadHandlerV6.HandleAsync(payloadId); + + public Task> engine_newPayloadV5(ExecutionPayloadV4 executionPayload, byte[]?[] blobVersionedHashes, Hash256? parentBeaconBlockRoot, byte[][]? executionRequests) + => NewPayload(new ExecutionPayloadParams(executionPayload, blobVersionedHashes, parentBeaconBlockRoot, executionRequests), EngineApiVersions.Amsterdam); + + public Task>> engine_getPayloadBodiesByHashV2(IReadOnlyList blockHashes) + => _executionGetPayloadBodiesByHashV2Handler.Handle(blockHashes); + + public Task>> engine_getPayloadBodiesByRangeV2(long start, long count) + => _executionGetPayloadBodiesByRangeV2Handler.Handle(start, count); +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Paris.cs b/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Paris.cs index 59989242ae3..c3bcea2649c 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Paris.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.Paris.cs @@ -6,14 +6,15 @@ using System.Threading; using System.Threading.Tasks; using Nethermind.Api; -using Nethermind.Blockchain; using Nethermind.Consensus; using Nethermind.Consensus.Producers; +using Nethermind.Core; using Nethermind.Core.Specs; using Nethermind.JsonRpc; using Nethermind.Merge.Plugin.Data; using Nethermind.Merge.Plugin.GC; using Nethermind.Merge.Plugin.Handlers; +using ValidationResult = Nethermind.Merge.Plugin.Data.ValidationResult; namespace Nethermind.Merge.Plugin; diff --git a/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.cs b/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.cs index 9d0edf55a74..03cf7b7053f 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.cs @@ -27,6 +27,7 @@ public EngineRpcModule( IAsyncHandler getPayloadHandlerV3, IAsyncHandler getPayloadHandlerV4, IAsyncHandler getPayloadHandlerV5, + IAsyncHandler getPayloadHandlerV6, IAsyncHandler newPayloadV1Handler, IForkchoiceUpdatedHandler forkchoiceUpdatedV1Handler, IHandler, IEnumerable> executionGetPayloadBodiesByHashV1Handler, @@ -35,6 +36,8 @@ public EngineRpcModule( IHandler, IEnumerable> capabilitiesHandler, IAsyncHandler> getBlobsHandler, IAsyncHandler?> getBlobsHandlerV2, + IHandler, IEnumerable> getPayloadBodiesByHashV2Handler, + IGetPayloadBodiesByRangeV2Handler getPayloadBodiesByRangeV2Handler, IEngineRequestsTracker engineRequestsTracker, ISpecProvider specProvider, GCKeeper gcKeeper, @@ -46,6 +49,7 @@ public EngineRpcModule( _getPayloadHandlerV3 = getPayloadHandlerV3; _getPayloadHandlerV4 = getPayloadHandlerV4; _getPayloadHandlerV5 = getPayloadHandlerV5; + _getPayloadHandlerV6 = getPayloadHandlerV6; _newPayloadV1Handler = newPayloadV1Handler; _forkchoiceUpdatedV1Handler = forkchoiceUpdatedV1Handler; _executionGetPayloadBodiesByHashV1Handler = executionGetPayloadBodiesByHashV1Handler; @@ -53,6 +57,8 @@ public EngineRpcModule( _transitionConfigurationHandler = transitionConfigurationHandler; _getBlobsHandler = getBlobsHandler; _getBlobsHandlerV2 = getBlobsHandlerV2; + _executionGetPayloadBodiesByHashV2Handler = getPayloadBodiesByHashV2Handler; + _executionGetPayloadBodiesByRangeV2Handler = getPayloadBodiesByRangeV2Handler; _engineRequestsTracker = engineRequestsTracker; _specProvider = specProvider ?? throw new ArgumentNullException(nameof(specProvider)); _gcKeeper = gcKeeper; diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/EngineRpcCapabilitiesProvider.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/EngineRpcCapabilitiesProvider.cs index 00e60cf5026..e30b9b930b0 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/EngineRpcCapabilitiesProvider.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/EngineRpcCapabilitiesProvider.cs @@ -9,21 +9,16 @@ namespace Nethermind.HealthChecks; -public class EngineRpcCapabilitiesProvider : IRpcCapabilitiesProvider +public class EngineRpcCapabilitiesProvider(ISpecProvider specProvider) : IRpcCapabilitiesProvider { private readonly ConcurrentDictionary _capabilities = new(); - private readonly ISpecProvider _specProvider; - public EngineRpcCapabilitiesProvider(ISpecProvider specProvider) - { - _specProvider = specProvider; - } public IReadOnlyDictionary GetEngineCapabilities() { if (_capabilities.IsEmpty) { - IReleaseSpec spec = _specProvider.GetFinalSpec(); + IReleaseSpec spec = specProvider.GetFinalSpec(); // The Merge _capabilities[nameof(IEngineRpcModule.engine_exchangeTransitionConfigurationV1)] = (true, false); @@ -54,6 +49,12 @@ public EngineRpcCapabilitiesProvider(ISpecProvider specProvider) _capabilities[nameof(IEngineRpcModule.engine_getPayloadV5)] = (spec.IsEip7594Enabled, spec.IsEip7594Enabled); _capabilities[nameof(IEngineRpcModule.engine_getBlobsV2)] = (spec.IsEip7594Enabled, false); _capabilities[nameof(IEngineRpcModule.engine_getBlobsV3)] = (spec.IsEip7594Enabled, false); + + // Amsterdam + _capabilities[nameof(IEngineRpcModule.engine_getPayloadV6)] = (spec.IsEip7928Enabled, spec.IsEip7928Enabled); + _capabilities[nameof(IEngineRpcModule.engine_newPayloadV5)] = (spec.IsEip7928Enabled, spec.IsEip7928Enabled); + _capabilities[nameof(IEngineRpcModule.engine_getPayloadBodiesByHashV2)] = (spec.IsEip7928Enabled, spec.IsEip7928Enabled); + _capabilities[nameof(IEngineRpcModule.engine_getPayloadBodiesByRangeV2)] = (spec.IsEip7928Enabled, spec.IsEip7928Enabled); } return _capabilities; diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV1Handler.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV1Handler.cs index b61ff36fe75..9b94372d303 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV1Handler.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV1Handler.cs @@ -14,6 +14,7 @@ namespace Nethermind.Merge.Plugin.Handlers; public class GetPayloadBodiesByHashV1Handler(IBlockTree blockTree, ILogManager logManager) : IHandler, IEnumerable> { + protected readonly IBlockTree _blockTree = blockTree; private const int MaxCount = 1024; private readonly ILogger _logger = logManager.GetClassLogger(); @@ -39,7 +40,7 @@ protected bool CheckHashCount(IReadOnlyList blockHashes, out string? er { for (int i = 0; i < blockHashes.Count; i++) { - Block? block = blockTree.FindBlock(blockHashes[i]); + Block? block = _blockTree.FindBlock(blockHashes[i]); yield return block is null ? null : new ExecutionPayloadBodyV1Result(block.Transactions, block.Withdrawals); } } diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV2Handler.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV2Handler.cs new file mode 100644 index 00000000000..adb5c47a217 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByHashV2Handler.cs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Nethermind.Blockchain; +using Nethermind.Blockchain.Headers; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.JsonRpc; +using Nethermind.Logging; +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Merge.Plugin.Handlers; + +public class GetPayloadBodiesByHashV2Handler(IBlockTree blockTree, ILogManager logManager, IBlockAccessListStore balStore) + : GetPayloadBodiesByHashV1Handler(blockTree, logManager), + IHandler, IEnumerable> +{ + public new ResultWrapper> Handle(IReadOnlyList blockHashes) => + !CheckHashCount(blockHashes, out string? error) + ? ResultWrapper>.Fail(error!, MergeErrorCodes.TooLargeRequest) + : ResultWrapper>.Success(GetRequests(blockHashes)); + + private IEnumerable GetRequests(IReadOnlyList blockHashes) + { + for (int i = 0; i < blockHashes.Count; i++) + { + Block? block = _blockTree.FindBlock(blockHashes[i]); + byte[]? bal = balStore.GetRlp(blockHashes[i]); + yield return block is null ? null : new ExecutionPayloadBodyV2Result(block.Transactions, block.Withdrawals, bal); + } + } +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV1Handler.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV1Handler.cs index c3098efb039..e716c257184 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV1Handler.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV1Handler.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Nethermind.Blockchain; +using Nethermind.Core; using Nethermind.JsonRpc; using Nethermind.Logging; using Nethermind.Merge.Plugin.Data; @@ -66,9 +67,8 @@ protected bool CheckRangeCount(long start, long count, out string? error, out in for (long i = start, c = Math.Min(start + count - 1, headNumber); i <= c; i++) { - var block = _blockTree.FindBlock(i); - - yield return (block is null ? null : new ExecutionPayloadBodyV1Result(block.Transactions, block.Withdrawals)); + Block? block = _blockTree.FindBlock(i); + yield return block is null ? null : new ExecutionPayloadBodyV1Result(block.Transactions, block.Withdrawals); } yield break; diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV2Handler.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV2Handler.cs new file mode 100644 index 00000000000..2a82759b052 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadBodiesByRangeV2Handler.cs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Nethermind.Blockchain; +using Nethermind.Blockchain.Headers; +using Nethermind.Core; +using Nethermind.JsonRpc; +using Nethermind.Logging; +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Merge.Plugin.Handlers; + +public class GetPayloadBodiesByRangeV2Handler(BlockTree blockTree, ILogManager logManager, IBlockAccessListStore balStore) + : GetPayloadBodiesByRangeV1Handler(blockTree, logManager), IGetPayloadBodiesByRangeV2Handler +{ + public new Task>> Handle(long start, long count) + { + if (!CheckRangeCount(start, count, out string? error, out int errorCode)) + { + return ResultWrapper>.Fail(error!, errorCode); + } + + return ResultWrapper>.Success(GetRequests(start, count)); + } + + private IEnumerable GetRequests(long start, long count) + { + var headNumber = _blockTree.Head?.Number ?? 0; + + for (long i = start, c = Math.Min(start + count - 1, headNumber); i <= c; i++) + { + Block? block = _blockTree.FindBlock(i); + byte[]? bal = block?.Hash is null ? null : balStore.GetRlp(block.Hash); + yield return block is null ? null : new ExecutionPayloadBodyV2Result(block.Transactions, block.Withdrawals, bal); + } + + yield break; + } +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadV6Handler.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadV6Handler.cs new file mode 100644 index 00000000000..9c5080d54e4 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/GetPayloadV6Handler.cs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Specs; +using Nethermind.Logging; +using Nethermind.Merge.Plugin.BlockProduction; +using Nethermind.Merge.Plugin.Data; +using Nethermind.Consensus.Processing.CensorshipDetector; +using Nethermind.Consensus.Producers; + +namespace Nethermind.Merge.Plugin.Handlers; + +/// +/// +/// engine_getpayloadv3 +/// +public class GetPayloadV6Handler( + IPayloadPreparationService payloadPreparationService, + ISpecProvider specProvider, + ILogManager logManager, + ICensorshipDetector? censorshipDetector = null) + : GetPayloadHandlerBase(6, payloadPreparationService, specProvider, logManager, censorshipDetector) +{ + protected override GetPayloadV6Result GetPayloadResultFromBlock(IBlockProductionContext context) + { + return new(context.CurrentBestBlock!, context.BlockFees, new BlobsBundleV2(context.CurrentBestBlock!), context.CurrentBestBlock!.ExecutionRequests!, ShouldOverrideBuilder(context.CurrentBestBlock!)); + } +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Handlers/IGetPayloadBodiesByRangeV2Handler.cs b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/IGetPayloadBodiesByRangeV2Handler.cs new file mode 100644 index 00000000000..6ab1811b0d5 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/Handlers/IGetPayloadBodiesByRangeV2Handler.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading.Tasks; +using Nethermind.JsonRpc; +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Merge.Plugin.Handlers; + +public interface IGetPayloadBodiesByRangeV2Handler +{ + Task>> Handle(long start, long count); +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/IEngineRpcModule.Amsterdam.cs b/src/Nethermind/Nethermind.Merge.Plugin/IEngineRpcModule.Amsterdam.cs new file mode 100644 index 00000000000..eadf2991205 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/IEngineRpcModule.Amsterdam.cs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading.Tasks; +using Nethermind.Core.Crypto; +using Nethermind.JsonRpc; +using Nethermind.JsonRpc.Modules; +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Merge.Plugin; + +public partial interface IEngineRpcModule : IRpcModule +{ + [JsonRpcMethod( + Description = "Returns the most recent version of an execution payload and fees with respect to the transaction set contained by the mempool.", + IsSharable = true, + IsImplemented = true)] + Task> engine_getPayloadV6(byte[] payloadId); + + [JsonRpcMethod( + Description = "Verifies the payload according to the execution environment rules and returns the verification status and hash of the last valid block.", + IsSharable = true, + IsImplemented = true)] + Task> engine_newPayloadV5(ExecutionPayloadV4 executionPayload, byte[]?[] blobVersionedHashes, Hash256? parentBeaconBlockRoot, byte[][]? executionRequests); + + [JsonRpcMethod( + Description = "Returns an array of execution payload bodies for the list of provided block hashes.", + IsSharable = true, + IsImplemented = true)] + Task>> engine_getPayloadBodiesByHashV2(IReadOnlyList blockHashes); + + [JsonRpcMethod( + Description = "Returns an array of execution payload bodies for the provided number range", + IsSharable = true, + IsImplemented = true)] + Task>> engine_getPayloadBodiesByRangeV2(long start, long count); +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs b/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs index 8c30652c2f4..93a9c0683a0 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs @@ -324,6 +324,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton, GetPayloadV3Handler>() .AddSingleton, GetPayloadV4Handler>() .AddSingleton, GetPayloadV5Handler>() + .AddSingleton, GetPayloadV6Handler>() .AddSingleton, NewPayloadHandler>() .AddSingleton() .AddSingleton, IEnumerable>, GetPayloadBodiesByHashV1Handler>() @@ -333,6 +334,9 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddSingleton>, GetBlobsHandler>() .AddSingleton?>, GetBlobsHandlerV2>() + .AddSingleton, IEnumerable>, GetPayloadBodiesByHashV2Handler>() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton((ctx) => diff --git a/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs b/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs index 4cc7aff8f31..81501672882 100644 --- a/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs @@ -13,6 +13,7 @@ using Nethermind.Core.Test.Container; using Nethermind.Core.Test.Modules; using Nethermind.Evm; +using Nethermind.Evm.State; using Nethermind.Specs.Forks; using NUnit.Framework; diff --git a/src/Nethermind/Nethermind.Runner/packages.lock.json b/src/Nethermind/Nethermind.Runner/packages.lock.json index 140fc267a57..a0c3e3c9695 100644 --- a/src/Nethermind/Nethermind.Runner/packages.lock.json +++ b/src/Nethermind/Nethermind.Runner/packages.lock.json @@ -799,7 +799,8 @@ "Nethermind.Core": "[1.37.0-unstable, )", "Nethermind.Crypto": "[1.37.0-unstable, )", "Nethermind.Serialization.Rlp": "[1.37.0-unstable, )", - "Nethermind.Specs": "[1.37.0-unstable, )" + "Nethermind.Specs": "[1.37.0-unstable, )", + "Nethermind.Trie": "[1.37.0-unstable, )" } }, "nethermind.evm.precompiles": { diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/AccountChangesDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/AccountChangesDecoder.cs new file mode 100644 index 00000000000..e4199f73b9d --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/AccountChangesDecoder.cs @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Int256; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class AccountChangesDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static AccountChangesDecoder? _instance = null; + public static AccountChangesDecoder Instance => _instance ??= new(); + + private static readonly RlpLimit _slotsLimit = new(Eip7928Constants.MaxSlots, "", ReadOnlyMemory.Empty); + private static readonly RlpLimit _storageLimit = new(Eip7928Constants.MaxSlots, "", ReadOnlyMemory.Empty); + private static readonly RlpLimit _txLimit = new(Eip7928Constants.MaxTxs, "", ReadOnlyMemory.Empty); + + public AccountChanges Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + int length = ctx.ReadSequenceLength(); + int check = length + ctx.Position; + + Address address = ctx.DecodeAddress(); + + SlotChanges[] slotChanges = ctx.DecodeArray(SlotChangesDecoder.Instance, true, default, _slotsLimit); + UInt256? lastSlot = null; + SortedList slotChangesList = new(slotChanges.Length); + foreach (SlotChanges slotChange in slotChanges) + { + UInt256 slot = slotChange.Slot; + if (lastSlot is not null && slot <= lastSlot) + { + throw new RlpException("Storage changes were in incorrect order."); + } + lastSlot = slot; + slotChangesList.Add(slot, slotChange); + } + + StorageRead[] storageReads = ctx.DecodeArray(StorageReadDecoder.Instance, true, default, _storageLimit); + SortedSet storageReadsList = []; + StorageRead? lastRead = null; + foreach (StorageRead storageRead in storageReads) + { + if (lastRead is not null && storageRead.CompareTo(lastRead.Value) <= 0) + { + throw new RlpException("Storage reads were in incorrect order."); + } + storageReadsList.Add(storageRead); + lastRead = storageRead; + } + + BalanceChange[] balanceChanges = ctx.DecodeArray(BalanceChangeDecoder.Instance, true, default, _txLimit); + ushort? lastIndex = null; + SortedList balanceChangesList = new(balanceChanges.Length); + foreach (BalanceChange balanceChange in balanceChanges) + { + ushort index = balanceChange.BlockAccessIndex; + if (lastIndex is not null && index <= lastIndex) + { + Console.WriteLine($"Balance changes were in incorrect order. index={index}, lastIndex={lastIndex}"); + throw new RlpException("Balance changes were in incorrect order."); + } + lastIndex = index; + balanceChangesList.Add(index, balanceChange); + } + + lastIndex = null; + NonceChange[] nonceChanges = ctx.DecodeArray(NonceChangeDecoder.Instance, true, default, _txLimit); + SortedList nonceChangesList = new(nonceChanges.Length); + foreach (NonceChange nonceChange in nonceChanges) + { + ushort index = nonceChange.BlockAccessIndex; + if (lastIndex is not null && index <= lastIndex) + { + throw new RlpException("Nonce changes were in incorrect order."); + } + lastIndex = index; + nonceChangesList.Add(index, nonceChange); + } + + CodeChange[] codeChanges = ctx.DecodeArray(CodeChangeDecoder.Instance, true, default, _txLimit); + + lastIndex = null; + SortedList codeChangesList = new(codeChanges.Length); + foreach (CodeChange codeChange in codeChanges) + { + ushort index = codeChange.BlockAccessIndex; + if (lastIndex is not null && index <= lastIndex) + { + throw new RlpException("Code changes were in incorrect order."); + } + lastIndex = index; + codeChangesList.Add(index, codeChange); + } + + if ((rlpBehaviors & RlpBehaviors.AllowExtraBytes) != RlpBehaviors.AllowExtraBytes) + { + ctx.Check(check); + } + + return new(address, slotChangesList, storageReadsList, balanceChangesList, nonceChangesList, codeChangesList); + } + + public int GetLength(AccountChanges item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public void Encode(RlpStream stream, AccountChanges item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + stream.Encode(item.Address); + stream.EncodeArray([.. item.StorageChanges], rlpBehaviors); + stream.EncodeArray([.. item.StorageReads], rlpBehaviors); + stream.EncodeArray([.. item.BalanceChanges], rlpBehaviors); + stream.EncodeArray([.. item.NonceChanges], rlpBehaviors); + stream.EncodeArray([.. item.CodeChanges], rlpBehaviors); + } + + public static int GetContentLength(AccountChanges item, RlpBehaviors rlpBehaviors) + { + int slotChangesLen = 0; + foreach (SlotChanges slotChanges in item.StorageChanges) + { + slotChangesLen += SlotChangesDecoder.Instance.GetLength(slotChanges, rlpBehaviors); + } + slotChangesLen = Rlp.LengthOfSequence(slotChangesLen); + + int storageReadsLen = 0; + foreach (StorageRead storageRead in item.StorageReads) + { + storageReadsLen += StorageReadDecoder.Instance.GetLength(storageRead, rlpBehaviors); + } + storageReadsLen = Rlp.LengthOfSequence(storageReadsLen); + + int balanceChangesLen = 0; + foreach (BalanceChange balanceChange in item.BalanceChanges) + { + balanceChangesLen += BalanceChangeDecoder.Instance.GetLength(balanceChange, rlpBehaviors); + } + balanceChangesLen = Rlp.LengthOfSequence(balanceChangesLen); + + int nonceChangesLen = 0; + foreach (NonceChange nonceChange in item.NonceChanges) + { + nonceChangesLen += NonceChangeDecoder.Instance.GetLength(nonceChange, rlpBehaviors); + } + nonceChangesLen = Rlp.LengthOfSequence(nonceChangesLen); + + int codeChangesLen = 0; + foreach (CodeChange codeChange in item.CodeChanges) + { + codeChangesLen += CodeChangeDecoder.Instance.GetLength(codeChange, rlpBehaviors); + } + codeChangesLen = Rlp.LengthOfSequence(codeChangesLen); + + return Rlp.LengthOfAddressRlp + slotChangesLen + storageReadsLen + balanceChangesLen + nonceChangesLen + codeChangesLen; + } +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/BalanceChangeDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/BalanceChangeDecoder.cs new file mode 100644 index 00000000000..94d7ab8bfe0 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/BalanceChangeDecoder.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.BlockAccessLists; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class BalanceChangeDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static BalanceChangeDecoder? _instance = null; + public static BalanceChangeDecoder Instance => _instance ??= new(); + + public int GetLength(BalanceChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public BalanceChange Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + int length = ctx.ReadSequenceLength(); + int check = length + ctx.Position; + + BalanceChange balanceChange = new() + { + BlockAccessIndex = ctx.DecodeUShort(), + PostBalance = ctx.DecodeUInt256() + }; + + if (!rlpBehaviors.HasFlag(RlpBehaviors.AllowExtraBytes)) + { + ctx.Check(check); + } + + return balanceChange; + } + + public void Encode(RlpStream stream, BalanceChange item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + stream.Encode(item.BlockAccessIndex); + stream.Encode(item.PostBalance); + } + + public static int GetContentLength(BalanceChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOf(item.BlockAccessIndex) + Rlp.LengthOf(item.PostBalance); +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/BlockAccessListDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/BlockAccessListDecoder.cs new file mode 100644 index 00000000000..1f24b62abff --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/BlockAccessListDecoder.cs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class BlockAccessListDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static BlockAccessListDecoder? _instance = null; + public static BlockAccessListDecoder Instance => _instance ??= new(); + + private static readonly RlpLimit _accountsLimit = new(Eip7928Constants.MaxAccounts, "", ReadOnlyMemory.Empty); + + public int GetLength(BlockAccessList item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public BlockAccessList Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + AccountChanges[] accountChanges = ctx.DecodeArray(AccountChangesDecoder.Instance, true, default, _accountsLimit); + + Address? lastAddress = null; + SortedDictionary accountChangesMap = new(accountChanges.ToDictionary(a => + { + Address address = a.Address; + if (lastAddress is not null && address.CompareTo(lastAddress) <= 0) + { + throw new RlpException("Account changes were in incorrect order."); + } + lastAddress = address; + return address; + }, a => a)); + BlockAccessList blockAccessList = new(accountChangesMap); + + return blockAccessList; + } + + public void Encode(RlpStream stream, BlockAccessList item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + => stream.EncodeArray([.. item.AccountChanges], rlpBehaviors); + + private static int GetContentLength(BlockAccessList item, RlpBehaviors rlpBehaviors) + => AccountChangesDecoder.Instance.GetContentLength([.. item.AccountChanges], rlpBehaviors); +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/CodeChangeDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/CodeChangeDecoder.cs new file mode 100644 index 00000000000..ae2e93a782b --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/CodeChangeDecoder.cs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class CodeChangeDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static CodeChangeDecoder? _instance = null; + public static CodeChangeDecoder Instance => _instance ??= new(); + private static readonly RlpLimit _codeLimit = new(Eip7928Constants.MaxCodeSize, "", ReadOnlyMemory.Empty); + + public CodeChange Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + int length = ctx.ReadSequenceLength(); + int check = length + ctx.Position; + + ushort blockAccessIndex = ctx.DecodeUShort(); + byte[] newCode = ctx.DecodeByteArray(_codeLimit); + + CodeChange codeChange = new() + { + BlockAccessIndex = blockAccessIndex, + NewCode = newCode + }; + + if (!rlpBehaviors.HasFlag(RlpBehaviors.AllowExtraBytes)) + { + ctx.Check(check); + } + + return codeChange; + } + + public int GetLength(CodeChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public void Encode(RlpStream stream, CodeChange item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + stream.Encode(item.BlockAccessIndex); + stream.Encode(item.NewCode); + } + + public static int GetContentLength(CodeChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOf(item.BlockAccessIndex) + Rlp.LengthOf(item.NewCode); +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/NonceChangeDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/NonceChangeDecoder.cs new file mode 100644 index 00000000000..8d25e45d5ab --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/NonceChangeDecoder.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.BlockAccessLists; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class NonceChangeDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static NonceChangeDecoder? _instance = null; + public static NonceChangeDecoder Instance => _instance ??= new(); + + public NonceChange Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + int length = ctx.ReadSequenceLength(); + int check = length + ctx.Position; + + NonceChange nonceChange = new() + { + BlockAccessIndex = ctx.DecodeUShort(), + NewNonce = ctx.DecodeULong() + }; + + if (!rlpBehaviors.HasFlag(RlpBehaviors.AllowExtraBytes)) + { + ctx.Check(check); + } + + return nonceChange; + } + + public int GetLength(NonceChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public void Encode(RlpStream stream, NonceChange item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + stream.Encode(item.BlockAccessIndex); + stream.Encode(item.NewNonce); + } + + public static int GetContentLength(NonceChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOf(item.BlockAccessIndex) + Rlp.LengthOf(item.NewNonce); +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/SlotChangesDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/SlotChangesDecoder.cs new file mode 100644 index 00000000000..c1e62c6c73f --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/SlotChangesDecoder.cs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Int256; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class SlotChangesDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static SlotChangesDecoder? _instance = null; + public static SlotChangesDecoder Instance => _instance ??= new(); + + private static readonly RlpLimit _codeLimit = new(Eip7928Constants.MaxCodeSize, "", ReadOnlyMemory.Empty); + + public SlotChanges Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + int length = ctx.ReadSequenceLength(); + int check = length + ctx.Position; + + UInt256 slot = ctx.DecodeUInt256(); + StorageChange[] changes = ctx.DecodeArray(StorageChangeDecoder.Instance, true, default, _codeLimit); + + ushort? lastIndex = null; + SortedList changesList = new(changes.ToDictionary(s => + { + ushort index = s.BlockAccessIndex; + if (lastIndex is not null && index <= lastIndex) + { + throw new RlpException($"Storage changes were in incorrect order. index={index}, lastIndex={lastIndex}"); + } + lastIndex = index; + return index; + }, s => s)); + SlotChanges slotChanges = new(slot, changesList); + + if (!rlpBehaviors.HasFlag(RlpBehaviors.AllowExtraBytes)) + { + ctx.Check(check); + } + + return slotChanges; + } + + public int GetLength(SlotChanges item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public void Encode(RlpStream stream, SlotChanges item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + stream.Encode(item.Slot); + stream.EncodeArray([.. item.Changes.Values], rlpBehaviors); + } + + public static int GetContentLength(SlotChanges item, RlpBehaviors rlpBehaviors) + { + int storageChangesLen = 0; + + foreach (StorageChange slotChange in item.Changes.Values) + { + storageChangesLen += StorageChangeDecoder.Instance.GetLength(slotChange, rlpBehaviors); + } + storageChangesLen = Rlp.LengthOfSequence(storageChangesLen); + + return storageChangesLen + Rlp.LengthOf(item.Slot); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/StorageChangeDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/StorageChangeDecoder.cs new file mode 100644 index 00000000000..237b83b1dfe --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/StorageChangeDecoder.cs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.BlockAccessLists; +using Nethermind.Int256; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class StorageChangeDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static StorageChangeDecoder? _instance = null; + public static StorageChangeDecoder Instance => _instance ??= new(); + + public StorageChange Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) + { + int length = ctx.ReadSequenceLength(); + int check = length + ctx.Position; + + ushort blockAccessIndex = ctx.DecodeUShort(); + UInt256 newValue = ctx.DecodeUInt256(); + StorageChange storageChange = new() + { + BlockAccessIndex = blockAccessIndex, + NewValue = newValue + }; + + if (!rlpBehaviors.HasFlag(RlpBehaviors.AllowExtraBytes)) + { + ctx.Check(check); + } + + return storageChange; + } + + public int GetLength(StorageChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + public void Encode(RlpStream stream, StorageChange item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + stream.Encode(item.BlockAccessIndex); + stream.Encode(item.NewValue); + } + + public static int GetContentLength(StorageChange item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOf(item.BlockAccessIndex) + Rlp.LengthOf(item.NewValue); +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/StorageReadDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/StorageReadDecoder.cs new file mode 100644 index 00000000000..25d9ee6759f --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Eip7928/StorageReadDecoder.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.BlockAccessLists; + +namespace Nethermind.Serialization.Rlp.Eip7928; + +public class StorageReadDecoder : IRlpValueDecoder, IRlpStreamEncoder +{ + private static StorageReadDecoder? _instance = null; + public static StorageReadDecoder Instance => _instance ??= new(); + + public int GetLength(StorageRead item, RlpBehaviors rlpBehaviors) + => GetContentLength(item, rlpBehaviors); + + public StorageRead Decode(ref Rlp.ValueDecoderContext ctx, RlpBehaviors rlpBehaviors) => new(ctx.DecodeUInt256()); + + public void Encode(RlpStream stream, StorageRead item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + => stream.Encode(item.Key); + + public static int GetContentLength(StorageRead item, RlpBehaviors rlpBehaviors) + => Rlp.LengthOf(item.Key); +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/HeaderDecoder.cs b/src/Nethermind/Nethermind.Serialization.Rlp/HeaderDecoder.cs index 919224640b8..f69c215460f 100644 --- a/src/Nethermind/Nethermind.Serialization.Rlp/HeaderDecoder.cs +++ b/src/Nethermind/Nethermind.Serialization.Rlp/HeaderDecoder.cs @@ -81,6 +81,7 @@ public HeaderDecoder() { } if (decoderContext.Position != headerCheck) blockHeader.ExcessBlobGas = decoderContext.DecodeULong(); if (decoderContext.Position != headerCheck) blockHeader.ParentBeaconBlockRoot = decoderContext.DecodeKeccak(); if (decoderContext.Position != headerCheck) blockHeader.RequestsHash = decoderContext.DecodeKeccak(); + if (decoderContext.Position != headerCheck) blockHeader.BlockAccessListHash = decoderContext.DecodeKeccak(); if ((rlpBehaviors & RlpBehaviors.AllowExtraBytes) != RlpBehaviors.AllowExtraBytes) { @@ -129,15 +130,16 @@ public override void Encode(RlpStream rlpStream, BlockHeader? header, RlpBehavio } } - Span requiredItems = stackalloc bool[6]; + Span requiredItems = stackalloc bool[7]; requiredItems[0] = !header.BaseFeePerGas.IsZero; - requiredItems[1] = (header.WithdrawalsRoot is not null); - requiredItems[2] = (header.BlobGasUsed is not null); - requiredItems[3] = (header.BlobGasUsed is not null || header.ExcessBlobGas is not null); - requiredItems[4] = (header.ParentBeaconBlockRoot is not null); - requiredItems[5] = (header.RequestsHash is not null); - - for (int i = 4; i >= 0; i--) + requiredItems[1] = header.WithdrawalsRoot is not null; + requiredItems[2] = header.BlobGasUsed is not null; + requiredItems[3] = header.BlobGasUsed is not null || header.ExcessBlobGas is not null; + requiredItems[4] = header.ParentBeaconBlockRoot is not null; + requiredItems[5] = header.RequestsHash is not null; + requiredItems[6] = header.BlockAccessListHash is not null; + + for (int i = 5; i >= 0; i--) { requiredItems[i] |= requiredItems[i + 1]; } @@ -148,6 +150,7 @@ public override void Encode(RlpStream rlpStream, BlockHeader? header, RlpBehavio if (requiredItems[3]) rlpStream.Encode(header.ExcessBlobGas.GetValueOrDefault()); if (requiredItems[4]) rlpStream.Encode(header.ParentBeaconBlockRoot); if (requiredItems[5]) rlpStream.Encode(header.RequestsHash); + if (requiredItems[6]) rlpStream.Encode(header.BlockAccessListHash); } public Rlp Encode(BlockHeader? item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) @@ -202,15 +205,16 @@ private static int GetContentLength(BlockHeader? item, RlpBehaviors rlpBehaviors } - Span requiredItems = stackalloc bool[6]; + Span requiredItems = stackalloc bool[7]; requiredItems[0] = !item.BaseFeePerGas.IsZero; - requiredItems[1] = (item.WithdrawalsRoot is not null); - requiredItems[2] = (item.BlobGasUsed is not null); - requiredItems[3] = (item.BlobGasUsed is not null || item.ExcessBlobGas is not null); - requiredItems[4] = (item.ParentBeaconBlockRoot is not null); - requiredItems[5] = (item.RequestsHash is not null); - - for (int i = 4; i >= 0; i--) + requiredItems[1] = item.WithdrawalsRoot is not null; + requiredItems[2] = item.BlobGasUsed is not null; + requiredItems[3] = item.BlobGasUsed is not null || item.ExcessBlobGas is not null; + requiredItems[4] = item.ParentBeaconBlockRoot is not null; + requiredItems[5] = item.RequestsHash is not null; + requiredItems[6] = item.BlockAccessListHash is not null; + + for (int i = 5; i >= 0; i--) { requiredItems[i] |= requiredItems[i + 1]; } @@ -221,12 +225,12 @@ private static int GetContentLength(BlockHeader? item, RlpBehaviors rlpBehaviors if (requiredItems[3]) contentLength += Rlp.LengthOf(item.ExcessBlobGas.GetValueOrDefault()); if (requiredItems[4]) contentLength += Rlp.LengthOf(item.ParentBeaconBlockRoot); if (requiredItems[5]) contentLength += Rlp.LengthOf(item.RequestsHash); + if (requiredItems[6]) contentLength += Rlp.LengthOf(item.BlockAccessListHash); + return contentLength; } public override int GetLength(BlockHeader? item, RlpBehaviors rlpBehaviors) - { - return Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); - } + => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); } } diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/Rlp.cs b/src/Nethermind/Nethermind.Serialization.Rlp/Rlp.cs index c174398acae..2f68a1f98c7 100644 --- a/src/Nethermind/Nethermind.Serialization.Rlp/Rlp.cs +++ b/src/Nethermind/Nethermind.Serialization.Rlp/Rlp.cs @@ -1319,6 +1319,45 @@ public byte[][] DecodeByteArrays() return result; } + public ushort DecodeUShort() + { + int prefix = ReadByte(); + + switch (prefix) + { + case 0: + throw new RlpException($"Non-canonical ushort (leading zero bytes) at position {Position}"); + case < 128: + return (ushort)prefix; + case 128: + return 0; + } + + int length = prefix - 128; + if (length > 8) + { + throw new RlpException($"Unexpected length of ushort value: {length}"); + } + + ushort result = 0; + for (int i = 2; i > 0; i--) + { + result <<= 8; + if (i <= length) + { + result |= PeekByte(length - i); + if (result == 0) + { + throw new RlpException($"Non-canonical ushort (leading zero bytes) at position {Position}"); + } + } + } + + SkipBytes(length); + + return result; + } + public byte DecodeByte() { byte byteValue = PeekByte(); @@ -1540,6 +1579,8 @@ public static int LengthOf(ulong value) public static int LengthOf(int value) => LengthOf((long)value); + public static int LengthOf(ushort value) => LengthOf((long)value); + public static int LengthOf(Hash256? item) => item is null ? 1 : 33; public static int LengthOf(in ValueHash256? item) => item is null ? 1 : 33; diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs b/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs index a9fbeb86bac..4f07603f089 100644 --- a/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs +++ b/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs @@ -10,11 +10,13 @@ using System.Runtime.InteropServices; using System.Text; using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; using Nethermind.Core.Buffers; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Int256; +using Nethermind.Serialization.Rlp.Eip7928; namespace Nethermind.Serialization.Rlp { @@ -25,6 +27,7 @@ public class RlpStream private static readonly BlockInfoDecoder _blockInfoDecoder = new(); private static readonly TxDecoder _txDecoder = TxDecoder.Instance; private static readonly WithdrawalDecoder _withdrawalDecoder = new(); + private static readonly BlockAccessListDecoder _blockAccessListDecoder = BlockAccessListDecoder.Instance; private static readonly LogEntryDecoder _logEntryDecoder = LogEntryDecoder.Instance; internal static ReadOnlySpan SingleBytes => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]; @@ -86,6 +89,8 @@ public void Encode(Transaction value, RlpBehaviors rlpBehaviors = RlpBehaviors.N public void Encode(BlockInfo value) => _blockInfoDecoder.Encode(this, value); + public void Encode(BlockAccessList value) => _blockAccessListDecoder.Encode(this, value); + public void StartByteArray(int contentLength, bool firstByteLessThan128) { switch (contentLength) diff --git a/src/Nethermind/Nethermind.Specs.Test/ChainParametersTests.cs b/src/Nethermind/Nethermind.Specs.Test/ChainParametersTests.cs index e7a6537c900..ed856bc0221 100644 --- a/src/Nethermind/Nethermind.Specs.Test/ChainParametersTests.cs +++ b/src/Nethermind/Nethermind.Specs.Test/ChainParametersTests.cs @@ -38,7 +38,8 @@ public void ChainParameters_should_be_loaded_from_chainSpecParamsJson() "MaxCodeSizeTransitionTimestamp", "Eip4844FeeCollectorTransitionTimestamp", "Eip6110TransitionTimestamp", - "Eip7692TransitionTimestamp" + "Eip7692TransitionTimestamp", + "Eip7928TransitionTimestamp" // todo: remove when added to chainspec ]; const ulong testValue = 1ul; diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index 2183a3ce5c5..1557e4c80c6 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -119,6 +119,7 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public bool IsEip7939Enabled { get; set; } = spec.IsEip7939Enabled; public bool IsEip7907Enabled { get; set; } = spec.IsEip7907Enabled; public bool IsRip7728Enabled { get; set; } = spec.IsRip7728Enabled; + public bool IsEip7928Enabled { get; set; } = spec.IsEip7928Enabled; public SpecGasCosts GasCosts => new(this); FrozenSet IReleaseSpec.Precompiles => spec.Precompiles; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index a1a02a58ea9..2614f80914b 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -174,4 +174,5 @@ public class ChainParameters #endregion public ulong? Rip7728TransitionTimestamp { get; set; } + public ulong? Eip7928TransitionTimestamp { get; set; } } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpec.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpec.cs index b46187d7263..ee3133cf23f 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpec.cs @@ -81,5 +81,6 @@ public class ChainSpec public ulong? PragueTimestamp { get; set; } public ulong? OsakaTimestamp { get; set; } + public ulong? AmsterdamTimestamp { get; set; } } } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index 9ab4f76e408..5c98217818e 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -294,6 +294,8 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsRip7728Enabled = (chainSpec.Parameters.Rip7728TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + releaseSpec.IsEip7928Enabled = (chainSpec.Parameters.Eip7928TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + foreach (IChainSpecEngineParameters item in _chainSpec.EngineChainSpecParametersProvider .AllChainSpecParameters) { diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs index 513dc6a9b43..b5922d94309 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -200,6 +200,8 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip7934MaxRlpBlockSize = chainSpecJson.Params.Eip7934MaxRlpBlockSize ?? Eip7934Constants.DefaultMaxRlpBlockSize, Rip7728TransitionTimestamp = chainSpecJson.Params.Rip7728TransitionTimestamp, + + Eip7928TransitionTimestamp = chainSpecJson.Params.Eip7928TransitionTimestamp, }; chainSpec.Parameters.Eip152Transition ??= GetTransitionForExpectedPricing("blake2_f", "price.blake2_f.gas_per_round", 1); @@ -260,6 +262,7 @@ chainSpec.Parameters.Eip1283DisableTransition is null chainSpec.CancunTimestamp = chainSpec.Parameters.Eip4844TransitionTimestamp; chainSpec.PragueTimestamp = chainSpec.Parameters.Eip7002TransitionTimestamp; chainSpec.OsakaTimestamp = chainSpec.Parameters.Eip7594TransitionTimestamp; + chainSpec.AmsterdamTimestamp = chainSpec.Parameters.Eip7928TransitionTimestamp; // TheMerge parameters chainSpec.MergeForkIdBlockNumber = chainSpec.Parameters.MergeForkIdTransition; @@ -331,27 +334,35 @@ private static void LoadGenesis(ChainSpecJson chainSpecJson, ChainSpec chainSpec 0, (long)gasLimit, timestamp, - extraData); - - genesisHeader.Author = beneficiary; - genesisHeader.Hash = Keccak.Zero; // need to run the block to know the actual hash - genesisHeader.Bloom = Bloom.Empty; - genesisHeader.MixHash = mixHash; - genesisHeader.Nonce = (ulong)nonce; - genesisHeader.ReceiptsRoot = Keccak.EmptyTreeHash; - genesisHeader.StateRoot = stateRoot; - genesisHeader.TxRoot = Keccak.EmptyTreeHash; - genesisHeader.BaseFeePerGas = baseFee; + extraData) + { + Author = beneficiary, + Hash = Keccak.Zero, // need to run the block to know the actual hash + Bloom = Bloom.Empty, + MixHash = mixHash, + Nonce = (ulong)nonce, + ReceiptsRoot = Keccak.EmptyTreeHash, + StateRoot = stateRoot, + TxRoot = Keccak.EmptyTreeHash, + BaseFeePerGas = baseFee + }; + bool withdrawalsEnabled = chainSpecJson.Params.Eip4895TransitionTimestamp is not null && genesisHeader.Timestamp >= chainSpecJson.Params.Eip4895TransitionTimestamp; bool depositsEnabled = chainSpecJson.Params.Eip6110TransitionTimestamp is not null && genesisHeader.Timestamp >= chainSpecJson.Params.Eip6110TransitionTimestamp; bool withdrawalRequestsEnabled = chainSpecJson.Params.Eip7002TransitionTimestamp is not null && genesisHeader.Timestamp >= chainSpecJson.Params.Eip7002TransitionTimestamp; bool consolidationRequestsEnabled = chainSpecJson.Params.Eip7251TransitionTimestamp is not null && genesisHeader.Timestamp >= chainSpecJson.Params.Eip7251TransitionTimestamp; + bool blockAccessListsEnabled = chainSpecJson.Params.Eip7928TransitionTimestamp is not null && genesisHeader.Timestamp >= chainSpecJson.Params.Eip7928TransitionTimestamp; + if (withdrawalsEnabled) + { genesisHeader.WithdrawalsRoot = Keccak.EmptyTreeHash; + } var requestsEnabled = depositsEnabled || withdrawalRequestsEnabled || consolidationRequestsEnabled; if (requestsEnabled) + { genesisHeader.RequestsHash = ExecutionRequestExtensions.EmptyRequestsHash; + } bool isEip4844Enabled = chainSpecJson.Params.Eip4844TransitionTimestamp is not null && genesisHeader.Timestamp >= chainSpecJson.Params.Eip4844TransitionTimestamp; if (isEip4844Enabled) @@ -371,16 +382,19 @@ private static void LoadGenesis(ChainSpecJson chainSpecJson, ChainSpec chainSpec genesisHeader.ReceiptsRoot = Keccak.EmptyTreeHash; } + if (blockAccessListsEnabled) + { + genesisHeader.BlockAccessListHash = Keccak.OfAnEmptySequenceRlp; + } + genesisHeader.AuRaStep = step; genesisHeader.AuRaSignature = auRaSignature; - chainSpec.Genesis = !withdrawalsEnabled - ? new Block(genesisHeader) - : new Block( - genesisHeader, - Array.Empty(), - Array.Empty(), - Array.Empty()); + chainSpec.Genesis = !blockAccessListsEnabled ? + (!withdrawalsEnabled + ? new Block(genesisHeader) + : new Block(genesisHeader, [], [], [])) + : new Block(genesisHeader, [], [], [], new()); } private static void LoadAllocations(ChainSpecJson chainSpecJson, ChainSpec chainSpec) diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index 442b3cffbb8..e7dfbb04271 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -176,4 +176,5 @@ public class ChainSpecParamsJson public ulong? Eip7594TransitionTimestamp { get; set; } public ulong? Eip7939TransitionTimestamp { get; set; } public ulong? Rip7728TransitionTimestamp { get; set; } + public ulong? Eip7928TransitionTimestamp { get; set; } } diff --git a/src/Nethermind/Nethermind.Specs/Forks/20_BPO1.cs b/src/Nethermind/Nethermind.Specs/Forks/20_BPO1.cs index 7a789986a8a..9ba38827d61 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/20_BPO1.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/20_BPO1.cs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only using System.Threading; diff --git a/src/Nethermind/Nethermind.Specs/Forks/21_BPO12.cs b/src/Nethermind/Nethermind.Specs/Forks/21_BPO2.cs similarity index 89% rename from src/Nethermind/Nethermind.Specs/Forks/21_BPO12.cs rename to src/Nethermind/Nethermind.Specs/Forks/21_BPO2.cs index 9b05179a601..bcf7394da89 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/21_BPO12.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/21_BPO2.cs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only using System.Threading; diff --git a/src/Nethermind/Nethermind.Specs/Forks/22_BPO3.cs b/src/Nethermind/Nethermind.Specs/Forks/22_BPO3.cs new file mode 100644 index 00000000000..61ef05adeb6 --- /dev/null +++ b/src/Nethermind/Nethermind.Specs/Forks/22_BPO3.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading; +using Nethermind.Core.Specs; + +namespace Nethermind.Specs.Forks; + +public class BPO3 : BPO2 +{ + private static IReleaseSpec _instance; + + public BPO3() + { + Name = "bpo3"; + MaxBlobCount = 32; + TargetBlobCount = 21; + BlobBaseFeeUpdateFraction = 17805213; + Released = false; + } + + public new static IReleaseSpec Instance => LazyInitializer.EnsureInitialized(ref _instance, static () => new BPO3()); +} diff --git a/src/Nethermind/Nethermind.Specs/Forks/23_BPO4.cs b/src/Nethermind/Nethermind.Specs/Forks/23_BPO4.cs new file mode 100644 index 00000000000..5d0b7d0dd5c --- /dev/null +++ b/src/Nethermind/Nethermind.Specs/Forks/23_BPO4.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading; +using Nethermind.Core.Specs; + +namespace Nethermind.Specs.Forks; + +public class BPO4 : BPO3 +{ + private static IReleaseSpec _instance; + + public BPO4() + { + Name = "bpo4"; + MaxBlobCount = 48; + TargetBlobCount = 32; + BlobBaseFeeUpdateFraction = 26707819; + Released = false; + } + + public new static IReleaseSpec Instance => LazyInitializer.EnsureInitialized(ref _instance, static () => new BPO4()); +} diff --git a/src/Nethermind/Nethermind.Specs/Forks/24_BPO5.cs b/src/Nethermind/Nethermind.Specs/Forks/24_BPO5.cs new file mode 100644 index 00000000000..1e730e1a725 --- /dev/null +++ b/src/Nethermind/Nethermind.Specs/Forks/24_BPO5.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading; +using Nethermind.Core.Specs; + +namespace Nethermind.Specs.Forks; + +public class BPO5 : BPO4 +{ + private static IReleaseSpec _instance; + + public BPO5() + { + Name = "bpo5"; + MaxBlobCount = 72; + TargetBlobCount = 48; + BlobBaseFeeUpdateFraction = 40061729; + Released = false; + } + + public new static IReleaseSpec Instance => LazyInitializer.EnsureInitialized(ref _instance, static () => new BPO5()); +} diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs new file mode 100644 index 00000000000..9aaf000282f --- /dev/null +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading; +using Nethermind.Core.Specs; + +namespace Nethermind.Specs.Forks; + +public class Amsterdam : BPO5 +{ + private static IReleaseSpec _instance; + + public Amsterdam() + { + Name = "Amsterdam"; + IsEip7928Enabled = true; + Released = false; + } + + public new static IReleaseSpec Instance => LazyInitializer.EnsureInitialized(ref _instance, static () => new Amsterdam()); +} diff --git a/src/Nethermind/Nethermind.Specs/MainnetSpecProvider.cs b/src/Nethermind/Nethermind.Specs/MainnetSpecProvider.cs index 948e8ebd869..4b4145ce62c 100644 --- a/src/Nethermind/Nethermind.Specs/MainnetSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/MainnetSpecProvider.cs @@ -30,6 +30,10 @@ public class MainnetSpecProvider : ISpecProvider public const ulong OsakaBlockTimestamp = 0x6930b057; public const ulong BPO1BlockTimestamp = 0x69383057; public const ulong BPO2BlockTimestamp = 0x695db057; + public const ulong BPO3BlockTimestamp = ulong.MaxValue - 3; + public const ulong BPO4BlockTimestamp = ulong.MaxValue - 2; + public const ulong BPO5BlockTimestamp = ulong.MaxValue - 1; + public const ulong AmsterdamBlockTimestamp = ulong.MaxValue; public IReleaseSpec GetSpec(ForkActivation forkActivation) => forkActivation switch @@ -53,7 +57,11 @@ public IReleaseSpec GetSpec(ForkActivation forkActivation) => { Timestamp: < OsakaBlockTimestamp } => Prague.Instance, { Timestamp: < BPO1BlockTimestamp } => Osaka.Instance, { Timestamp: < BPO2BlockTimestamp } => BPO1.Instance, - _ => BPO2.Instance + { Timestamp: < BPO3BlockTimestamp } => BPO2.Instance, + { Timestamp: < BPO4BlockTimestamp } => BPO3.Instance, + { Timestamp: < BPO5BlockTimestamp } => BPO4.Instance, + { Timestamp: < AmsterdamBlockTimestamp } => BPO5.Instance, + _ => Amsterdam.Instance }; public void UpdateMergeTransitionInfo(long? blockNumber, UInt256? terminalTotalDifficulty = null) @@ -80,6 +88,10 @@ public void UpdateMergeTransitionInfo(long? blockNumber, UInt256? terminalTotalD public static ForkActivation OsakaActivation { get; } = (ParisBlockNumber + 4, OsakaBlockTimestamp); public static ForkActivation BPO1Activation { get; } = (ParisBlockNumber + 5, BPO1BlockTimestamp); public static ForkActivation BPO2Activation { get; } = (ParisBlockNumber + 6, BPO2BlockTimestamp); + public static ForkActivation BPO3Activation { get; } = (ParisBlockNumber + 7, BPO3BlockTimestamp); + public static ForkActivation BPO4Activation { get; } = (ParisBlockNumber + 8, BPO4BlockTimestamp); + public static ForkActivation BPO5Activation { get; } = (ParisBlockNumber + 9, BPO5BlockTimestamp); + public static ForkActivation AmsterdamActivation { get; } = (ParisBlockNumber + 10, AmsterdamBlockTimestamp); public ForkActivation[] TransitionActivations { get; } = { (ForkActivation)HomesteadBlockNumber, diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index b295ccd2c0b..0e752f6cd07 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -164,6 +164,7 @@ public virtual FrozenSet BuildPrecompilesCache() return cache.ToFrozenSet(); } + public bool IsEip7928Enabled { get; set; } private ReleaseSpec? _systemSpec; diff --git a/src/Nethermind/Nethermind.Specs/SpecNameParser.cs b/src/Nethermind/Nethermind.Specs/SpecNameParser.cs index 0b4abd07435..a4030e1274c 100644 --- a/src/Nethermind/Nethermind.Specs/SpecNameParser.cs +++ b/src/Nethermind/Nethermind.Specs/SpecNameParser.cs @@ -54,6 +54,12 @@ public static IReleaseSpec Parse(string specName) "Paris" => Paris.Instance, "Prague" => Prague.Instance, "Osaka" => Osaka.Instance, + "BPO1" => BPO1.Instance, + "BPO2" => BPO2.Instance, + "BPO3" => BPO3.Instance, + "BPO4" => BPO4.Instance, + "BPO5" => BPO5.Instance, + "Amsterdam" => Amsterdam.Instance, _ => throw new NotSupportedException() }; } diff --git a/src/Nethermind/Nethermind.State.Test/StateProviderTests.cs b/src/Nethermind/Nethermind.State.Test/StateProviderTests.cs index 1584a548eaf..4233a79b859 100644 --- a/src/Nethermind/Nethermind.State.Test/StateProviderTests.cs +++ b/src/Nethermind/Nethermind.State.Test/StateProviderTests.cs @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +#nullable enable + using System; using Autofac; using FluentAssertions; @@ -33,7 +35,7 @@ public class StateProviderTests(bool useFlat) private class Context : IDisposable { public IWorldState WorldState { get; } - private readonly IContainer _container; + private readonly IContainer? _container; public Context(bool useFlat) { @@ -79,7 +81,8 @@ public void Eip_158_zero_value_transfer_deletes() public void Eip_158_touch_zero_value_system_account_is_not_deleted() { using Context ctx = new(useFlat); - IWorldState provider = ctx.WorldState; + ParallelWorldState? parallelWorldState = ctx.WorldState as ParallelWorldState; + IWorldState provider = parallelWorldState is null ? ctx.WorldState : parallelWorldState.Inner; using var _ = provider.BeginScope(IWorldState.PreGenesis); var systemUser = Address.SystemUser; @@ -130,7 +133,7 @@ public void Returns_empty_byte_code_for_non_existing_accounts() using Context ctx = new(useFlat); IWorldState provider = ctx.WorldState; using var _ = provider.BeginScope(IWorldState.PreGenesis); - byte[] code = provider.GetCode(TestItem.AddressA); + byte[] code = provider.GetCode(TestItem.AddressA)!; code.Should().BeEmpty(); } diff --git a/src/Nethermind/Nethermind.State/ParallelWorldState.cs b/src/Nethermind/Nethermind.State/ParallelWorldState.cs new file mode 100644 index 00000000000..55b66dcefa8 --- /dev/null +++ b/src/Nethermind/Nethermind.State/ParallelWorldState.cs @@ -0,0 +1,375 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using Nethermind.Core; +using Nethermind.Core.BlockAccessLists; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Int256; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Evm.State; + +public class ParallelWorldState(IWorldState innerWorldState) : WrappedWorldState(innerWorldState), IBlockAccessListBuilder, IPreBlockCaches +{ + public bool TracingEnabled { get; set; } = false; + public BlockAccessList GeneratedBlockAccessList { get; set; } = new(); + + public PreBlockCaches? Caches => (_innerWorldState as IPreBlockCaches)?.Caches; + + public bool IsWarmWorldState => (_innerWorldState as IPreBlockCaches)?.IsWarmWorldState ?? false; + public class InvalidBlockLevelAccessListException(BlockHeader block, string message) : InvalidBlockException(block, "InvalidBlockLevelAccessList: " + message); + + private BlockAccessList _suggestedBlockAccessList; + private long _gasUsed; + + public void LoadSuggestedBlockAccessList(BlockAccessList suggested, long gasUsed) + { + _suggestedBlockAccessList = suggested; + _gasUsed = gasUsed; + } + + public override void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => AddToBalance(address, balanceChange, spec, out _); + + public override void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + _innerWorldState.AddToBalance(address, balanceChange, spec, out oldBalance); + + if (TracingEnabled) + { + UInt256 newBalance = oldBalance + balanceChange; + GeneratedBlockAccessList.AddBalanceChange(address, oldBalance, newBalance); + } + } + + public override bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out _); + + public override bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + bool res = _innerWorldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out oldBalance); + + if (TracingEnabled) + { + UInt256 newBalance = oldBalance + balanceChange; + GeneratedBlockAccessList.AddBalanceChange(address, oldBalance, newBalance); + } + + return res; + } + + public override IDisposable BeginScope(BlockHeader? baseBlock) + => _innerWorldState.BeginScope(baseBlock); + + public override ReadOnlySpan Get(in StorageCell storageCell) + { + if (TracingEnabled) + { + GeneratedBlockAccessList.AddStorageRead(storageCell); + } + return _innerWorldState.Get(storageCell); + } + + public override void IncrementNonce(Address address, UInt256 delta) + => IncrementNonce(address, delta, out _); + + public override void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) + { + _innerWorldState.IncrementNonce(address, delta, out oldNonce); + + if (TracingEnabled) + { + GeneratedBlockAccessList.AddNonceChange(address, (ulong)(oldNonce + delta)); + } + } + + public override void SetNonce(Address address, in UInt256 nonce) + { + _innerWorldState.SetNonce(address, nonce); + + if (TracingEnabled) + { + GeneratedBlockAccessList.AddNonceChange(address, (ulong)nonce); + } + } + + public override bool InsertCode(Address address, in ValueHash256 codeHash, ReadOnlyMemory code, IReleaseSpec spec, bool isGenesis = false) + { + if (TracingEnabled) + { + byte[] oldCode = _innerWorldState.GetCode(address) ?? Array.Empty(); + GeneratedBlockAccessList.AddCodeChange(address, oldCode, code); + } + return _innerWorldState.InsertCode(address, codeHash, code, spec, isGenesis); + } + + public override void Set(in StorageCell storageCell, byte[] newValue) + { + if (TracingEnabled) + { + ReadOnlySpan oldValue = _innerWorldState.Get(storageCell); + GeneratedBlockAccessList.AddStorageChange(storageCell, new(oldValue, true), new(newValue, true)); + } + _innerWorldState.Set(storageCell, newValue); + } + + public override ref readonly UInt256 GetBalance(Address address) + { + AddAccountRead(address); + return ref _innerWorldState.GetBalance(address); + } + + public UInt256 GetNonce(Address address) + { + AddAccountRead(address); + return _innerWorldState.GetNonce(address); + } + + public bool HasCode(Address address) + { + AddAccountRead(address); + return _innerWorldState.HasCode(address); + } + + public override ref readonly ValueHash256 GetCodeHash(Address address) + { + AddAccountRead(address); + return ref _innerWorldState.GetCodeHash(address); + } + + public override byte[]? GetCode(Address address) + { + AddAccountRead(address); + return _innerWorldState.GetCode(address); + } + + public override void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => SubtractFromBalance(address, balanceChange, spec, out _); + + public override void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + _innerWorldState.SubtractFromBalance(address, balanceChange, spec, out oldBalance); + + if (TracingEnabled) + { + UInt256 newBalance = oldBalance - balanceChange; + GeneratedBlockAccessList.AddBalanceChange(address, oldBalance, newBalance); + } + } + + public override void DeleteAccount(Address address) + { + if (TracingEnabled) + { + GeneratedBlockAccessList.DeleteAccount(address, _innerWorldState.GetBalance(address)); + } + _innerWorldState.DeleteAccount(address); + } + + public override void CreateAccount(Address address, in UInt256 balance, in UInt256 nonce = default) + { + if (TracingEnabled) + { + GeneratedBlockAccessList.AddAccountRead(address); + if (!balance.IsZero) + { + GeneratedBlockAccessList.AddBalanceChange(address, 0, balance); + } + if (!nonce.IsZero) + { + GeneratedBlockAccessList.AddNonceChange(address, (ulong)nonce); + } + } + _innerWorldState.CreateAccount(address, balance, nonce); + } + + public override void CreateAccountIfNotExists(Address address, in UInt256 balance, in UInt256 nonce = default) + { + if (!_innerWorldState.AccountExists(address)) + { + CreateAccount(address, balance, nonce); + } + } + + public override bool TryGetAccount(Address address, out AccountStruct account) + { + AddAccountRead(address); + return _innerWorldState.TryGetAccount(address, out account); + } + + public void AddAccountRead(Address address) + { + if (TracingEnabled) + { + GeneratedBlockAccessList.AddAccountRead(address); + } + } + + public override void Restore(Snapshot snapshot) + { + if (TracingEnabled) + { + GeneratedBlockAccessList.Restore(snapshot.BlockAccessListSnapshot); + } + _innerWorldState.Restore(snapshot); + } + + public override Snapshot TakeSnapshot(bool newTransactionStart = false) + { + int blockAccessListSnapshot = GeneratedBlockAccessList.TakeSnapshot(); + Snapshot snapshot = _innerWorldState.TakeSnapshot(newTransactionStart); + return new(snapshot.StorageSnapshot, snapshot.StateSnapshot, blockAccessListSnapshot); + } + + public override bool AccountExists(Address address) + { + AddAccountRead(address); + return _innerWorldState.AccountExists(address); + } + + public override bool IsContract(Address address) + { + AddAccountRead(address); + return _innerWorldState.IsContract(address); + } + + public override bool IsDeadAccount(Address address) + { + AddAccountRead(address); + return _innerWorldState.IsDeadAccount(address); + } + + public override void ClearStorage(Address address) + { + AddAccountRead(address); + _innerWorldState.ClearStorage(address); + } + + public long GasUsed() + => _gasUsed; + + public void ValidateBlockAccessList(BlockHeader block, ushort index, long gasRemaining) + { + if (_suggestedBlockAccessList is null) + { + return; + } + + IEnumerator generatedChanges = GeneratedBlockAccessList.GetChangesAtIndex(index).GetEnumerator(); + IEnumerator suggestedChanges = _suggestedBlockAccessList.GetChangesAtIndex(index).GetEnumerator(); + + ChangeAtIndex? generatedHead; + ChangeAtIndex? suggestedHead; + + int generatedReads = 0; + int suggestedReads = 0; + + void AdvanceGenerated() + { + generatedHead = generatedChanges.MoveNext() ? generatedChanges.Current : null; + if (generatedHead is not null) generatedReads += generatedHead.Value.Reads; + } + + void AdvanceSuggested() + { + suggestedHead = suggestedChanges.MoveNext() ? suggestedChanges.Current : null; + if (suggestedHead is not null) suggestedReads += suggestedHead.Value.Reads; + } + + AdvanceGenerated(); + AdvanceSuggested(); + + while (generatedHead is not null || suggestedHead is not null) + { + if (suggestedHead is null) + { + if (HasNoChanges(generatedHead.Value)) + { + AdvanceGenerated(); + continue; + } + throw new InvalidBlockLevelAccessListException(block, $"Suggested block-level access list missing account changes for {generatedHead.Value.Address} at index {index}."); + } + else if (generatedHead is null) + { + if (HasNoChanges(suggestedHead.Value)) + { + AdvanceSuggested(); + continue; + } + throw new InvalidBlockLevelAccessListException(block, $"Suggested block-level access list contained surplus changes for {suggestedHead.Value.Address} at index {index}."); + } + + int cmp = generatedHead.Value.Address.CompareTo(suggestedHead.Value.Address); + + if (cmp == 0) + { + if (generatedHead.Value.BalanceChange != suggestedHead.Value.BalanceChange || + generatedHead.Value.NonceChange != suggestedHead.Value.NonceChange || + generatedHead.Value.CodeChange != suggestedHead.Value.CodeChange || + !Enumerable.SequenceEqual(generatedHead.Value.SlotChanges, suggestedHead.Value.SlotChanges)) + { + throw new InvalidBlockLevelAccessListException(block, $"Suggested block-level access list contained incorrect changes for {suggestedHead.Value.Address} at index {index}."); + } + } + else if (cmp > 0) + { + if (HasNoChanges(suggestedHead.Value)) + { + AdvanceSuggested(); + continue; + } + throw new InvalidBlockLevelAccessListException(block, $"Suggested block-level access list contained surplus changes for {suggestedHead.Value.Address} at index {index}."); + } + else + { + if (HasNoChanges(generatedHead.Value)) + { + AdvanceGenerated(); + continue; + } + throw new InvalidBlockLevelAccessListException(block, $"Suggested block-level access list missing account changes for {generatedHead.Value.Address} at index {index}."); + } + + AdvanceGenerated(); + AdvanceSuggested(); + } + + if (gasRemaining < (suggestedReads - generatedReads) * GasCostOf.ColdSLoad) + { + throw new InvalidBlockLevelAccessListException(block, "Suggested block-level access list contained invalid storage reads."); + } + } + + public void SetBlockAccessList(Block block, IReleaseSpec spec) + { + if (!spec.BlockLevelAccessListsEnabled) + { + return; + } + + if (block.IsGenesis) + { + block.Header.BlockAccessListHash = Keccak.OfAnEmptySequenceRlp; + } + else + { + block.GeneratedBlockAccessList = GeneratedBlockAccessList; + block.EncodedBlockAccessList = Rlp.Encode(GeneratedBlockAccessList).Bytes; + block.Header.BlockAccessListHash = new(ValueKeccak.Compute(block.EncodedBlockAccessList).Bytes); + } + } + + // for testing + internal IWorldState Inner => _innerWorldState; + + private static bool HasNoChanges(in ChangeAtIndex c) + => c.BalanceChange is null && + c.NonceChange is null && + c.CodeChange is null && + !c.SlotChanges.GetEnumerator().MoveNext(); +} diff --git a/src/Nethermind/Nethermind.State/StateProvider.cs b/src/Nethermind/Nethermind.State/StateProvider.cs index 6191c265866..5bd74371f41 100644 --- a/src/Nethermind/Nethermind.State/StateProvider.cs +++ b/src/Nethermind/Nethermind.State/StateProvider.cs @@ -26,7 +26,7 @@ namespace Nethermind.State { - internal class StateProvider(ILogManager logManager) + internal class StateProvider(ILogManager logManager) : IJournal { private static readonly UInt256 _zero = UInt256.Zero; @@ -168,7 +168,7 @@ static Account ThrowIfNull(Address address) => throw new InvalidOperationException($"Account {address} is null when updating code hash"); } - private void SetNewBalance(Address address, in UInt256 balanceChange, IReleaseSpec releaseSpec, bool isSubtracting) + private void SetNewBalance(Address address, in UInt256 balanceChange, IReleaseSpec releaseSpec, bool isSubtracting, out UInt256 oldBalance) { _needsStateRootUpdate = true; @@ -204,6 +204,7 @@ static void ThrowNonExistingAccount() } } + oldBalance = 0; return; } @@ -214,6 +215,7 @@ static void ThrowNonExistingAccount() ThrowInsufficientBalanceException(address); } + oldBalance = account.Balance; UInt256 newBalance = isSubtracting ? account.Balance - balanceChange : account.Balance + balanceChange; Account changedAccount = account.WithChangedBalance(newBalance); @@ -234,20 +236,25 @@ static void ThrowInsufficientBalanceException(Address address) } public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec releaseSpec) - { - SetNewBalance(address, balanceChange, releaseSpec, true); - } + => SubtractFromBalance(address, balanceChange, releaseSpec, out _); + public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec releaseSpec, out UInt256 oldBalance) + => SetNewBalance(address, balanceChange, releaseSpec, true, out oldBalance); public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec releaseSpec) - { - SetNewBalance(address, balanceChange, releaseSpec, false); - } + => AddToBalance(address, balanceChange, releaseSpec, out _); + + public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec releaseSpec, out UInt256 oldBalance) + => SetNewBalance(address, balanceChange, releaseSpec, false, out oldBalance); public void IncrementNonce(Address address, UInt256 delta) + => IncrementNonce(address, delta, out _); + + public void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) { _needsStateRootUpdate = true; Account account = GetThroughCache(address) ?? ThrowNullAccount(address); - Account changedAccount = account.WithChangedNonce(account.Nonce + delta); + oldNonce = account.Nonce; + Account changedAccount = account.WithChangedNonce(oldNonce + delta); if (_logger.IsTrace) Trace(address, account, changedAccount); PushUpdate(address, changedAccount); @@ -453,15 +460,16 @@ public void CreateAccountIfNotExists(Address address, in UInt256 balance, in UIn } } - public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balance, IReleaseSpec spec) + public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balance, IReleaseSpec spec, out UInt256 oldBalance) { if (AccountExists(address)) { - AddToBalance(address, balance, spec); + AddToBalance(address, balance, spec, out oldBalance); return false; } else { + oldBalance = 0; CreateAccount(address, balance); return true; } diff --git a/src/Nethermind/Nethermind.State/WorldState.cs b/src/Nethermind/Nethermind.State/WorldState.cs index c476411496e..fdbebbdbbf6 100644 --- a/src/Nethermind/Nethermind.State/WorldState.cs +++ b/src/Nethermind/Nethermind.State/WorldState.cs @@ -198,25 +198,36 @@ public bool InsertCode(Address address, in ValueHash256 codeHash, ReadOnlyMemory DebugGuardInScope(); return _stateProvider.InsertCode(address, codeHash, code, spec, isGenesis); } + public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) + { + DebugGuardInScope(); + _stateProvider.AddToBalance(address, balanceChange, spec, out oldBalance); + } public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => AddToBalance(address, balanceChange, spec, out _); + public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { DebugGuardInScope(); - _stateProvider.AddToBalance(address, balanceChange, spec); + return _stateProvider.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out oldBalance); } public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) + => AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out _); + public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { DebugGuardInScope(); - return _stateProvider.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec); + _stateProvider.SubtractFromBalance(address, balanceChange, spec, out oldBalance); } public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) { DebugGuardInScope(); - _stateProvider.SubtractFromBalance(address, balanceChange, spec); + _stateProvider.SubtractFromBalance(address, balanceChange, spec, out _); } public void IncrementNonce(Address address, UInt256 delta) + => IncrementNonce(address, delta, out _); + public void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) { DebugGuardInScope(); - _stateProvider.IncrementNonce(address, delta); + _stateProvider.IncrementNonce(address, delta, out oldNonce); } public void DecrementNonce(Address address, UInt256 delta) { @@ -274,7 +285,7 @@ public ref readonly UInt256 GetBalance(Address address) public ValueHash256 GetStorageRoot(Address address) { DebugGuardInScope(); - if (address == null) throw new ArgumentNullException(nameof(address)); + ArgumentNullException.ThrowIfNull(address); return _persistentStorageProvider.GetStorageRoot(address); } @@ -345,9 +356,9 @@ public Snapshot TakeSnapshot(bool newTransactionStart = false) DebugGuardInScope(); int persistentSnapshot = _persistentStorageProvider.TakeSnapshot(newTransactionStart); int transientSnapshot = _transientStorageProvider.TakeSnapshot(newTransactionStart); - Snapshot.Storage storageSnapshot = new Snapshot.Storage(persistentSnapshot, transientSnapshot); + Snapshot.Storage storageSnapshot = new(persistentSnapshot, transientSnapshot); int stateSnapshot = _stateProvider.TakeSnapshot(); - return new Snapshot(storageSnapshot, stateSnapshot); + return new Snapshot(storageSnapshot, stateSnapshot, -1); } public void Restore(Snapshot snapshot) @@ -361,7 +372,7 @@ public void Restore(Snapshot snapshot) internal void Restore(int state, int persistentStorage, int transientStorage) { DebugGuardInScope(); - Restore(new Snapshot(new Snapshot.Storage(persistentStorage, transientStorage), state)); + Restore(new Snapshot(new Snapshot.Storage(persistentStorage, transientStorage), state, -1)); } public void SetNonce(Address address, in UInt256 nonce) diff --git a/src/Nethermind/Nethermind.State/WorldStateMetricsDecorator.cs b/src/Nethermind/Nethermind.State/WorldStateMetricsDecorator.cs index 5d1ff549165..001d078eeeb 100644 --- a/src/Nethermind/Nethermind.State/WorldStateMetricsDecorator.cs +++ b/src/Nethermind/Nethermind.State/WorldStateMetricsDecorator.cs @@ -1,143 +1,42 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System; using System.Diagnostics; -using Nethermind.Core; -using Nethermind.Core.Collections; -using Nethermind.Core.Crypto; -using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; using Nethermind.Evm.State; using Nethermind.Evm.Tracing.State; -using Nethermind.Int256; namespace Nethermind.State; -public class WorldStateMetricsDecorator(IWorldState innerState) : IWorldState +public class WorldStateMetricsDecorator(IWorldState innerWorldState) : WrappedWorldState(innerWorldState) { - public void Restore(Snapshot snapshot) => innerState.Restore(snapshot); - - public bool TryGetAccount(Address address, out AccountStruct account) => innerState.TryGetAccount(address, out account); - - public byte[] GetOriginal(in StorageCell storageCell) => innerState.GetOriginal(in storageCell); - - public ReadOnlySpan Get(in StorageCell storageCell) => innerState.Get(in storageCell); - - public void Set(in StorageCell storageCell, byte[] newValue) => innerState.Set(in storageCell, newValue); - - public ReadOnlySpan GetTransientState(in StorageCell storageCell) => innerState.GetTransientState(in storageCell); - - public void SetTransientState(in StorageCell storageCell, byte[] newValue) => innerState.SetTransientState(in storageCell, newValue); + public double StateMerkleizationTime { get; private set; } - public void Reset(bool resetBlockChanges = true) + public override void Reset(bool resetBlockChanges = true) { StateMerkleizationTime = 0d; - innerState.Reset(resetBlockChanges); + _innerWorldState.Reset(resetBlockChanges); } - public Snapshot TakeSnapshot(bool newTransactionStart = false) => innerState.TakeSnapshot(newTransactionStart); - - public void WarmUp(AccessList? accessList) => innerState.WarmUp(accessList); - - public void WarmUp(Address address) => innerState.WarmUp(address); - - public void ClearStorage(Address address) => innerState.ClearStorage(address); - - public void RecalculateStateRoot() + public override void RecalculateStateRoot() { long start = Stopwatch.GetTimestamp(); - innerState.RecalculateStateRoot(); + _innerWorldState.RecalculateStateRoot(); StateMerkleizationTime += Stopwatch.GetElapsedTime(start).TotalMilliseconds; } - public Hash256 StateRoot => innerState.StateRoot; - - public double StateMerkleizationTime { get; private set; } - - public void DeleteAccount(Address address) => innerState.DeleteAccount(address); - - public void CreateAccount(Address address, in UInt256 balance, in UInt256 nonce = default) => - innerState.CreateAccount(address, in balance, in nonce); - - public void CreateAccountIfNotExists(Address address, in UInt256 balance, in UInt256 nonce = default) => - innerState.CreateAccountIfNotExists(address, in balance, in nonce); - - public void CreateEmptyAccountIfDeleted(Address address) => - innerState.CreateEmptyAccountIfDeleted(address); - - public bool InsertCode(Address address, in ValueHash256 codeHash, ReadOnlyMemory code, IReleaseSpec spec, bool isGenesis = false) => - innerState.InsertCode(address, in codeHash, code, spec, isGenesis); - - public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) => - innerState.AddToBalance(address, in balanceChange, spec); - - public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) => - innerState.AddToBalanceAndCreateIfNotExists(address, in balanceChange, spec); - - public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) => - innerState.SubtractFromBalance(address, in balanceChange, spec); - - public void IncrementNonce(Address address, UInt256 delta) => innerState.IncrementNonce(address, delta); - - public void DecrementNonce(Address address, UInt256 delta) => innerState.DecrementNonce(address, delta); - - public void SetNonce(Address address, in UInt256 nonce) => innerState.SetNonce(address, nonce); - - public void Commit(IReleaseSpec releaseSpec, bool isGenesis = false, bool commitRoots = true) - { - long start = Stopwatch.GetTimestamp(); - innerState.Commit(releaseSpec, isGenesis, commitRoots); - if (commitRoots) - StateMerkleizationTime += Stopwatch.GetElapsedTime(start).TotalMilliseconds; - } - - public void Commit(IReleaseSpec releaseSpec, IWorldStateTracer tracer, bool isGenesis = false, bool commitRoots = true) + public override void Commit(IReleaseSpec releaseSpec, IWorldStateTracer tracer, bool isGenesis = false, bool commitRoots = true) { long start = Stopwatch.GetTimestamp(); - innerState.Commit(releaseSpec, tracer, isGenesis, commitRoots); + _innerWorldState.Commit(releaseSpec, tracer, isGenesis, commitRoots); if (commitRoots) StateMerkleizationTime += Stopwatch.GetElapsedTime(start).TotalMilliseconds; } - public void CommitTree(long blockNumber) + public override void CommitTree(long blockNumber) { long start = Stopwatch.GetTimestamp(); - innerState.CommitTree(blockNumber); + _innerWorldState.CommitTree(blockNumber); StateMerkleizationTime += Stopwatch.GetElapsedTime(start).TotalMilliseconds; } - - public ArrayPoolList? GetAccountChanges() => innerState.GetAccountChanges(); - - public void ResetTransient() => innerState.ResetTransient(); - - public byte[]? GetCode(Address address) => innerState.GetCode(address); - - public byte[]? GetCode(in ValueHash256 codeHash) => innerState.GetCode(in codeHash); - - public bool IsContract(Address address) => innerState.IsContract(address); - - public bool AccountExists(Address address) => innerState.AccountExists(address); - - public bool IsDeadAccount(Address address) => innerState.IsDeadAccount(address); - - public bool HasStateForBlock(BlockHeader? stateRoot) => innerState.HasStateForBlock(stateRoot); - - public IDisposable BeginScope(BlockHeader? baseBlock) => innerState.BeginScope(baseBlock); - public bool IsInScope => innerState.IsInScope; - public IWorldStateScopeProvider ScopeProvider => innerState.ScopeProvider; - - public ref readonly UInt256 GetBalance(Address account) => ref innerState.GetBalance(account); - - UInt256 IAccountStateProvider.GetBalance(Address address) => innerState.GetBalance(address); - - public ref readonly ValueHash256 GetCodeHash(Address address) => ref innerState.GetCodeHash(address); - - ValueHash256 IAccountStateProvider.GetCodeHash(Address address) => innerState.GetCodeHash(address); - - UInt256 IAccountStateProvider.GetNonce(Address address) => innerState.GetNonce(address); - - bool IAccountStateProvider.IsStorageEmpty(Address address) => innerState.IsStorageEmpty(address); - - bool IAccountStateProvider.HasCode(Address address) => innerState.HasCode(address); } diff --git a/src/Nethermind/Nethermind.Taiko.Test/TxPoolContentListsTests.cs b/src/Nethermind/Nethermind.Taiko.Test/TxPoolContentListsTests.cs index 3ace773efba..3467fc31bc5 100644 --- a/src/Nethermind/Nethermind.Taiko.Test/TxPoolContentListsTests.cs +++ b/src/Nethermind/Nethermind.Taiko.Test/TxPoolContentListsTests.cs @@ -190,6 +190,7 @@ private static TaikoEngineRpcModule CreateRpcModule( Substitute.For>(), Substitute.For>(), Substitute.For>(), + Substitute.For>(), Substitute.For>(), Substitute.For(), Substitute.For, IEnumerable>>(), @@ -198,6 +199,8 @@ private static TaikoEngineRpcModule CreateRpcModule( Substitute.For, IEnumerable>>(), Substitute.For>>(), Substitute.For?>>(), + Substitute.For, IEnumerable>>(), + Substitute.For(), Substitute.For(), Substitute.For(), null!, diff --git a/src/Nethermind/Nethermind.Taiko/Rpc/TaikoEngineRpcModule.cs b/src/Nethermind/Nethermind.Taiko/Rpc/TaikoEngineRpcModule.cs index 16394316a6a..85d4207c8d0 100644 --- a/src/Nethermind/Nethermind.Taiko/Rpc/TaikoEngineRpcModule.cs +++ b/src/Nethermind/Nethermind.Taiko/Rpc/TaikoEngineRpcModule.cs @@ -40,6 +40,7 @@ public class TaikoEngineRpcModule(IAsyncHandler getPa IAsyncHandler getPayloadHandlerV3, IAsyncHandler getPayloadHandlerV4, IAsyncHandler getPayloadHandlerV5, + IAsyncHandler getPayloadHandlerV6, IAsyncHandler newPayloadV1Handler, IForkchoiceUpdatedHandler forkchoiceUpdatedV1Handler, IHandler, IEnumerable> executionGetPayloadBodiesByHashV1Handler, @@ -48,6 +49,8 @@ public class TaikoEngineRpcModule(IAsyncHandler getPa IHandler, IEnumerable> capabilitiesHandler, IAsyncHandler> getBlobsHandler, IAsyncHandler?> getBlobsHandlerV2, + IHandler, IEnumerable> getPayloadBodiesByHashV2Handler, + IGetPayloadBodiesByRangeV2Handler getPayloadBodiesByRangeV2Handler, IEngineRequestsTracker engineRequestsTracker, ISpecProvider specProvider, GCKeeper gcKeeper, @@ -63,6 +66,7 @@ public class TaikoEngineRpcModule(IAsyncHandler getPa getPayloadHandlerV3, getPayloadHandlerV4, getPayloadHandlerV5, + getPayloadHandlerV6, newPayloadV1Handler, forkchoiceUpdatedV1Handler, executionGetPayloadBodiesByHashV1Handler, @@ -71,6 +75,8 @@ public class TaikoEngineRpcModule(IAsyncHandler getPa capabilitiesHandler, getBlobsHandler, getBlobsHandlerV2, + getPayloadBodiesByHashV2Handler, + getPayloadBodiesByRangeV2Handler, engineRequestsTracker, specProvider, gcKeeper, diff --git a/src/Nethermind/Nethermind.Taiko/Tdx/BlockHeaderForRpc.cs b/src/Nethermind/Nethermind.Taiko/Tdx/BlockHeaderForRpc.cs index ac5942e1fd4..b8b0e3eee55 100644 --- a/src/Nethermind/Nethermind.Taiko/Tdx/BlockHeaderForRpc.cs +++ b/src/Nethermind/Nethermind.Taiko/Tdx/BlockHeaderForRpc.cs @@ -40,6 +40,7 @@ public BlockHeaderForRpc(BlockHeader header) ExcessBlobGas = header.ExcessBlobGas; ParentBeaconBlockRoot = header.ParentBeaconBlockRoot; RequestsHash = header.RequestsHash; + BlockAccessListHash = header.BlockAccessListHash; } [JsonIgnore(Condition = JsonIgnoreCondition.Never)] @@ -82,4 +83,7 @@ public BlockHeaderForRpc(BlockHeader header) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Hash256? RequestsHash { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Hash256? BlockAccessListHash { get; set; } } diff --git a/src/Nethermind/Nethermind.Xdc.Test/SpecialTransactionsTests.cs b/src/Nethermind/Nethermind.Xdc.Test/SpecialTransactionsTests.cs index 6060d6bdf6c..2a9d62018ea 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/SpecialTransactionsTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/SpecialTransactionsTests.cs @@ -13,6 +13,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Evm; +using Nethermind.Evm.State; using Nethermind.Evm.Tracing; using Nethermind.Evm.Tracing.State; using Nethermind.Evm.TransactionProcessing; diff --git a/src/Nethermind/Nethermind.Xdc/Errors/IncomingMessageBlockNotFoundException.cs b/src/Nethermind/Nethermind.Xdc/Errors/IncomingMessageBlockNotFoundException.cs index e9d4ff80325..f245e51fca4 100644 --- a/src/Nethermind/Nethermind.Xdc/Errors/IncomingMessageBlockNotFoundException.cs +++ b/src/Nethermind/Nethermind.Xdc/Errors/IncomingMessageBlockNotFoundException.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Blockchain; +using Nethermind.Core; using Nethermind.Core.Crypto; using System; diff --git a/src/Nethermind/Nethermind.Xdc/XdcBlockTree.cs b/src/Nethermind/Nethermind.Xdc/XdcBlockTree.cs index 1037e1b6de5..b883fdf38d1 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcBlockTree.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcBlockTree.cs @@ -28,12 +28,13 @@ public XdcBlockTree( [KeyFilter("blockInfos")] IDb? blockInfoDb, [KeyFilter("metadata")] IDb? metadataDb, IBadBlockStore? badBlockStore, + IBlockAccessListStore? balStore, IChainLevelInfoRepository? chainLevelInfoRepository, ISpecProvider? specProvider, IBloomStorage? bloomStorage, ISyncConfig? syncConfig, ILogManager? logManager, - long genesisBlockNumber = 0) : base(blockStore, headerDb, blockInfoDb, metadataDb, badBlockStore, chainLevelInfoRepository, specProvider, bloomStorage, syncConfig, logManager, genesisBlockNumber) + long genesisBlockNumber = 0) : base(blockStore, headerDb, blockInfoDb, metadataDb, badBlockStore, balStore, chainLevelInfoRepository, specProvider, bloomStorage, syncConfig, logManager, genesisBlockNumber) { _xdcConsensus = xdcConsensus; }