diff --git a/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs index f27e4f2065a..7eedc5d246f 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs @@ -33,6 +33,24 @@ namespace Nethermind.Blockchain.Test; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class BlockchainProcessorTests { + [Test] + public void LogDiagnosticTrace_does_not_throw_for_null_hash_variant() + { + ILogger logger = LimboLogs.Instance.GetClassLogger(); + + Assert.DoesNotThrow(() => + BlockTraceDumper.LogDiagnosticTrace(NullBlockTracer.Instance, (Hash256)null!, logger)); + } + + [Test] + public void LogDiagnosticTrace_does_not_throw_for_default_either_variant() + { + ILogger logger = LimboLogs.Instance.GetClassLogger(); + + Assert.DoesNotThrow(() => + BlockTraceDumper.LogDiagnosticTrace(NullBlockTracer.Instance, default(Either>), logger)); + } + private class ProcessingTestContext { private readonly ILogManager _logManager = LimboLogs.Instance; diff --git a/src/Nethermind/Nethermind.Blockchain/LogTraceDumper.cs b/src/Nethermind/Nethermind.Blockchain/LogTraceDumper.cs index 61b3d14cd41..515df73c9ef 100644 --- a/src/Nethermind/Nethermind.Blockchain/LogTraceDumper.cs +++ b/src/Nethermind/Nethermind.Blockchain/LogTraceDumper.cs @@ -102,21 +102,25 @@ private static bool GetConditionAndHashString(Either> bloc blockHash = failedBlockHash.ToString(); return false; } - else + + if (blocksOrHash.Is(out IList blocks)) { - blocksOrHash.To(out IList blocks); condition = "valid on rerun"; if (blocks.Count == 1) { - blockHash = blocks[0].Hash.ToString(); + blockHash = blocks[0].Hash?.ToString() ?? "unknown"; } else { - blockHash = string.Join("|", blocks.Select(b => b.Hash.ToString())); + blockHash = string.Join("|", blocks.Select(static b => b.Hash?.ToString() ?? "unknown")); } return true; } + + condition = "unknown"; + blockHash = "unknown"; + return false; } private static FileStream GetFileStream(string name) => diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs index 3dc4ec0e270..77f0faf9398 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs @@ -9,27 +9,29 @@ public static class ModuleType { public const string Admin = nameof(Admin); public const string Clique = nameof(Clique); - public const string Engine = nameof(Engine); public const string Db = nameof(Db); public const string Debug = nameof(Debug); + public const string Deposit = nameof(Deposit); + public const string Engine = nameof(Engine); public const string Erc20 = nameof(Erc20); public const string Eth = nameof(Eth); public const string LogIndex = nameof(LogIndex); public const string Evm = nameof(Evm); public const string Flashbots = nameof(Flashbots); + public const string Health = nameof(Health); public const string Net = nameof(Net); public const string Nft = nameof(Nft); public const string Parity = nameof(Parity); public const string Personal = nameof(Personal); public const string Proof = nameof(Proof); + public const string Rbuilder = nameof(Rbuilder); + public const string Rpc = nameof(Rpc); public const string Subscribe = nameof(Subscribe); + public const string Testing = nameof(Testing); public const string Trace = nameof(Trace); public const string TxPool = nameof(TxPool); public const string Web3 = nameof(Web3); - public const string Deposit = nameof(Deposit); - public const string Health = nameof(Health); - public const string Rpc = nameof(Rpc); - public const string Rbuilder = nameof(Rbuilder); + public const string Vault = nameof(Vault); public static IEnumerable DefaultModules { get; } = new List() { diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V5.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V5.cs index 218014837ee..fb8e3f75c24 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V5.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V5.cs @@ -9,8 +9,10 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Autofac; using CkzgLib; using FluentAssertions; +using Nethermind.Consensus.Producers; using Nethermind.Core; using Nethermind.Core.Extensions; using Nethermind.Core.Test.Builders; @@ -42,6 +44,45 @@ public async Task GetPayloadV5_should_return_all_the_blobs([Values(0, 1, 2, 3, 4 Assert.That(IBlobProofsManager.For(ProofVersion.V1).ValidateProofs(wrapper), Is.True); } + [Test] + public async Task Testing_buildBlockV1_empty_block_with_empty_withdrawals_has_valid_hash() + { + MergeTestBlockchain chain = await CreateBlockchain(releaseSpec: Osaka.Instance); + ITestingRpcModule testingRpcModule = chain.Container.Resolve(); + + Block head = chain.BlockTree.Head!; + PayloadAttributes payloadAttributes = new() + { + Timestamp = head.Timestamp + 12, + PrevRandao = TestItem.KeccakA, + SuggestedFeeRecipient = Address.Zero, + Withdrawals = [], + ParentBeaconBlockRoot = TestItem.KeccakB + }; + + ResultWrapper buildResult = await testingRpcModule.testing_buildBlockV1( + head.Hash!, + payloadAttributes, + [], + []); + + buildResult.Result.Should().Be(Result.Success); + buildResult.Data.Should().NotBeNull(); + + ExecutionPayloadV3 executionPayload = buildResult.Data!.ExecutionPayload; + executionPayload.ExecutionRequests = buildResult.Data.ExecutionRequests; + executionPayload.TryGetBlock().Block!.CalculateHash().Should().Be(executionPayload.BlockHash); + + ResultWrapper newPayloadResult = await chain.EngineRpcModule.engine_newPayloadV4( + executionPayload, + [], + payloadAttributes.ParentBeaconBlockRoot, + buildResult.Data.ExecutionRequests); + + newPayloadResult.Result.Should().Be(Result.Success); + newPayloadResult.Data.Status.Should().Be(PayloadStatus.Valid); + } + [Test] public async Task GetBlobsV2_should_throw_if_more_than_128_requested_blobs([Values(128, 129)] int requestSize) { diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/MergePluginTests.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/MergePluginTests.cs index 3739ab6d7c8..b39db169b00 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/MergePluginTests.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/MergePluginTests.cs @@ -2,28 +2,41 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Threading; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; using Autofac; using FluentAssertions; using Nethermind.Api; +using Nethermind.Blockchain.Find; using Nethermind.Blockchain.Synchronization; using Nethermind.Config; +using Nethermind.Consensus; using Nethermind.Consensus.Clique; using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Producers; using Nethermind.Core; +using Nethermind.Core.Crypto; using Nethermind.Core.Exceptions; +using Nethermind.Core.Specs; +using Nethermind.Evm; +using Nethermind.Evm.Tracing; using Nethermind.HealthChecks; +using Nethermind.Int256; using Nethermind.JsonRpc; using Nethermind.JsonRpc.Modules; +using Nethermind.JsonRpc.Test; using Nethermind.Logging; using Nethermind.Merge.Plugin.BlockProduction; +using Nethermind.Merge.Plugin.Data; using Nethermind.Runner.Ethereum.Modules; using Nethermind.Runner.Test.Ethereum; using Nethermind.Serialization.Json; +using Nethermind.Specs.Forks; using Nethermind.Specs.ChainSpecStyle; using Nethermind.Specs.Test.ChainSpecStyle; +using Nethermind.State.Proofs; using NUnit.Framework; using NSubstitute; @@ -139,6 +152,189 @@ public async Task Initializes_correctly() Assert.That(api.BlockProducer, Is.InstanceOf()); } + [Test] + public async Task Init_registers_gas_limit_calculator_for_testing_rpc_module() + { + using IContainer container = BuildContainer(); + INethermindApi api = container.Resolve(); + await _consensusPlugin!.Init(api); + await _plugin.Init(api); + + Assert.DoesNotThrow(() => container.Resolve()); + } + + [Test] + public async Task Testing_buildBlockV1_sets_excess_blob_gas_for_eip4844() + { + Hash256 parentHash = Keccak.Compute("parent"); + BlockHeader parentHeader = new( + Keccak.Compute("grandparent"), + Keccak.OfAnEmptySequenceRlp, + Address.Zero, + UInt256.Zero, + 1, + 30_000_000, + 1, + []) + { + Hash = parentHash, + TotalDifficulty = UInt256.Zero, + BaseFeePerGas = UInt256.One, + GasUsed = 0, + StateRoot = Keccak.EmptyTreeHash, + ReceiptsRoot = Keccak.EmptyTreeHash, + Bloom = Bloom.Empty, + BlobGasUsed = 0, + ExcessBlobGas = 0, + }; + + Block parentBlock = new(parentHeader, Array.Empty(), Array.Empty(), Array.Empty()); + IBlockFinder blockFinder = Substitute.For(); + blockFinder.FindBlock(parentHash).Returns(parentBlock); + + Hash256? suggestedWithdrawalsRoot = null; + IBlockchainProcessor blockchainProcessor = Substitute.For(); + blockchainProcessor + .Process(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(static callInfo => + { + Block block = callInfo.Arg(); + block.Header.StateRoot ??= Keccak.EmptyTreeHash; + block.Header.ReceiptsRoot ??= Keccak.EmptyTreeHash; + block.Header.Bloom ??= Bloom.Empty; + block.Header.GasUsed = 0; + block.Header.Hash ??= Keccak.Compute("produced"); + return block; + }); + blockchainProcessor + .When(x => x.Process(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())) + .Do(callInfo => + { + Block block = callInfo.Arg(); + suggestedWithdrawalsRoot = block.Header.WithdrawalsRoot; + }); + + IMainProcessingContext mainProcessingContext = Substitute.For(); + mainProcessingContext.BlockchainProcessor.Returns(blockchainProcessor); + + ISpecProvider specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(Osaka.Instance); + + IGasLimitCalculator gasLimitCalculator = Substitute.For(); + gasLimitCalculator.GetGasLimit(Arg.Any()).Returns(parentHeader.GasLimit); + + TestingRpcModule module = new( + mainProcessingContext, + gasLimitCalculator, + specProvider, + blockFinder, + LimboLogs.Instance); + + PayloadAttributes payloadAttributes = new() + { + Timestamp = parentHeader.Timestamp + 12, + PrevRandao = Keccak.Compute("randao"), + SuggestedFeeRecipient = Address.Zero, + Withdrawals = + [ + new Withdrawal + { + Index = 0, + ValidatorIndex = 0, + Address = Address.Zero, + AmountInGwei = 1 + } + ], + ParentBeaconBlockRoot = Keccak.Compute("parentBeaconBlockRoot") + }; + + ResultWrapper result = await module.testing_buildBlockV1(parentHash, payloadAttributes, Array.Empty(), Array.Empty()); + + result.Result.ResultType.Should().Be(ResultType.Success); + result.Data.Should().NotBeNull(); + result.Data!.ExecutionPayload.BlobGasUsed.Should().Be(0); + result.Data!.ExecutionPayload.ExcessBlobGas.Should().Be(BlobGasCalculator.CalculateExcessBlobGas(parentHeader, Osaka.Instance)); + suggestedWithdrawalsRoot.Should().Be(new WithdrawalTrie(payloadAttributes.Withdrawals!).RootHash); + } + + [Test] + public async Task Testing_buildBlockV1_json_rpc_accepts_omitted_extraData() + { + Hash256 parentHash = Keccak.Compute("parent"); + BlockHeader parentHeader = new( + Keccak.Compute("grandparent"), + Keccak.OfAnEmptySequenceRlp, + Address.Zero, + UInt256.Zero, + 1, + 30_000_000, + 1, + []) + { + Hash = parentHash, + TotalDifficulty = UInt256.Zero, + BaseFeePerGas = UInt256.One, + GasUsed = 0, + StateRoot = Keccak.EmptyTreeHash, + ReceiptsRoot = Keccak.EmptyTreeHash, + Bloom = Bloom.Empty, + BlobGasUsed = 0, + ExcessBlobGas = 0, + }; + + Block parentBlock = new(parentHeader, Array.Empty(), Array.Empty(), Array.Empty()); + IBlockFinder blockFinder = Substitute.For(); + blockFinder.FindBlock(parentHash).Returns(parentBlock); + + IBlockchainProcessor blockchainProcessor = Substitute.For(); + blockchainProcessor + .Process(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(static callInfo => + { + Block block = callInfo.Arg(); + block.Header.StateRoot ??= Keccak.EmptyTreeHash; + block.Header.ReceiptsRoot ??= Keccak.EmptyTreeHash; + block.Header.Bloom ??= Bloom.Empty; + block.Header.GasUsed = 0; + block.Header.Hash ??= Keccak.Compute("produced"); + return block; + }); + + IMainProcessingContext mainProcessingContext = Substitute.For(); + mainProcessingContext.BlockchainProcessor.Returns(blockchainProcessor); + + ISpecProvider specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(Osaka.Instance); + + IGasLimitCalculator gasLimitCalculator = Substitute.For(); + gasLimitCalculator.GetGasLimit(Arg.Any()).Returns(parentHeader.GasLimit); + + TestingRpcModule module = new( + mainProcessingContext, + gasLimitCalculator, + specProvider, + blockFinder, + LimboLogs.Instance); + + PayloadAttributes payloadAttributes = new() + { + Timestamp = parentHeader.Timestamp + 12, + PrevRandao = Keccak.Compute("randao"), + SuggestedFeeRecipient = Address.Zero, + Withdrawals = [], + ParentBeaconBlockRoot = Keccak.Compute("parentBeaconBlockRoot") + }; + + JsonRpcResponse response = await RpcTest.TestRequest( + module, + nameof(ITestingRpcModule.testing_buildBlockV1), + parentHash, + payloadAttributes, + Array.Empty()); + + response.Should().BeOfType(); + } + [TestCase(true, true)] [TestCase(false, true)] [TestCase(true, false)] diff --git a/src/Nethermind/Nethermind.Merge.Plugin/ITestingRpcModule.cs b/src/Nethermind/Nethermind.Merge.Plugin/ITestingRpcModule.cs new file mode 100644 index 00000000000..80b97c90d3f --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/ITestingRpcModule.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading.Tasks; +using Nethermind.Consensus.Producers; +using Nethermind.Core.Crypto; +using Nethermind.JsonRpc; +using Nethermind.JsonRpc.Modules; +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Merge.Plugin; + +[RpcModule(ModuleType.Testing)] +public interface ITestingRpcModule : IRpcModule +{ + [JsonRpcMethod( + Description = "Building a block from provided transactions, under provided rules.", + IsSharable = true, + IsImplemented = true)] + + public Task> testing_buildBlockV1(Hash256 parentBlockHash, PayloadAttributes payloadAttributes, IEnumerable txRlps, byte[]? extraData = null, string? targetFork = null); +} diff --git a/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs b/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs index 756e9d571ad..d486c9024f2 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/MergePlugin.cs @@ -21,6 +21,7 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Core.Exceptions; +using Nethermind.Core.Specs; using Nethermind.Db; using Nethermind.Facade.Proxy; using Nethermind.HealthChecks; @@ -343,6 +344,11 @@ protected override void Load(ContainerBuilder builder) ctx.Resolve()); }) .AddSingleton() + .AddSingleton((specProvider, blocksConfig) => + new TargetAdjustedGasLimitCalculator(specProvider, blocksConfig)) + + // Testing rpc + .RegisterSingletonJsonRpcModule() ; } diff --git a/src/Nethermind/Nethermind.Merge.Plugin/TestingRpcModule.cs b/src/Nethermind/Nethermind.Merge.Plugin/TestingRpcModule.cs new file mode 100644 index 00000000000..58f06faca71 --- /dev/null +++ b/src/Nethermind/Nethermind.Merge.Plugin/TestingRpcModule.cs @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2025 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.Find; +using Nethermind.Consensus; +using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Producers; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Evm; +using Nethermind.Evm.Tracing; +using Nethermind.Int256; +using Nethermind.JsonRpc; +using Nethermind.Logging; +using Nethermind.Merge.Plugin.Data; +using Nethermind.Serialization.Rlp; +using Nethermind.State.Proofs; +using ILogger = Nethermind.Logging.ILogger; + +namespace Nethermind.Merge.Plugin; + +public class TestingRpcModule( + IMainProcessingContext mainProcessingContext, + IGasLimitCalculator gasLimitCalculator, + ISpecProvider specProvider, + IBlockFinder blockFinder, + ILogManager logManager) + : ITestingRpcModule +{ + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly IBlockchainProcessor _processor = mainProcessingContext.BlockchainProcessor; + + public Task> testing_buildBlockV1(Hash256 parentBlockHash, PayloadAttributes payloadAttributes, IEnumerable txRlps, byte[]? extraData = null, string? targetFork = null) + { + Block? parentBlock = blockFinder.FindBlock(parentBlockHash); + + if (parentBlock is not null) + { + BlockHeader header = PrepareBlockHeader(parentBlock.Header, payloadAttributes, extraData); + Transaction[] transactions = GetTransactions(txRlps).ToArray(); + header.TxRoot = TxTrie.CalculateRoot(transactions); + Block block = new(header, transactions, Array.Empty(), payloadAttributes.Withdrawals); + + FeesTracer feesTracer = new(); + Block? processedBlock = _processor.Process(block, ProcessingOptions.ProducingBlock, feesTracer); + + if (processedBlock is not null) + { + GetPayloadV5Result getPayloadV5Result = new(processedBlock, feesTracer.Fees, new BlobsBundleV2(processedBlock), processedBlock.ExecutionRequests!, shouldOverrideBuilder: false); + + if (!ValidateFork(getPayloadV5Result, targetFork)) + { + if (_logger.IsWarn) _logger.Warn($"The payload is not supported by the target fork: {targetFork ?? "prague"}"); + return ResultWrapper.Fail("unsupported fork", MergeErrorCodes.UnsupportedFork); + } + + if (_logger.IsDebug) _logger.Debug($"testing_buildBlockV1 produced payload for block {processedBlock.Header.ToString(BlockHeader.Format.Short)}."); + return ResultWrapper.Success(getPayloadV5Result); + } + + return ResultWrapper.Fail("payload processing failed", MergeErrorCodes.UnknownPayload); + } + return ResultWrapper.Fail("unknown parent block", MergeErrorCodes.InvalidPayloadAttributes); + } + + private BlockHeader PrepareBlockHeader(BlockHeader parent, PayloadAttributes payloadAttributes, byte[]? extraData) + { + Address blockAuthor = payloadAttributes.SuggestedFeeRecipient; + BlockHeader header = new( + parent.Hash!, + Keccak.OfAnEmptySequenceRlp, + blockAuthor, + UInt256.Zero, + parent.Number + 1, + payloadAttributes.GetGasLimit() ?? gasLimitCalculator.GetGasLimit(parent), + payloadAttributes.Timestamp, + extraData ?? []) + { + Author = blockAuthor, + MixHash = payloadAttributes.PrevRandao, + ParentBeaconBlockRoot = payloadAttributes.ParentBeaconBlockRoot + }; + + UInt256 difficulty = UInt256.Zero; + header.Difficulty = difficulty; + header.TotalDifficulty = parent.TotalDifficulty + difficulty; + + header.IsPostMerge = true; + IReleaseSpec spec = specProvider.GetSpec(header); + header.BaseFeePerGas = BaseFeeCalculator.Calculate(parent, spec); + + if (spec.IsEip4844Enabled) + { + header.BlobGasUsed = 0; + header.ExcessBlobGas = BlobGasCalculator.CalculateExcessBlobGas(parent, spec); + } + + if (spec.WithdrawalsEnabled) + { + header.WithdrawalsRoot = payloadAttributes.Withdrawals is null || payloadAttributes.Withdrawals.Length == 0 + ? Keccak.EmptyTreeHash + : new WithdrawalTrie(payloadAttributes.Withdrawals).RootHash; + } + + return header; + } + + private IEnumerable GetTransactions(IEnumerable txRlps) + { + foreach (var txRlp in txRlps) + { + yield return TxDecoder.Instance.Decode(new RlpStream(txRlp), RlpBehaviors.SkipTypedWrapping); + } + } + + private bool ValidateFork(GetPayloadV5Result payload, string? targetFork) + { + IReleaseSpec spec = specProvider.GetSpec(payload.ExecutionPayload.BlockNumber, payload.ExecutionPayload.Timestamp); + + return targetFork?.ToLowerInvariant() switch + { + "amsterdam" => spec.IsEip7702Enabled, + "prague" => spec.IsEip7594Enabled, + null => spec.IsEip7594Enabled, + _ => false + }; + } +} diff --git a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs index 5a195a39e47..9dfefdde5fc 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs @@ -1069,6 +1069,12 @@ private void VerifyNewCommitSet(long blockNumber) if (_lastCommitSet.BlockNumber != blockNumber - 1 && blockNumber != 0 && _lastCommitSet.BlockNumber != 0) { + if (_lastCommitSet.BlockNumber == blockNumber) + { + if (_logger.IsDebug) _logger.Debug($"Duplicate block-number commit. Last block commit: {_lastCommitSet.BlockNumber}. New block commit: {blockNumber}."); + return; + } + if (_logger.IsInfo) _logger.Info($"Non consecutive block commit. This is likely a reorg. Last block commit: {_lastCommitSet.BlockNumber}. New block commit: {blockNumber}."); } }