diff --git a/.github/workflows/nethermind-tests.yml b/.github/workflows/nethermind-tests.yml index c5b1ee08652..febdbfeff95 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 b5ffe466585..700f91629ef 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", "AuRaMerge", "Merge", "MEV", "HealthChecks", "Hive" }; + public string[] PluginOrder { get; set; } = { "Clique", "Aura", "Ethash", "Optimism", "Shutter", "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..54a02f7a087 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.Setup.cs @@ -0,0 +1,171 @@ +// 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 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), + 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..f47b81f79b5 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots.Test/FlashbotsModuleTests.cs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using Nethermind.Consensus.Processing; +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Crypto; +using Nethermind.Flashbots.Modules.Flashbots; +using Nethermind.Int256; +using Nethermind.State; + +namespace Nethermind.Flasbots.Test; + +public partial class FlashbotsModuleTests +{ + + public async Task TestValidateBuilderSubmissionV3() + { + using MergeTestBlockChain chain = await CreateBlockChain(); + 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 = new Transaction( + + ); + } +} 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..038d62c896f --- /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..c0b904693ea --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/BuilderBlockValidationRequest.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Text.Json.Serialization; +using Nethermind.Core.Crypto; +using Nethermind.JsonRpc; + +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..60863f294c3 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Data/SubmitBlockRequest.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Merge.Plugin.Data; + +namespace Nethermind.Flashbots.Data; + +public class SubmitBlockRequest +{ + private readonly ExecutionPayload _executionPayload; + private readonly BlobsBundleV1 _blobsBundle; + + public SubmitBlockRequest(ExecutionPayload executionPayload, BlobsBundleV1 blobsBundle, BidTrace message) + { + _executionPayload = executionPayload; + _blobsBundle = blobsBundle; + Message = message; + } + public ExecutionPayload 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..077b50ac98f --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Flashbots.cs @@ -0,0 +1,65 @@ +// 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() + { + 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, + _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..be96b14b53b --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/FlashbotsConfig.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Config; + +namespace Nethermind.Flashbots; + +public class FlashbotsConfig : IFlashbotsConfig +{ + public bool Enabled { get; set; } + public bool UseBalanceDiffProfit { get; set; } = false; + + public bool ExcludeWithdrawals { 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..1b6846c5e6c --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Handlers/ValidateBuilderSubmissionHandler.cs @@ -0,0 +1,385 @@ +// 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; + +namespace Nethermind.Flashbots.Handlers; + +public class ValidateSubmissionHandler +{ + private const ProcessingOptions ValidateSubmissionProcessingOptions = ProcessingOptions.ReadOnlyChain + | ProcessingOptions.IgnoreParentNotOnMainChain + | ProcessingOptions.ForceProcessing; + + private readonly ReadOnlyTxProcessingEnv _txProcessingEnv; + 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(); + + public ValidateSubmissionHandler( + IHeaderValidator headerValidator, + IBlockValidator blockValidator, + ReadOnlyTxProcessingEnv txProcessingEnv, + IFlashbotsConfig flashbotsConfig) + { + _headerValidator = headerValidator; + _blockValidator = blockValidator; + _txProcessingEnv = txProcessingEnv; + _blockTree = _txProcessingEnv.BlockTree; + _logger = txProcessingEnv.LogManager!.GetClassLogger(); + _flashbotsConfig = flashbotsConfig; + } + + public Task> ValidateSubmission(BuilderBlockValidationRequest request) + { + ExecutionPayload payload = request.BlockRequest.ExecutionPayload; + 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!.Length); + + 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; + } + + + IReadOnlyTxProcessingScope processingScope = _txProcessingEnv.Build(parentHeader.StateRoot!); + IWorldState currentState = processingScope.WorldState; + ITransactionProcessor transactionProcessor = processingScope.TransactionProcessor; + + UInt256 feeRecipientBalanceBefore = currentState.GetBalance(feeRecipient); + + BlockProcessor blockProcessor = CreateBlockProcessor(currentState, transactionProcessor); + + List suggestedBlocks = [block]; + BlockReceiptsTracer blockReceiptsTracer = new(); + + try + { + Block processedBlock = blockProcessor.Process(currentState.StateRoot, suggestedBlocks, ValidateSubmissionProcessingOptions, blockReceiptsTracer)[0]; + FinalizeStateAndBlock(currentState, processedBlock, _txProcessingEnv.SpecProvider.GetSpec(parentHeader), block, _blockTree); + } + 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, out error)) + { + return false; + } + + error = null; + return true; + } + + 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 = _txProcessingEnv.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, 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; + } + error = null; + return true; + } + + private BlockProcessor CreateBlockProcessor(IWorldState stateProvider, ITransactionProcessor transactionProcessor) + { + return new BlockProcessor( + _txProcessingEnv.SpecProvider, + _blockValidator, + new Consensus.Rewards.RewardCalculator(_txProcessingEnv.SpecProvider), + new BlockProcessor.BlockValidationTransactionsExecutor(transactionProcessor, stateProvider), + stateProvider, + _receiptStorage, + transactionProcessor, + new BeaconBlockRootHandler(transactionProcessor), + new BlockhashStore(_txProcessingEnv.SpecProvider, stateProvider), + logManager: _txProcessingEnv.LogManager, + withdrawalProcessor: new WithdrawalProcessor(stateProvider, _txProcessingEnv.LogManager!), + receiptsRootCalculator: new ReceiptsRootCalculator() + ); + } + + private static void FinalizeStateAndBlock(IWorldState stateProvider, Block processedBlock, IReleaseSpec currentSpec, Block currentBlock, IBlockTree blockTree) + { + stateProvider.StateRoot = processedBlock.StateRoot!; + stateProvider.Commit(currentSpec); + stateProvider.CommitTree(currentBlock.Number); + blockTree.SuggestBlock(processedBlock, BlockTreeSuggestOptions.ForceSetAsMain); + blockTree.UpdateHeadBlock(processedBlock.Hash!); + } +} diff --git a/src/Nethermind/Nethermind.Flashbots/IFlashbotsConfig.cs b/src/Nethermind/Nethermind.Flashbots/IFlashbotsConfig.cs new file mode 100644 index 00000000000..35fc68cd00c --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/IFlashbotsConfig.cs @@ -0,0 +1,18 @@ +// 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; } +} 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..066bf8fd642 --- /dev/null +++ b/src/Nethermind/Nethermind.Flashbots/Modules/Flashbots/FlashbotsRpcModule.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +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 301b21f850e..99edc395803 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 = "keystore/jwt-secret")] public string JwtSecretFile { get; set; } diff --git a/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs b/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs index 26d3d32760e..269dca89971 100644 --- a/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs +++ b/src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs @@ -39,6 +39,7 @@ public int WebSocketsPort 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; } = "keystore/jwt-secret"; public bool UnsecureDevNoRpcAuthentication { get; set; } public int? MaxLoggedRequestParametersCharacters { get; set; } = null; 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.Runner/Nethermind.Runner.csproj b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj index 613b0e330ee..e1997b43f06 100644 --- a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj +++ b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj @@ -34,6 +34,7 @@ + @@ -91,6 +92,11 @@ + + + + + diff --git a/src/Nethermind/Nethermind.Runner/configs/holesky.cfg b/src/Nethermind/Nethermind.Runner/configs/holesky.cfg index c662bd62c59..36e42c361eb 100644 --- a/src/Nethermind/Nethermind.Runner/configs/holesky.cfg +++ b/src/Nethermind/Nethermind.Runner/configs/holesky.cfg @@ -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 + }, + "BlockValidation": { + "Enabled": true } } diff --git a/src/Nethermind/Nethermind.sln b/src/Nethermind/Nethermind.sln index 68a7e1f8b66..2e2e306147f 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Shutter", "Nethermind.Shutter\Nethermind.Shutter.csproj", "{F38037D2-98EA-4263-887A-4B383635F605}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Shutter.Test", "Nethermind.Shutter.Test\Nethermind.Shutter.Test.csproj", "{CEA1C413-A96C-4339-AC1C-839B603DECC8}" @@ -604,6 +607,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 {F38037D2-98EA-4263-887A-4B383635F605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F38037D2-98EA-4263-887A-4B383635F605}.Debug|Any CPU.Build.0 = Debug|Any CPU {F38037D2-98EA-4263-887A-4B383635F605}.Release|Any CPU.ActiveCfg = Release|Any CPU