diff --git a/.github/workflows/nethermind-tests.yml b/.github/workflows/nethermind-tests.yml index d3f847f37a4..0800c19094a 100644 --- a/.github/workflows/nethermind-tests.yml +++ b/.github/workflows/nethermind-tests.yml @@ -46,6 +46,7 @@ jobs: - Nethermind.EthStats.Test - Nethermind.Evm.Test - Nethermind.Facade.Test + - Nethermind.Flashbots.Test - Nethermind.HealthChecks.Test - Nethermind.Hive.Test - Nethermind.JsonRpc.Test diff --git a/src/Nethermind/Nethermind.Api/Extensions/PluginConfig.cs b/src/Nethermind/Nethermind.Api/Extensions/PluginConfig.cs index 8dfce8de1ef..84d2964cead 100644 --- a/src/Nethermind/Nethermind.Api/Extensions/PluginConfig.cs +++ b/src/Nethermind/Nethermind.Api/Extensions/PluginConfig.cs @@ -5,5 +5,5 @@ namespace Nethermind.Api.Extensions; public class PluginConfig : IPluginConfig { - public string[] PluginOrder { get; set; } = { "Clique", "Aura", "Ethash", "Optimism", "Shutter", "Taiko", "AuRaMerge", "Merge", "MEV", "HealthChecks", "Hive" }; + public string[] PluginOrder { get; set; } = { "Clique", "Aura", "Ethash", "Optimism", "Shutter", "Taiko", "AuRaMerge", "Merge", "Flashbots", "MEV", "HealthChecks", "Hive" }; } diff --git a/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.Setup.cs b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.Setup.cs new file mode 100644 index 00000000000..9669157bd7c --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.Setup.cs @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using Nethermind.Blockchain.BeaconBlockRoot; +using Nethermind.Blockchain.Blocks; +using Nethermind.Blockchain.Synchronization; +using Nethermind.Consensus; +using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Rewards; +using Nethermind.Consensus.Validators; +using Nethermind.Consensus.Withdrawals; +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Blockchain; +using Nethermind.Crypto; +using Nethermind.Db; +using Nethermind.Flashbots; +using Nethermind.Flashbots.Handlers; +using Nethermind.Flashbots.Modules.Flashbots; +using Nethermind.Int256; +using Nethermind.Logging; +using Nethermind.Merge.Plugin; +using Nethermind.Specs; +using Nethermind.Specs.ChainSpecStyle; +using Nethermind.Specs.Forks; +using NUnit.Framework; + +namespace Nethermind.Flasbots.Test; + +public partial class FlashbotsModuleTests +{ + TestKeyAndAddress? TestKeysAndAddress; + + [SetUp] + public void SetUp() + { + TestKeysAndAddress = new TestKeyAndAddress(); + } + + internal class TestKeyAndAddress + { + public PrivateKey PrivateKey = new PrivateKey("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + public Address TestAddr; + + public PrivateKey TestValidatorKey = new PrivateKey("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0"); + public Address TestValidatorAddr; + + public PrivateKey TestBuilderKey = new PrivateKey("0bfbbbc68fefd990e61ba645efb84e0a62e94d5fff02c9b1da8eb45fea32b4e0"); + public Address TestBuilderAddr; + + public UInt256 TestBalance = UInt256.Parse("2000000000000000000"); + public byte[] logCode = Bytes.FromHexString("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00"); + + public UInt256 BaseInitialFee = 1000000000; + public TestKeyAndAddress() + { + TestAddr = PrivateKey.Address; + TestValidatorAddr = TestValidatorKey.Address; + TestBuilderAddr = TestBuilderKey.Address; + } + } + + protected virtual MergeTestBlockChain CreateBaseBlockChain( + IFlashbotsConfig flashbotsConfig, + ILogManager? logManager = null) + { + return new MergeTestBlockChain(flashbotsConfig, logManager); + } + + protected async Task CreateBlockChain( + IReleaseSpec? releaseSpec = null, + IFlashbotsConfig? flashbotsConfig = null, + ILogManager? logManager = null) + => await CreateBaseBlockChain(flashbotsConfig ?? new FlashbotsConfig(), logManager).Build(new TestSingleReleaseSpecProvider(releaseSpec ?? London.Instance)); + + private IFlashbotsRpcModule CreateFlashbotsModule(MergeTestBlockChain chain, ReadOnlyTxProcessingEnv readOnlyTxProcessingEnv) + { + return new FlashbotsRpcModule( + new ValidateSubmissionHandler( + chain.HeaderValidator, + chain.BlockValidator, + readOnlyTxProcessingEnv, + chain.FlashbotsConfig + ) + ); + } + + public class MergeTestBlockChain : TestBlockchain + { + public IFlashbotsConfig FlashbotsConfig; + + public IMergeConfig MergeConfig; + + public IWithdrawalProcessor? WithdrawalProcessor { get; set; } + + public ReadOnlyTxProcessingEnv ReadOnlyTxProcessingEnv { get; set; } + + public MergeTestBlockChain(IFlashbotsConfig flashbotsConfig, ILogManager? logManager = null) + { + FlashbotsConfig = flashbotsConfig; + MergeConfig = new MergeConfig() { TerminalTotalDifficulty = "0" }; + LogManager = logManager ?? LogManager; + } + + public sealed override ILogManager LogManager { get; set; } = LimboLogs.Instance; + + public ReadOnlyTxProcessingEnv CreateReadOnlyTxProcessingEnv() + { + ReadOnlyTxProcessingEnv = new ReadOnlyTxProcessingEnv( + WorldStateManager, + BlockTree, + SpecProvider, + LogManager + ); + return ReadOnlyTxProcessingEnv; + } + + protected override IBlockProcessor CreateBlockProcessor() + { + BlockValidator = CreateBlockValidator(); + WithdrawalProcessor = new WithdrawalProcessor(State, LogManager); + IBlockProcessor prcessor = new BlockProcessor( + SpecProvider, + BlockValidator, + NoBlockRewards.Instance, + new BlockProcessor.BlockValidationTransactionsExecutor(TxProcessor, State), + State, + ReceiptStorage, + TxProcessor, + new BeaconBlockRootHandler(TxProcessor, State), + new BlockhashStore(SpecProvider, State), + LogManager, + WithdrawalProcessor + ); + + return prcessor; + } + + protected IBlockValidator CreateBlockValidator() + { + PoSSwitcher = new PoSSwitcher(MergeConfig, SyncConfig.Default, new MemDb(), BlockTree, SpecProvider, new ChainSpec() { Genesis = Core.Test.Builders.Build.A.Block.WithDifficulty(0).TestObject }, LogManager); + ISealValidator SealValidator = new MergeSealValidator(PoSSwitcher, Always.Valid); + HeaderValidator = new MergeHeaderValidator( + PoSSwitcher, + new HeaderValidator(BlockTree, SealValidator, SpecProvider, LogManager), + BlockTree, + SpecProvider, + SealValidator, + LogManager + ); + + return new BlockValidator( + new TxValidator(SpecProvider.ChainId), + HeaderValidator, + Always.Valid, + SpecProvider, + LogManager + ); + } + + protected override async Task Build(ISpecProvider? specProvider = null, UInt256? initialValues = null, bool addBlockOnStart = true) + { + TestBlockchain chain = await base.Build(specProvider, initialValues); + return chain; + } + + public async Task Build(ISpecProvider? specProvider = null) => + (MergeTestBlockChain)await Build(specProvider, null); + + } +} diff --git a/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs new file mode 100644 index 00000000000..654ebb5d1b7 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Consensus.Processing; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Flashbots.Modules.Flashbots; +using Nethermind.Int256; +using Nethermind.JsonRpc; +using Nethermind.JsonRpc.Test; +using Nethermind.Merge.Plugin.Data; +using Nethermind.Specs.Forks; +using Nethermind.State; +using NUnit.Framework; + +namespace Nethermind.Flasbots.Test; + +public partial class FlashbotsModuleTests +{ + private static readonly DateTime Timestamp = DateTimeOffset.FromUnixTimeSeconds(1000).UtcDateTime; + private ITimestamper Timestamper { get; } = new ManualTimestamper(Timestamp); + + [Test] + public virtual async Task TestValidateBuilderSubmissionV3() + { + using MergeTestBlockChain chain = await CreateBlockChain(releaseSpec: Cancun.Instance); + ReadOnlyTxProcessingEnv readOnlyTxProcessingEnv = chain.CreateReadOnlyTxProcessingEnv(); + IFlashbotsRpcModule rpc = CreateFlashbotsModule(chain, readOnlyTxProcessingEnv); + BlockHeader currentHeader = chain.BlockTree.Head.Header; + IWorldState State = chain.State; + + UInt256 nonce = State.GetNonce(TestKeysAndAddress.TestAddr); + + Transaction tx1 = Build.A.Transaction.WithNonce(nonce).WithTo(new Address("0x16")).WithValue(10).WithGasLimit(21000).WithGasPrice(TestKeysAndAddress.BaseInitialFee).Signed(TestKeysAndAddress.PrivateKey).TestObject; + chain.TxPool.SubmitTx(tx1, TxPool.TxHandlingOptions.None); + + Transaction tx2 = Build.A.Transaction.WithNonce(nonce + 1).WithValue(0).WithGasLimit(1000000).WithGasPrice(2 * TestKeysAndAddress.BaseInitialFee).Signed(TestKeysAndAddress.PrivateKey).TestObject; + chain.TxPool.SubmitTx(tx2, TxPool.TxHandlingOptions.None); + + UInt256 baseFee = BaseFeeCalculator.Calculate(currentHeader, chain.SpecProvider.GetFinalSpec()); + + Transaction tx3 = Build.A.Transaction.WithNonce(nonce + 2).WithValue(10).WithGasLimit(21000).WithValue(baseFee).Signed(TestKeysAndAddress.PrivateKey).TestObject; + chain.TxPool.SubmitTx(tx3, TxPool.TxHandlingOptions.None); + + Withdrawal[] withdrawals = [ + Build.A.Withdrawal.WithIndex(0).WithValidatorIndex(1).WithAmount(100).WithRecipient(TestKeysAndAddress.TestAddr).TestObject, + Build.A.Withdrawal.WithIndex(1).WithValidatorIndex(1).WithAmount(100).WithRecipient(TestKeysAndAddress.TestAddr).TestObject + ]; + + ulong timestamp = Timestamper.UnixTime.Seconds; + Hash256 prevRandao = Keccak.Zero; + + var payloadAttrs = new + { + timestamp = timestamp.ToHexString(true), + prevRandao, + suggestedFeeRecipient = TestKeysAndAddress.TestAddr.ToString(), + withdrawals, + parentBeaconBLockRoot = Keccak.Zero + }; + + string?[] @params = new string?[] + { + chain.JsonSerializer.Serialize(new { + headBlockHash = currentHeader.Hash.ToString(), + safeBlockHash = currentHeader.Hash.ToString(), + finalizedBlockHash = currentHeader.Hash.ToString(), + }), chain.JsonSerializer.Serialize(payloadAttrs) + }; + string expectedPayloadId = "0x774c6aff527bbc68"; + + string response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV3", @params!); + JsonRpcSuccessResponse? successResponse = chain.JsonSerializer.Deserialize(response); + + successResponse.Should().NotBeNull(); + response.Should().Be(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new ForkchoiceUpdatedV1Result + { + PayloadId = expectedPayloadId, + PayloadStatus = new PayloadStatusV1 + { + LatestValidHash = new("0xd7e58364f16b4a329b959b166f9c32323cb135669335db5dadd0344568f8dc9a"), + Status = PayloadStatus.Valid, + ValidationError = null + } + } + })); + + Hash256 expectedBlockHash = new("0xfafb92e8ece12d5fcfa867df9ae6865c5bd8aaf0b277c244552bfe869f61fb26"); + string stateRoot = "0xa272b2f949e4a0e411c9b45542bd5d0ef3c311b5f26c4ed6b7a8d4f605a91154"; + + Block block = new( + new( + currentHeader.Hash, + Keccak.OfAnEmptySequenceRlp, + TestKeysAndAddress.TestAddr, + 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), + }, + Array.Empty(), + Array.Empty(), + withdrawals + ); + + GetPayloadV3Result expectedPayload = new(block, UInt256.Zero, new BlobsBundleV1(block)); + + response = await RpcTest.TestSerializedRequest(rpc, "engine_getPayloadV3", expectedPayloadId); + successResponse = chain.JsonSerializer.Deserialize(response); + + successResponse.Should().NotBeNull(); + response.Should().Be(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = expectedPayload + })); + } +} diff --git a/src/Nethermind/Nethermind.Flashbots.Test/Nethermind.Flasbots.Test.csproj b/src/Nethermind/Nethermind.Flashbots.Test/Nethermind.Flasbots.Test.csproj new file mode 100644 index 00000000000..dd095ad9bff --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots.Test/Nethermind.Flasbots.Test.csproj @@ -0,0 +1,31 @@ + + + + annotations + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/src/Nethermind/Nethermind.Flashbots/Data/BidTrace.cs b/src/Nethermind/Nethermind.Flashbots/Data/BidTrace.cs new file mode 100644 index 00000000000..8fddcb0d205 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/BidTrace.cs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Int256; + +namespace Nethermind.Flashbots.Data; + +public class BidTrace( + ulong slot, + Hash256 blockHash, + PublicKey builderPublicKey, + PublicKey proposerPublicKey, + Address proposerFeeRecipient, + long gasLimit, + long gasUsed, + UInt256 value) +{ + public ulong Slot { get; } = slot; + public required Hash256 ParentHash { get; set; } + public Hash256 BlockHash { get; } = blockHash; + public PublicKey BuilderPublicKey { get; } = builderPublicKey; + public PublicKey ProposerPublicKey { get; } = proposerPublicKey; + public Address ProposerFeeRecipient { get; } = proposerFeeRecipient; + public long GasLimit { get; } = gasLimit; + public long GasUsed { get; } = gasUsed; + public UInt256 Value { get; } = value; +} diff --git a/src/Nethermind/Nethermind.Flashbots/Data/BlockValidationResult.cs b/src/Nethermind/Nethermind.Flashbots/Data/BlockValidationResult.cs new file mode 100644 index 00000000000..1a0eae8d723 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/BlockValidationResult.cs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Text.Json.Serialization; +using Nethermind.JsonRpc; + +namespace Nethermind.Flashbots.Data; + +/// +/// Represents the result of a block validation. +/// +public class FlashbotsResult +{ + + public static ResultWrapper Invalid(string error) + { + return ResultWrapper.Success(new FlashbotsResult + { + Status = FlashbotsStatus.Invalid, + ValidationError = error + }); + } + + public static ResultWrapper Valid() + { + return ResultWrapper.Success(new FlashbotsResult + { + Status = FlashbotsStatus.Valid + }); + } + + public static ResultWrapper Error(string error) + { + return ResultWrapper.Fail(error); + } + + /// + /// The status of the validation of the builder submissions + /// + public string Status { get; set; } = FlashbotsStatus.Invalid; + + /// + /// Message providing additional details on the validation error if the payload is classified as . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string? ValidationError { get; set; } +} diff --git a/src/Nethermind/Nethermind.Flashbots/Data/BlockValidationStatus.cs b/src/Nethermind/Nethermind.Flashbots/Data/BlockValidationStatus.cs new file mode 100644 index 00000000000..bb74faebb00 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/BlockValidationStatus.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Flashbots.Data; + +public static class FlashbotsStatus +{ + /// + /// The submissions are invalid. + /// + public const string Invalid = "Invalid"; + + /// + /// The submissions are valid. + /// + public const string Valid = "Valid"; +} diff --git a/src/Nethermind/Nethermind.Flashbots/Data/BuilderBlockValidationRequest.cs b/src/Nethermind/Nethermind.Flashbots/Data/BuilderBlockValidationRequest.cs new file mode 100644 index 00000000000..66463d74ffc --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/BuilderBlockValidationRequest.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Text.Json.Serialization; +using Nethermind.Core.Crypto; + +namespace Nethermind.Flashbots.Data; + +public class BuilderBlockValidationRequest +{ + /// + /// The block hash of the parent beacon block. + /// + /// + [JsonRequired] + public required Hash256 ParentBeaconBlockRoot { get; set; } + + [JsonRequired] + public long RegisterGasLimit { get; set; } + + [JsonRequired] + public required SubmitBlockRequest BlockRequest { get; set; } +} diff --git a/src/Nethermind/Nethermind.Flashbots/Data/SubmitBlockRequest.cs b/src/Nethermind/Nethermind.Flashbots/Data/SubmitBlockRequest.cs new file mode 100644 index 00000000000..6e7cc6d783c --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/SubmitBlockRequest.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Flashbots.Data; + +public class SubmitBlockRequest +{ + private readonly ExecutionPayloadV3 _executionPayload; + private readonly BlobsBundleV1 _blobsBundle; + + public SubmitBlockRequest(ExecutionPayloadV3 executionPayload, BlobsBundleV1 blobsBundle, BidTrace message) + { + _executionPayload = executionPayload; + _blobsBundle = blobsBundle; + Message = message; + } + public ExecutionPayloadV3 ExecutionPayload => _executionPayload; + public BlobsBundleV1 BlobsBundle => _blobsBundle; + public BidTrace Message { get; } +} diff --git a/src/Nethermind/Nethermind.Flashbots/Flashbots.cs b/src/Nethermind/Nethermind.Flashbots/Flashbots.cs new file mode 100644 index 00000000000..ce8eedc8281 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Flashbots.cs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq; +using System.Threading.Tasks; +using Nethermind.Api; +using Nethermind.Api.Extensions; +using Nethermind.Flashbots.Handlers; +using Nethermind.Flashbots.Modules.Flashbots; +using Nethermind.Consensus.Processing; +using Nethermind.JsonRpc; +using Nethermind.JsonRpc.Modules; + +namespace Nethermind.Flashbots; + +public class Flashbots : INethermindPlugin +{ + private INethermindApi _api = null!; + + private IFlashbotsConfig _flashbotsConfig = null!; + + private IJsonRpcConfig _jsonRpcConfig = null!; + + public virtual string Name => "Flashbots"; + public virtual string Description => "Flashbots"; + public string Author => "Nethermind"; + public Task InitRpcModules() + { + ReadOnlyTxProcessingEnvFactory readOnlyTxProcessingEnvFactory = new ReadOnlyTxProcessingEnvFactory( + _api.WorldStateManager ?? throw new ArgumentNullException(nameof(_api.WorldStateManager)), + _api.BlockTree ?? throw new ArgumentNullException(nameof(_api.BlockTree)), + _api.SpecProvider, + _api.LogManager + ); + + ReadOnlyTxProcessingEnv readOnlyTxProcessingEnv = new ReadOnlyTxProcessingEnv( + _api.WorldStateManager ?? throw new ArgumentNullException(nameof(_api.WorldStateManager)), + _api.BlockTree ?? throw new ArgumentNullException(nameof(_api.BlockTree)), + _api.SpecProvider, + _api.LogManager + ); + + ValidateSubmissionHandler validateSubmissionHandler = new ValidateSubmissionHandler( + _api.HeaderValidator ?? throw new ArgumentNullException(nameof(_api.HeaderValidator)), + _api.BlockValidator ?? throw new ArgumentNullException(nameof(_api.BlockValidator)), + readOnlyTxProcessingEnv, + readOnlyTxProcessingEnvFactory, + _api.LogManager ?? throw new ArgumentNullException(nameof(_api.LogManager)), + _api.SpecProvider ?? throw new ArgumentNullException(nameof(_api.SpecProvider)), + _flashbotsConfig + ); + + ModuleFactoryBase flashbotsRpcModule = new FlashbotsRpcModuleFactory(validateSubmissionHandler); + + ArgumentNullException.ThrowIfNull(_api.RpcModuleProvider); + _api.RpcModuleProvider.RegisterBounded(flashbotsRpcModule, + _jsonRpcConfig.EthModuleConcurrentInstances ?? Environment.ProcessorCount, _jsonRpcConfig.Timeout); + + return Task.CompletedTask; + } + + public Task Init(INethermindApi api) + { + _api = api; + _flashbotsConfig = api.Config(); + _jsonRpcConfig = api.Config(); + if (_flashbotsConfig.Enabled) + { + _jsonRpcConfig.EnabledModules = _jsonRpcConfig.EnabledModules.Append(ModuleType.Flashbots).ToArray(); + } + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; +} diff --git a/src/Nethermind/Nethermind.Flashbots/FlashbotsConfig.cs b/src/Nethermind/Nethermind.Flashbots/FlashbotsConfig.cs new file mode 100644 index 00000000000..363483a383d --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/FlashbotsConfig.cs @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Flashbots; + +public class FlashbotsConfig : IFlashbotsConfig +{ + public bool Enabled { get; set; } = true; + public bool UseBalanceDiffProfit { get; set; } = false; + + public bool ExcludeWithdrawals { get; set; } = false; + + public bool EnablePreWarmer { get; set; } = true; + public bool EnableValidation { get; set; } = false; +} diff --git a/src/Nethermind/Nethermind.Flashbots/Handlers/ValidateBuilderSubmissionHandler.cs b/src/Nethermind/Nethermind.Flashbots/Handlers/ValidateBuilderSubmissionHandler.cs new file mode 100644 index 00000000000..29d6df9958e --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Handlers/ValidateBuilderSubmissionHandler.cs @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: 2024 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; +using Nethermind.Blockchain.BeaconBlockRoot; +using Nethermind.Blockchain.Blocks; +using Nethermind.Blockchain.Receipts; +using Nethermind.Flashbots.Data; +using Nethermind.Consensus; +using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Validators; +using Nethermind.Consensus.Withdrawals; +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Crypto; +using Nethermind.Evm; +using Nethermind.Evm.Tracing; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.Int256; +using Nethermind.JsonRpc; +using Nethermind.Logging; +using Nethermind.Merge.Plugin.Data; +using Nethermind.State; +using Nethermind.Core.Crypto; + +namespace Nethermind.Flashbots.Handlers; + +public class ValidateSubmissionHandler +{ + private ProcessingOptions ValidateSubmissionProcessingOptions = ProcessingOptions.ReadOnlyChain + | ProcessingOptions.IgnoreParentNotOnMainChain + | ProcessingOptions.ForceProcessing + | ProcessingOptions.StoreReceipts; + private readonly ReadOnlyTxProcessingEnv _txProcessingEnv; + + private readonly ReadOnlyTxProcessingEnvFactory _readOnlyTxProcessingEnvFactory; + + private readonly IBlockTree _blockTree; + private readonly IHeaderValidator _headerValidator; + private readonly IBlockValidator _blockValidator; + private readonly ILogger _logger; + + private readonly IFlashbotsConfig _flashbotsConfig; + + private readonly IReceiptStorage _receiptStorage = new InMemoryReceiptStorage(); + + private readonly ILogManager _logManager; + private readonly ISpecProvider _specProvider; + + public ValidateSubmissionHandler( + IHeaderValidator headerValidator, + IBlockValidator blockValidator, + ReadOnlyTxProcessingEnv txProcessingEnv, + ReadOnlyTxProcessingEnvFactory readOnlyTxProcessingEnvFactory, + ILogManager logManager, + ISpecProvider specProvider, + IFlashbotsConfig flashbotsConfig) + { + _headerValidator = headerValidator; + _blockValidator = blockValidator; + _txProcessingEnv = txProcessingEnv; + _readOnlyTxProcessingEnvFactory = readOnlyTxProcessingEnvFactory; + _specProvider = specProvider; + _logManager = logManager; + _blockTree = _txProcessingEnv.BlockTree; + _logger = txProcessingEnv.LogManager!.GetClassLogger(); + _flashbotsConfig = flashbotsConfig; + } + + public Task> ValidateSubmission(BuilderBlockValidationRequest request) + { + ExecutionPayloadV3 payload = request.BlockRequest.ExecutionPayload; + + if (request.ParentBeaconBlockRoot is null) + { + return FlashbotsResult.Invalid("Parent beacon block root must be set in the request"); + } + + payload.ParentBeaconBlockRoot = new Hash256(request.ParentBeaconBlockRoot); + + + BlobsBundleV1 blobsBundle = request.BlockRequest.BlobsBundle; + + string payloadStr = $"BuilderBlock: {payload}"; + + _logger.Info($"blobs bundle blobs {blobsBundle.Blobs.Length} commits {blobsBundle.Commitments.Length} proofs {blobsBundle.Proofs.Length}"); + + if (!payload.TryGetBlock(out Block? block)) + { + if (_logger.IsWarn) _logger.Warn($"Invalid block. Result of {payloadStr}."); + return FlashbotsResult.Invalid($"Block {payload} coud not be parsed as a block"); + } + + if (block is not null && !ValidateBlock(block, request.BlockRequest.Message, request.RegisterGasLimit, out string? error)) + { + if (_logger.IsWarn) _logger.Warn($"Invalid block. Result of {payloadStr}. Error: {error}"); + return FlashbotsResult.Invalid(error ?? "Block validation failed"); + } + + if (block is not null && !ValidateBlobsBundle(block.Transactions, blobsBundle, out string? blobsError)) + { + if (_logger.IsWarn) _logger.Warn($"Invalid blobs bundle. Result of {payloadStr}. Error: {blobsError}"); + return FlashbotsResult.Invalid(blobsError ?? "Blobs bundle validation failed"); + } + + + return FlashbotsResult.Valid(); + } + + private bool ValidateBlock(Block block, BidTrace message, long registerGasLimit, out string? error) + { + error = null; + + if (message.ParentHash != block.Header.ParentHash) + { + error = $"Parent hash mismatch. Expected {message.ParentHash} but got {block.Header.ParentHash}"; + return false; + } + + if (message.BlockHash != block.Header.Hash) + { + error = $"Block hash mismatch. Expected {message.BlockHash} but got {block.Header.Hash}"; + return false; + } + + if (message.GasLimit != block.GasLimit) + { + error = $"Gas limit mismatch. Expected {message.GasLimit} but got {block.GasLimit}"; + return false; + } + + if (message.GasUsed != block.GasUsed) + { + error = $"Gas used mismatch. Expected {message.GasUsed} but got {block.GasUsed}"; + return false; + } + + Address feeRecipient = message.ProposerFeeRecipient; + UInt256 expectedProfit = message.Value; + + if (!ValidatePayload(block, feeRecipient, expectedProfit, registerGasLimit, _flashbotsConfig.UseBalanceDiffProfit, _flashbotsConfig.ExcludeWithdrawals, out error)) + { + return false; + } + + _logger.Info($"Validated block Hash: {block.Header.Hash} Number: {block.Header.Number} ParentHash: {block.Header.ParentHash}"); + + return true; + } + + private bool ValidateBlobsBundle(Transaction[] transactions, BlobsBundleV1 blobsBundle, out string? error) + { + // get sum of length of blobs of each transaction + int totalBlobsLength = transactions.Sum(t => t.BlobVersionedHashes is not null ? t.BlobVersionedHashes.Length : 0); + + if (totalBlobsLength != blobsBundle.Blobs.Length) + { + error = $"Total blobs length mismatch. Expected {totalBlobsLength} but got {blobsBundle.Blobs.Length}"; + return false; + } + + if (totalBlobsLength != blobsBundle.Commitments.Length) + { + error = $"Total commitments length mismatch. Expected {totalBlobsLength} but got {blobsBundle.Commitments.Length}"; + return false; + } + + if (totalBlobsLength != blobsBundle.Proofs.Length) + { + error = $"Total proofs length mismatch. Expected {totalBlobsLength} but got {blobsBundle.Proofs.Length}"; + return false; + } + + if (!KzgPolynomialCommitments.AreProofsValid(blobsBundle.Proofs, blobsBundle.Commitments, blobsBundle.Blobs)) + { + error = "Invalid KZG proofs"; + return false; + } + + error = null; + + _logger.Info($"Validated blobs bundle with {totalBlobsLength} blobs, commitments: {blobsBundle.Commitments.Length}, proofs: {blobsBundle.Proofs.Length}"); + + return true; + } + + private bool ValidatePayload(Block block, Address feeRecipient, UInt256 expectedProfit, long registerGasLimit, bool useBalanceDiffProfit, bool excludeWithdrawals, out string? error) + { + BlockHeader? parentHeader = _blockTree.FindHeader(block.ParentHash!, BlockTreeLookupOptions.DoNotCreateLevelIfMissing); + + if (parentHeader is null) + { + error = $"Parent header {block.ParentHash} not found"; + return false; + } + + if (!ValidateBlockMetadata(block, registerGasLimit, parentHeader, out error)) + { + return false; + } + + using IReadOnlyTxProcessingScope processingScope = _readOnlyTxProcessingEnvFactory.Create().Build(parentHeader.StateRoot!); + IWorldState currentState = processingScope.WorldState; + ITransactionProcessor transactionProcessor = processingScope.TransactionProcessor; + + UInt256 feeRecipientBalanceBefore = currentState.HasStateForRoot(currentState.StateRoot) ? (currentState.AccountExists(feeRecipient) ? currentState.GetBalance(feeRecipient) : UInt256.Zero) : UInt256.Zero; + + IBlockCachePreWarmer preWarmer = new BlockCachePreWarmer(_readOnlyTxProcessingEnvFactory, _specProvider, _logManager); + + BlockProcessor blockProcessor = CreateBlockProcessor(currentState, transactionProcessor, _flashbotsConfig.EnablePreWarmer ? preWarmer : null); + + EthereumEcdsa ecdsa = new EthereumEcdsa(_specProvider.ChainId); + IReleaseSpec spec = _specProvider.GetSpec(parentHeader); + + RecoverSenderAddress(block, ecdsa, spec); + + List suggestedBlocks = [block]; + BlockReceiptsTracer blockReceiptsTracer = new(); + + try + { + if (!_flashbotsConfig.EnableValidation) + { + ValidateSubmissionProcessingOptions |= ProcessingOptions.NoValidation; + } + Block processedBlock = blockProcessor.Process(currentState.StateRoot, suggestedBlocks, ValidateSubmissionProcessingOptions, blockReceiptsTracer)[0]; + } + catch (Exception e) + { + error = $"Block processing failed: {e.Message}"; + return false; + } + + UInt256 feeRecipientBalanceAfter = currentState.GetBalance(feeRecipient); + + UInt256 amtBeforeOrWithdrawn = feeRecipientBalanceBefore; + + if (excludeWithdrawals) + { + foreach (Withdrawal withdrawal in block.Withdrawals ?? []) + { + if (withdrawal.Address == feeRecipient) + { + amtBeforeOrWithdrawn += withdrawal.AmountInGwei; + } + } + } + + if (!_blockValidator.ValidateSuggestedBlock(block, out error)) + { + return false; + } + + if (ValidateProposerPayment(expectedProfit, useBalanceDiffProfit, feeRecipientBalanceAfter, amtBeforeOrWithdrawn)) return true; + + if (!ValidateProcessedBlock(block, feeRecipient, expectedProfit, blockReceiptsTracer.TxReceipts.ToArray(), out error)) + { + return false; + } + + error = null; + return true; + } + + private void RecoverSenderAddress(Block block, EthereumEcdsa ecdsa, IReleaseSpec spec) + { + foreach (Transaction tx in block.Transactions) + { + if (tx.SenderAddress is null) + { + tx.SenderAddress = ecdsa.RecoverAddress(tx, !spec.ValidateChainId); + } + } + } + + private bool ValidateBlockMetadata(Block block, long registerGasLimit, BlockHeader parentHeader, out string? error) + { + if (!_headerValidator.Validate(block.Header)) + { + error = $"Invalid block header hash {block.Header.Hash}"; + return false; + } + + // if (!_blockTree.IsBetterThanHead(block.Header)) + // { + // error = $"Block {block.Header.Hash} is not better than head"; + // return false; + // } + + long calculatedGasLimit = GetGasLimit(parentHeader, registerGasLimit); + + if (calculatedGasLimit != block.Header.GasLimit) + { + error = $"Gas limit mismatch. Expected {calculatedGasLimit} but got {block.Header.GasLimit}"; + return false; + } + error = null; + return true; + } + + private long GetGasLimit(BlockHeader parentHeader, long desiredGasLimit) + { + long parentGasLimit = parentHeader.GasLimit; + long gasLimit = parentGasLimit; + + long? targetGasLimit = desiredGasLimit; + long newBlockNumber = parentHeader.Number + 1; + IReleaseSpec spec = _specProvider.GetSpec(newBlockNumber, parentHeader.Timestamp); + if (targetGasLimit is not null) + { + long maxGasLimitDifference = Math.Max(0, parentGasLimit / spec.GasLimitBoundDivisor - 1); + gasLimit = targetGasLimit.Value > parentGasLimit + ? parentGasLimit + Math.Min(targetGasLimit.Value - parentGasLimit, maxGasLimitDifference) + : parentGasLimit - Math.Min(parentGasLimit - targetGasLimit.Value, maxGasLimitDifference); + } + + gasLimit = Eip1559GasLimitAdjuster.AdjustGasLimit(spec, gasLimit, newBlockNumber); + return gasLimit; + } + + private bool ValidateProposerPayment(UInt256 expectedProfit, bool useBalanceDiffProfit, UInt256 feeRecipientBalanceAfter, UInt256 amtBeforeOrWithdrawn) + { + // validate proposer payment + + if (useBalanceDiffProfit && feeRecipientBalanceAfter >= amtBeforeOrWithdrawn) + { + UInt256 feeRecipientBalanceDelta = feeRecipientBalanceAfter - amtBeforeOrWithdrawn; + if (feeRecipientBalanceDelta >= expectedProfit) + { + if (feeRecipientBalanceDelta > expectedProfit) + { + _logger.Warn($"Builder claimed profit is lower than calculated profit. Expected {expectedProfit} but actual {feeRecipientBalanceDelta}"); + } + return true; + } + _logger.Warn($"Proposer payment is not enough, trying last tx payment validation, expected: {expectedProfit}, actual: {feeRecipientBalanceDelta}"); + } + + return false; + } + + private bool ValidateProcessedBlock(Block processedBlock, Address feeRecipient, UInt256 expectedProfit, TxReceipt[] receipts, out string? error) + { + // TxReceipt[] receipts = processedBlock.Hash != null ? _receiptStorage.Get(processedBlock.Hash) : []; + + if (receipts.Length == 0) + { + error = "No proposer payment receipt"; + return false; + } + + TxReceipt lastReceipt = receipts[^1]; + + if (lastReceipt.StatusCode != StatusCode.Success) + { + error = $"Proposer payment failed "; + return false; + } + + int txIndex = lastReceipt.Index; + + if (txIndex + 1 != processedBlock.Transactions.Length) + { + error = $"Proposer payment index not last transaction in the block({txIndex} of {processedBlock.Transactions.Length - 1})"; + return false; + } + + Transaction paymentTx = processedBlock.Transactions[txIndex]; + + if (paymentTx.To != feeRecipient) + { + error = $"Proposer payment transaction recipient is not the proposer,received {paymentTx.To} expected {feeRecipient}"; + return false; + } + + if (paymentTx.Value != expectedProfit) + { + error = $"Proposer payment transaction value is not the expected profit, received {paymentTx.Value} expected {expectedProfit}"; + return false; + } + + if (paymentTx.Data != null && paymentTx.Data.Value.Length != 0) + { + error = "Proposer payment transaction data is not empty"; + return false; + } + + if (paymentTx.GasPrice != processedBlock.BaseFeePerGas) + { + error = "Malformed proposer payment, gas price not equal to base fee"; + return false; + } + + if (paymentTx.MaxPriorityFeePerGas != processedBlock.BaseFeePerGas && paymentTx.MaxPriorityFeePerGas != 0) + { + error = "Malformed proposer payment, max priority fee per gas not equal to block max priority fee per gas"; + return false; + } + + if (paymentTx.MaxFeePerGas != processedBlock.BaseFeePerGas) + { + error = "Malformed proposer payment, max fee per gas not equal to block base fee per gas"; + return false; + } + + error = null; + return true; + } + + private BlockProcessor CreateBlockProcessor(IWorldState stateProvider, ITransactionProcessor transactionProcessor, IBlockCachePreWarmer? preWarmer = null) + { + return new BlockProcessor( + _specProvider, + _blockValidator, + new Consensus.Rewards.RewardCalculator(_specProvider), + new BlockProcessor.BlockValidationTransactionsExecutor(transactionProcessor, stateProvider), + stateProvider, + _receiptStorage, + transactionProcessor, + new BeaconBlockRootHandler(transactionProcessor, stateProvider), + new BlockhashStore(_specProvider, stateProvider), + logManager: _logManager, + withdrawalProcessor: new WithdrawalProcessor(stateProvider, _logManager!), + receiptsRootCalculator: new ReceiptsRootCalculator(), + preWarmer: preWarmer + ); + } +} diff --git a/src/Nethermind/Nethermind.Flashbots/IFlashbotsConfig.cs b/src/Nethermind/Nethermind.Flashbots/IFlashbotsConfig.cs new file mode 100644 index 00000000000..7142d3f0182 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/IFlashbotsConfig.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Config; + +namespace Nethermind.Flashbots; + +public interface IFlashbotsConfig : IConfig +{ + [ConfigItem(Description = "Whether to enable the Flashbots endpoints.", DefaultValue = "false")] + bool Enabled { get; set; } + + [ConfigItem(Description = "If set to true, proposer payment is calculated as a balance difference of the fee recipient", DefaultValue = "false")] + public bool UseBalanceDiffProfit { get; set; } + + [ConfigItem(Description = "If set to true, withdrawals to the fee recipient are excluded from the balance delta", DefaultValue = "false")] + public bool ExcludeWithdrawals { get; set; } + + [ConfigItem(Description = "If set to true, the pre-warmer will be enabled", DefaultValue = "false")] + public bool EnablePreWarmer { get; set; } + + [ConfigItem(Description = "If set to true, the validation will be enabled", DefaultValue = "false")] + public bool EnableValidation { get; set; } +} diff --git a/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModule.cs b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModule.cs new file mode 100644 index 00000000000..8f10a21fd5d --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModule.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using Nethermind.Core.Crypto; +using Nethermind.Flashbots.Data; +using Nethermind.Flashbots.Handlers; +using Nethermind.JsonRpc; + +namespace Nethermind.Flashbots.Modules.Flashbots; + +public class FlashbotsRpcModule : IFlashbotsRpcModule +{ + private readonly ValidateSubmissionHandler _validateSubmissionHandler; + + public FlashbotsRpcModule(ValidateSubmissionHandler validateSubmissionHandler) + { + _validateSubmissionHandler = validateSubmissionHandler; + } + + Task> IFlashbotsRpcModule.flashbots_validateBuilderSubmissionV3(BuilderBlockValidationRequest @params) => + _validateSubmissionHandler.ValidateSubmission(@params); + +} diff --git a/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModuleFactory.cs b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModuleFactory.cs new file mode 100644 index 00000000000..5f9b9d918c5 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModuleFactory.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Flashbots.Handlers; +using Nethermind.JsonRpc.Modules; + +namespace Nethermind.Flashbots.Modules.Flashbots +{ + public class FlashbotsRpcModuleFactory( + ValidateSubmissionHandler validateSubmissionHandler + ) : ModuleFactoryBase + { + private readonly ValidateSubmissionHandler _validateSubmissionHandler = validateSubmissionHandler ?? throw new ArgumentNullException(nameof(validateSubmissionHandler)); + + public override IFlashbotsRpcModule Create() + { + return new FlashbotsRpcModule(_validateSubmissionHandler); + } + } +} diff --git a/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/IFlashbotsRpcModule.cs b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/IFlashbotsRpcModule.cs new file mode 100644 index 00000000000..b81da13fb39 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/IFlashbotsRpcModule.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using Nethermind.Flashbots.Data; +using Nethermind.JsonRpc; +using Nethermind.JsonRpc.Modules; + +namespace Nethermind.Flashbots.Modules.Flashbots; + +[RpcModule(ModuleType.Flashbots)] +public interface IFlashbotsRpcModule : IRpcModule +{ + [JsonRpcMethod( + Description = " validate the builder submissions as received by a relay", + IsSharable = false, + IsImplemented = true)] + Task> flashbots_validateBuilderSubmissionV3(BuilderBlockValidationRequest @params); +} diff --git a/src/Nethermind/Nethermind.Flashbots/Nethermind.Flashbots.csproj b/src/Nethermind/Nethermind.Flashbots/Nethermind.Flashbots.csproj new file mode 100644 index 00000000000..22a6d0fedd8 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Nethermind.Flashbots.csproj @@ -0,0 +1,12 @@ + + + + Nethermind.Flashbots + enable + + + + + + + diff --git a/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs b/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs index b9389ec4ccd..832a68b842c 100644 --- a/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs +++ b/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs @@ -128,6 +128,15 @@ public interface IJsonRpcConfig : IConfig """)] int? EthModuleConcurrentInstances { get; set; } + + [ConfigItem( + Description = """ + The number of concurrent instances for non-sharable calls: + - `flashbots_validateBuilderSubmissionV3` + This limits the load on the CPU and I/O to reasonable levels. If the limit is exceeded, HTTP 503 is returned along with the JSON-RPC error. Defaults to the number of logical processors. + """)] + int? FlashbotsModuleConcurrentInstances { get; set; } + [ConfigItem(Description = "The path to the JWT secret file required for the Engine API authentication.", DefaultValue = "null")] public string JwtSecretFile { get; set; } diff --git a/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs b/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs index 7bd2761f14a..b8c7ec0d489 100644 --- a/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs +++ b/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs @@ -47,6 +47,7 @@ public string[] EnabledModules public long? MaxRequestBodySize { get; set; } = 30000000; public int MaxLogsPerResponse { get; set; } = 20_000; public int? EthModuleConcurrentInstances { get; set; } = null; + public int? FlashbotsModuleConcurrentInstances { get; set; } = null; public string JwtSecretFile { get; set; } = null; public bool UnsecureDevNoRpcAuthentication { get; set; } public int? MaxLoggedRequestParametersCharacters { get; set; } = null; diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs index 1a1989f322f..f64a3a7cad0 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs @@ -404,9 +404,10 @@ public ResultWrapper eth_getBlockByNumber(BlockParameter blockParam return ResultWrapper.Success(null); } - IByteBuffer buffer = PooledByteBufferAllocator.Default.Buffer(TxDecoder.Instance.GetLength(transaction, RlpBehaviors.None)); + IByteBuffer buffer = PooledByteBufferAllocator.Default.Buffer(TxDecoder.Instance.GetLength(transaction, RlpBehaviors.SkipTypedWrapping)); using NettyRlpStream stream = new(buffer); - TxDecoder.Instance.Encode(stream, transaction); + TxDecoder.Instance.Encode(stream, transaction, rlpBehaviors: RlpBehaviors.SkipTypedWrapping); + return ResultWrapper.Success(buffer.AsSpan().ToHexString(false)); } diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs index 045f201109b..6d730ad3628 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/ModuleType.cs @@ -15,6 +15,7 @@ public static class ModuleType public const string Erc20 = nameof(Erc20); public const string Eth = nameof(Eth); public const string Evm = nameof(Evm); + public const string Flashbots = nameof(Flashbots); public const string Net = nameof(Net); public const string Nft = nameof(Nft); public const string Parity = nameof(Parity); @@ -39,6 +40,7 @@ public static class ModuleType Erc20, Eth, Evm, + Flashbots, Net, Nft, Parity, diff --git a/src/Nethermind/Nethermind.Merge.Plugin/Data/BlobsBundleV1.cs b/src/Nethermind/Nethermind.Merge.Plugin/Data/BlobsBundleV1.cs index 8d350a98e04..4aeefd3d845 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/Data/BlobsBundleV1.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/Data/BlobsBundleV1.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Text.Json.Serialization; using Nethermind.Core; namespace Nethermind.Merge.Plugin.Data; @@ -49,6 +50,14 @@ public BlobsBundleV1(Block block) } } + [JsonConstructor] + public BlobsBundleV1(byte[][] commitments, byte[][] blobs, byte[][] proofs) + { + Commitments = commitments; + Blobs = blobs; + Proofs = proofs; + } + public byte[][] Commitments { get; } public byte[][] Blobs { get; } public byte[][] Proofs { get; } diff --git a/src/Nethermind/Nethermind.Merge.Plugin/MergeHeaderValidator.cs b/src/Nethermind/Nethermind.Merge.Plugin/MergeHeaderValidator.cs index ba2ffebab83..dc0a832f64b 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin/MergeHeaderValidator.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin/MergeHeaderValidator.cs @@ -43,6 +43,12 @@ public override bool Validate(BlockHeader header, BlockHeader? parent, bool isUn } public override bool Validate(BlockHeader header, BlockHeader? parent, bool isUncle, out string? error) { + BlockHeader? currentHeader; + if (header.Hash != null) + { + currentHeader = _blockTree.FindHeader(header.Hash, BlockTreeLookupOptions.None); + } + error = null; return _poSSwitcher.IsPostMerge(header) ? ValidateTheMergeChecks(header) && base.Validate(header, parent, isUncle, out error) diff --git a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj index 2cc6fa3b095..30aadcf77cf 100644 --- a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj +++ b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj @@ -33,6 +33,7 @@ + @@ -90,8 +91,8 @@ - - + + diff --git a/src/Nethermind/Nethermind.Runner/configs/holesky.json b/src/Nethermind/Nethermind.Runner/configs/holesky.json index a19f827bba4..0898fa2b7c2 100644 --- a/src/Nethermind/Nethermind.Runner/configs/holesky.json +++ b/src/Nethermind/Nethermind.Runner/configs/holesky.json @@ -26,9 +26,12 @@ "Port": 8545, "EngineHost": "127.0.0.1", "EnginePort": 8551, - "EngineEnabledModules": "net,eth,subscribe,engine,web3,client" + "EngineEnabledModules": "net,eth,subscribe,engine,web3,client,flashbots" }, "Merge": { "Enabled": true + }, + "Flashbots": { + "Enabled": true } } diff --git a/src/Nethermind/Nethermind.sln b/src/Nethermind/Nethermind.sln index 44cf9aa69bd..642d8487108 100644 --- a/src/Nethermind/Nethermind.sln +++ b/src/Nethermind/Nethermind.sln @@ -218,6 +218,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Signer", "Signer", "{89311B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nethermind.ExternalSigner.Plugin", "Nethermind.ExternalSigner.Plugin\Nethermind.ExternalSigner.Plugin.csproj", "{6528010D-7DCE-4935-9785-5270FF515F3E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Flashbots", "Nethermind.Flashbots\Nethermind.Flashbots.csproj", "{580DB104-AE89-444F-BD99-7FE0C84C615C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Flasbots.Test", "Nethermind.Flashbots.Test\Nethermind.Flasbots.Test.csproj", "{370C4088-0DB9-401A-872F-E72E3272AB54}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nethermind.Taiko", "Nethermind.Taiko\Nethermind.Taiko.csproj", "{B4070433-328E-40E6-B89A-6554F015694C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nethermind.Taiko.Test", "Nethermind.Taiko.Test\Nethermind.Taiko.Test.csproj", "{3E097797-F8D5-4BB5-B544-C78FF0A14986}" @@ -608,6 +611,14 @@ Global {6528010D-7DCE-4935-9785-5270FF515F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU {6528010D-7DCE-4935-9785-5270FF515F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU {6528010D-7DCE-4935-9785-5270FF515F3E}.Release|Any CPU.Build.0 = Release|Any CPU + {580DB104-AE89-444F-BD99-7FE0C84C615C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {580DB104-AE89-444F-BD99-7FE0C84C615C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {580DB104-AE89-444F-BD99-7FE0C84C615C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {580DB104-AE89-444F-BD99-7FE0C84C615C}.Release|Any CPU.Build.0 = Release|Any CPU + {370C4088-0DB9-401A-872F-E72E3272AB54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {370C4088-0DB9-401A-872F-E72E3272AB54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {370C4088-0DB9-401A-872F-E72E3272AB54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {370C4088-0DB9-401A-872F-E72E3272AB54}.Release|Any CPU.Build.0 = Release|Any CPU {B4070433-328E-40E6-B89A-6554F015694C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B4070433-328E-40E6-B89A-6554F015694C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4070433-328E-40E6-B89A-6554F015694C}.Release|Any CPU.ActiveCfg = Release|Any CPU