public async Task newPayloadV1_can_insert_blocks_from_cache_when_syncing()
    {
        using MergeTestBlockchain chain = await CreateBlockChain();

        IEngineRpcModule rpc          = CreateEngineModule(chain);
        Keccak           startingHead = chain.BlockTree.HeadHash;

        ExecutionPayloadV1 parentBlockRequest = new(Build.A.Block.WithNumber(2).TestObject);

        ExecutionPayloadV1[]            requests = CreateBlockRequestBranch(parentBlockRequest, Address.Zero, 7);
        ResultWrapper <PayloadStatusV1> payloadStatus;

        foreach (ExecutionPayloadV1 r in requests)
        {
            payloadStatus = await rpc.engine_newPayloadV1(r);

            payloadStatus.Data.Status.Should().Be(nameof(PayloadStatusV1.Syncing).ToUpper());
            chain.BeaconSync.IsBeaconSyncHeadersFinished().Should().BeTrue();
            chain.BeaconSync.ShouldBeInBeaconHeaders().Should().BeFalse();
            chain.BeaconPivot.BeaconPivotExists().Should().BeFalse();
        }

        int pivotNum = 3;

        requests[pivotNum].TryGetBlock(out Block? pivotBlock);
        // initiate sync
        ForkchoiceStateV1 forkchoiceStateV1 = new(pivotBlock !.Hash, startingHead, startingHead);
        ResultWrapper <ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult =
            await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);

        forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should()
        .Be(nameof(PayloadStatusV1.Syncing).ToUpper());
        // trigger insertion of blocks in cache into block tree
        payloadStatus = await rpc.engine_newPayloadV1(requests[^ 1]);
    public virtual async Task forkchoiceUpdatedV1_should_communicate_with_boost_relay_through_http()
    {
        MergeConfig mergeConfig = new() { Enabled = true, SecondsPerSlot = 1, TerminalTotalDifficulty = "0" };

        using MergeTestBlockchain chain = await CreateBlockChain(mergeConfig);

        IJsonSerializer serializer = chain.JsonSerializer;

        UInt256           timestamp         = Timestamper.UnixTime.Seconds;
        PayloadAttributes payloadAttributes = new() { Timestamp = timestamp, SuggestedFeeRecipient = Address.Zero, PrevRandao = Keccak.Zero };

        string relayUrl = "http://localhost";
        MockHttpMessageHandler mockHttp = new();

        mockHttp.Expect(HttpMethod.Post, relayUrl + BoostRelay.GetPayloadAttributesPath)
        .WithContent("{\"timestamp\":\"0x3e8\",\"prevRandao\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"suggestedFeeRecipient\":\"0x0000000000000000000000000000000000000000\"}")
        .Respond("application/json", "{\"timestamp\":\"0x3e9\",\"prevRandao\":\"0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760\",\"suggestedFeeRecipient\":\"0xb7705ae4c6f81b66cdb323c65f4e8133690fc099\"}");

        mockHttp.Expect(HttpMethod.Post, relayUrl + BoostRelay.SendPayloadPath)
        .WithContent("{\"block\":{\"parentHash\":\"0x1c53bdbf457025f80c6971a9cf50986974eed02f0a9acaeeb49cafef10efd133\",\"feeRecipient\":\"0xb7705ae4c6f81b66cdb323c65f4e8133690fc099\",\"stateRoot\":\"0x1ef7300d8961797263939a3d29bbba4ccf1702fabf02d8ad7a20b454edb6fd2f\",\"receiptsRoot\":\"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421\",\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"prevRandao\":\"0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760\",\"blockNumber\":\"0x1\",\"gasLimit\":\"0x3d0900\",\"gasUsed\":\"0x0\",\"timestamp\":\"0x3e9\",\"extraData\":\"0x\",\"baseFeePerGas\":\"0x0\",\"blockHash\":\"0xb519d89363b891216dbf9bd046f226cae9348a90ba0db00e7ade437fff7a1510\",\"transactions\":[]},\"profit\":\"0x0\"}");

        DefaultHttpClient defaultHttpClient = new(mockHttp.ToHttpClient(), serializer, chain.LogManager, 1, 100);
        BoostRelay        boostRelay        = new(defaultHttpClient, relayUrl);
        BoostBlockImprovementContextFactory improvementContextFactory = new(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5000), boostRelay, chain.StateReader);
        TimeSpan timePerSlot = TimeSpan.FromSeconds(1000);

        chain.PayloadPreparationService = new PayloadPreparationService(
            chain.PostMergeBlockProducer !,
            improvementContextFactory,
            TimerFactory.Default,
            chain.LogManager,
            timePerSlot);

        IEngineRpcModule rpc          = CreateEngineModule(chain);
        Keccak           startingHead = chain.BlockTree.HeadHash;

        ManualResetEvent wait = new(false);

        chain.PayloadPreparationService.BlockImproved += (_, _) => wait.Set();

        string payloadId = rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(startingHead, Keccak.Zero, startingHead),
                                                          payloadAttributes).Result.Data
                           .PayloadId !;

        await wait.WaitOneAsync(100, CancellationToken.None);

        ResultWrapper <ExecutionPayloadV1?> response = await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId));

        ExecutionPayloadV1 executionPayloadV1 = response.Data !;

        executionPayloadV1.FeeRecipient.Should().Be(TestItem.AddressA);
        executionPayloadV1.PrevRandao.Should().Be(TestItem.KeccakA);

        mockHttp.VerifyNoOutstandingExpectation();
    }

    [Test]
        public async Task setHead_to_unknown_block_fails()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule       rpc           = CreateConsensusModule(chain);
            ResultWrapper <Result> setHeadResult = await rpc.engine_setHead(TestItem.KeccakF);

            setHeadResult.Data.Success.Should().BeFalse();
        }
        private void AssertExecutionStatusChanged(IEngineRpcModule rpc, Keccak headBlockHash, Keccak finalizedBlockHash,
                                                  Keccak safeBlockHash)
        {
            ExecutionStatusResult?result = rpc.engine_executionStatus().Data;

            Assert.AreEqual(headBlockHash, result.HeadBlockHash);
            Assert.AreEqual(finalizedBlockHash, result.FinalizedBlockHash);
            Assert.AreEqual(safeBlockHash, result.SafeBlockHash);
        }
    public async Task forkChoiceUpdatedV1_unknown_block_initiates_syncing()
    {
        using MergeTestBlockchain chain = await CreateBlockChain();

        IEngineRpcModule rpc          = CreateEngineModule(chain);
        Keccak?          startingHead = chain.BlockTree.HeadHash;
        BlockHeader      parent       = Build.A.BlockHeader
                                        .WithNumber(1)
                                        .WithHash(TestItem.KeccakA)
                                        .WithNonce(0)
                                        .WithDifficulty(0)
                                        .TestObject;
        Block block = Build.A.Block
                      .WithNumber(2)
                      .WithParent(parent)
                      .WithNonce(0)
                      .WithDifficulty(0)
                      .WithAuthor(Address.Zero)
                      .WithPostMergeFlag(true)
                      .TestObject;
        await rpc.engine_newPayloadV1(new ExecutionPayloadV1(block));

        // sync has not started yet
        chain.BeaconSync.IsBeaconSyncHeadersFinished().Should().BeTrue();
        chain.BeaconSync.IsBeaconSyncFinished(block.Header).Should().BeTrue();
        chain.BeaconSync.ShouldBeInBeaconHeaders().Should().BeFalse();
        chain.BeaconPivot.BeaconPivotExists().Should().BeFalse();
        BlockTreePointers pointers = new()
        {
            BestKnownNumber            = 0,
            BestSuggestedHeader        = chain.BlockTree.Genesis !,
            BestSuggestedBody          = chain.BlockTree.FindBlock(0) !,
            BestKnownBeaconBlock       = 0,
            LowestInsertedHeader       = null,
            LowestInsertedBeaconHeader = null
        };

        AssertBlockTreePointers(chain.BlockTree, pointers);

        ForkchoiceStateV1 forkchoiceStateV1 = new(block.Hash !, startingHead, startingHead);
        ResultWrapper <ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult =
            await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);

        forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should()
        .Be(nameof(PayloadStatusV1.Syncing).ToUpper());

        chain.BeaconSync.ShouldBeInBeaconHeaders().Should().BeTrue();
        chain.BeaconSync.IsBeaconSyncHeadersFinished().Should().BeFalse();
        chain.BeaconSync.IsBeaconSyncFinished(chain.BlockTree.FindBlock(block.Hash)?.Header).Should().BeFalse();
        AssertBeaconPivotValues(chain.BeaconPivot, block.Header);
        pointers.LowestInsertedBeaconHeader = block.Header;
        pointers.BestKnownBeaconBlock       = block.Number;
        pointers.LowestInsertedHeader       = block.Header;
        AssertBlockTreePointers(chain.BlockTree, pointers);
        AssertExecutionStatusNotChangedV1(rpc, block.Hash !, startingHead, startingHead);
    }
        public async Task assembleBlock_should_not_create_block_with_unknown_parent()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule     rpc                     = CreateConsensusModule(chain);
            Keccak               notExistingHash         = TestItem.KeccakH;
            AssembleBlockRequest assembleBlockRequest    = new() { ParentHash = notExistingHash };
            ResultWrapper <BlockRequestResult?> response = await rpc.engine_assembleBlock(assembleBlockRequest);

            response.Data.Should().BeNull();
        }
        public async Task finaliseBlock_should_succeed()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule rpc   = CreateConsensusModule(chain);
            Block            block = Build.A.Block.WithParent(chain.BlockTree.Head !).TestObject;

            chain.BlockTree.SuggestBlock(block);
            ResultWrapper <Result> resultWrapper = await rpc.engine_finaliseBlock(block.Hash !);

            resultWrapper.Data.Should().Be(Result.Ok);
        }
        public async Task can_progress_chain_one_by_one(int count)
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule rpc      = CreateConsensusModule(chain);
            Keccak           lastHash = (await ProduceBranch(rpc, chain.BlockTree, count, chain.BlockTree.HeadHash, true)).Last().BlockHash;

            chain.BlockTree.HeadHash.Should().Be(lastHash);
            Block?last = RunForAllBlocksInBranch(chain.BlockTree, chain.BlockTree.HeadHash, b => b.IsGenesis, true);

            last.Should().NotBeNull();
            last !.IsGenesis.Should().BeTrue();
        }
        public async Task newBlock_accepts_first_block()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule   rpc = CreateConsensusModule(chain);
            BlockRequestResult blockRequestResult = CreateBlockRequest(
                CreateParentBlockRequestOnHead(chain.BlockTree),
                TestItem.AddressD);
            ResultWrapper <NewBlockResult> resultWrapper = await rpc.engine_newBlock(blockRequestResult);

            resultWrapper.Data.Valid.Should().BeTrue();
            new BlockRequestResult(chain.BlockTree.BestSuggestedBody).Should().BeEquivalentTo(blockRequestResult);
        }
    public async Task should_return_invalid_lvh_null_on_invalid_blocks_during_the_sync()
    {
        using MergeTestBlockchain chain = await CreateBlockChain();

        IEngineRpcModule rpc          = CreateEngineModule(chain);
        Keccak?          startingHead = chain.BlockTree.HeadHash;
        BlockHeader      parent       = Build.A.BlockHeader
                                        .WithNumber(1)
                                        .WithHash(TestItem.KeccakA)
                                        .WithNonce(0)
                                        .WithDifficulty(0)
                                        .TestObject;
        Block block = Build.A.Block
                      .WithNumber(2)
                      .WithParent(parent)
                      .WithNonce(0)
                      .WithDifficulty(0)
                      .WithAuthor(Address.Zero)
                      .WithPostMergeFlag(true)
                      .TestObject;
        ExecutionPayloadV1 startingNewPayload = new(block);
        await rpc.engine_newPayloadV1(startingNewPayload);

        ForkchoiceStateV1 forkchoiceStateV1 = new(block.Hash !, startingHead, startingHead);
        ResultWrapper <ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult =
            await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);

        forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should()
        .Be(nameof(PayloadStatusV1.Syncing).ToUpper());

        ExecutionPayloadV1[] requests = CreateBlockRequestBranch(startingNewPayload, TestItem.AddressD, 1);
        foreach (ExecutionPayloadV1 r in requests)
        {
            ResultWrapper <PayloadStatusV1> payloadStatus = await rpc.engine_newPayloadV1(r);

            payloadStatus.Data.Status.Should().Be(nameof(PayloadStatusV1.Syncing).ToUpper());
        }

        ExecutionPayloadV1[] invalidRequests = CreateBlockRequestBranch(requests[0], TestItem.AddressD, 1);
        foreach (ExecutionPayloadV1 r in invalidRequests)
        {
            r.TryGetBlock(out Block? newBlock);
            newBlock !.Header.GasLimit = long.MaxValue; // incorrect gas limit
            newBlock.Header.Hash       = newBlock.CalculateHash();
            ResultWrapper <PayloadStatusV1> payloadStatus = await rpc.engine_newPayloadV1(new ExecutionPayloadV1(newBlock));

            payloadStatus.Data.Status.Should().Be(nameof(PayloadStatusV1.Invalid).ToUpper());
            payloadStatus.Data.LatestValidHash.Should().BeNull();
        }
    }
        public async Task newBlock_accepts_already_known_block()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule rpc   = CreateConsensusModule(chain);
            Block            block = Build.A.Block.WithNumber(1).WithParent(chain.BlockTree.Head !).TestObject;

            block.Header.Hash = new Keccak("0xdc3419cbd81455372f3e576f930560b35ec828cd6cdfbd4958499e43c68effdf");
            chain.BlockTree.SuggestBlock(block);

            ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(new BlockRequestResult(block));

            newBlockResult.Data.Valid.Should().BeTrue();
        }
        public async Task setHead_no_common_branch_fails()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule rpc    = CreateConsensusModule(chain);
            BlockHeader      parent = Build.A.BlockHeader.WithNumber(1).WithHash(TestItem.KeccakA).TestObject;
            Block            block  = Build.A.Block.WithNumber(2).WithParent(parent).TestObject;

            chain.BlockTree.SuggestBlock(block);

            ResultWrapper <Result> setHeadResult = await rpc.engine_setHead(block.Hash !);

            setHeadResult.Data.Success.Should().BeFalse();
        }
    public async Task forkchoiceUpdatedV1_should_ignore_gas_limit([Values(false, true)] bool relay)
    {
        MergeConfig mergeConfig = new() { Enabled = true, SecondsPerSlot = 1, TerminalTotalDifficulty = "0" };

        using MergeTestBlockchain chain = await CreateBlockChain(mergeConfig);

        IBlockImprovementContextFactory improvementContextFactory;

        if (relay)
        {
            IBoostRelay boostRelay = Substitute.For <IBoostRelay>();
            boostRelay.GetPayloadAttributes(Arg.Any <PayloadAttributes>(), Arg.Any <CancellationToken>())
            .Returns(c => c.Arg <PayloadAttributes>());

            improvementContextFactory = new BoostBlockImprovementContextFactory(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5), boostRelay, chain.StateReader);
        }
        else
        {
            improvementContextFactory = new BlockImprovementContextFactory(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5));
        }

        TimeSpan timePerSlot = TimeSpan.FromSeconds(10);

        chain.PayloadPreparationService = new PayloadPreparationService(
            chain.PostMergeBlockProducer !,
            improvementContextFactory,
            TimerFactory.Default,
            chain.LogManager,
            timePerSlot);

        IEngineRpcModule rpc          = CreateEngineModule(chain);
        Keccak           startingHead = chain.BlockTree.HeadHash;
        UInt256          timestamp    = Timestamper.UnixTime.Seconds;
        Keccak           random       = Keccak.Zero;
        Address          feeRecipient = Address.Zero;

        string payloadId = rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(startingHead, Keccak.Zero, startingHead),
                                                          new PayloadAttributes {
            Timestamp = timestamp, SuggestedFeeRecipient = feeRecipient, PrevRandao = random, GasLimit = 10_000_000L
        }).Result.Data
                           .PayloadId !;

        ResultWrapper <ExecutionPayloadV1?> response = await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId));

        ExecutionPayloadV1 executionPayloadV1 = response.Data !;

        executionPayloadV1.GasLimit.Should().Be(4_000_000L);
    }
        public async Task setHead_can_reorganize_to_any_block()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule rpc = CreateConsensusModule(chain);

            async Task CanReorganizeToBlock(BlockRequestResult block, MergeTestBlockchain testChain)
            {
                ResultWrapper <Result> result = await rpc.engine_setHead(block.BlockHash);

                result.Data.Should().Be(Result.Ok);
                testChain.BlockTree.HeadHash.Should().Be(block.BlockHash);
                testChain.BlockTree.Head !.Number.Should().Be(block.Number);
                testChain.State.StateRoot.Should().Be(testChain.BlockTree.Head !.StateRoot !);
            }

            async Task CanReorganizeToAnyBlock(MergeTestBlockchain testChain, params IReadOnlyList <BlockRequestResult>[] branches)
        public async Task newBlock_rejects_incorrect_input(Action <BlockRequestResult> breakerAction)
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule   rpc = CreateConsensusModule(chain);
            BlockRequestResult assembleBlockResult = await GetAssembleBlockResult(chain, rpc);

            Keccak blockHash = assembleBlockResult.BlockHash;

            breakerAction(assembleBlockResult);
            if (blockHash == assembleBlockResult.BlockHash && TryCalculateHash(assembleBlockResult, out var hash))
            {
                assembleBlockResult.BlockHash = hash;
            }

            ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(assembleBlockResult);

            newBlockResult.Data.Valid.Should().BeFalse();
        }
        public async Task assembleBlock_should_create_block_on_top_of_genesis()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule     rpc                     = CreateConsensusModule(chain);
            Keccak               startingHead            = chain.BlockTree.HeadHash;
            UInt256              timestamp               = Timestamper.UnixTime.Seconds;
            AssembleBlockRequest assembleBlockRequest    = new() { ParentHash = startingHead, Timestamp = timestamp };
            ResultWrapper <BlockRequestResult?> response = await rpc.engine_assembleBlock(assembleBlockRequest);

            BlockRequestResult expected = CreateParentBlockRequestOnHead(chain.BlockTree);

            expected.GasLimit   = 4000000L;
            expected.BlockHash  = new Keccak("0xfe37027d377e75ffb161f11733d8880083378fe6236270c7a2ee1fc7efe71cfd");
            expected.LogsBloom  = Bloom.Empty;
            expected.Miner      = chain.MinerAddress;
            expected.Number     = 1;
            expected.ParentHash = startingHead;
            expected.SetTransactions(Array.Empty <Transaction>());
            expected.Timestamp = timestamp;

            response.Data.Should().BeEquivalentTo(expected);
        }
        public async Task setHead_should_change_head()
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule rpc          = CreateConsensusModule(chain);
            Keccak           startingHead = chain.BlockTree.HeadHash;

            BlockRequestResult blockRequestResult = CreateBlockRequest(
                CreateParentBlockRequestOnHead(chain.BlockTree),
                TestItem.AddressD);
            ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(blockRequestResult);

            newBlockResult.Data.Valid.Should().BeTrue();

            Keccak newHeadHash = blockRequestResult.BlockHash;
            ResultWrapper <Result> setHeadResult = await rpc.engine_setHead(newHeadHash !);

            setHeadResult.Data.Should().Be(Result.Ok);

            Keccak actualHead = chain.BlockTree.HeadHash;

            actualHead.Should().NotBe(startingHead);
            actualHead.Should().Be(newHeadHash);
        }
        public async Task newBlock_accepts_previously_assembled_block_multiple_times([Values(1, 3)] int times)
        {
            using MergeTestBlockchain chain = await CreateBlockChain();

            IEngineRpcModule     rpc          = CreateConsensusModule(chain);
            Keccak               startingHead = chain.BlockTree.HeadHash;
            BlockHeader          startingBestSuggestedHeader        = chain.BlockTree.BestSuggestedHeader !;
            AssembleBlockRequest assembleBlockRequest               = new() { ParentHash = startingHead };
            ResultWrapper <BlockRequestResult?> assembleBlockResult = await rpc.engine_assembleBlock(assembleBlockRequest);

            assembleBlockResult.Data !.ParentHash.Should().Be(startingHead);

            for (int i = 0; i < times; i++)
            {
                ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(assembleBlockResult.Data !);

                newBlockResult.Data.Valid.Should().BeTrue();
            }

            Keccak bestSuggestedHeaderHash = chain.BlockTree.BestSuggestedHeader !.Hash !;

            bestSuggestedHeaderHash.Should().Be(assembleBlockResult.Data !.BlockHash);
            bestSuggestedHeaderHash.Should().NotBe(startingBestSuggestedHeader !.Hash !);
        }
    public async Task forkchoiceUpdatedV1_should_communicate_with_boost_relay()
    {
        MergeConfig mergeConfig = new() { Enabled = true, SecondsPerSlot = 1, TerminalTotalDifficulty = "0" };

        using MergeTestBlockchain chain = await CreateBlockChain(mergeConfig);

        IBoostRelay boostRelay = Substitute.For <IBoostRelay>();

        boostRelay.GetPayloadAttributes(Arg.Any <PayloadAttributes>(), Arg.Any <CancellationToken>())
        .Returns(c =>
        {
            PayloadAttributes payloadAttributes     = c.Arg <PayloadAttributes>();
            payloadAttributes.SuggestedFeeRecipient = TestItem.AddressA;
            payloadAttributes.PrevRandao            = TestItem.KeccakA;
            payloadAttributes.Timestamp            += 1;
            payloadAttributes.GasLimit = 10_000_000L;
            return(payloadAttributes);
        });

        BoostBlockImprovementContextFactory improvementContextFactory = new(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5), boostRelay, chain.StateReader);
        TimeSpan timePerSlot = TimeSpan.FromSeconds(10);

        chain.PayloadPreparationService = new PayloadPreparationService(
            chain.PostMergeBlockProducer !,
            improvementContextFactory,
            TimerFactory.Default,
            chain.LogManager,
            timePerSlot);

        IEngineRpcModule rpc          = CreateEngineModule(chain);
        Keccak           startingHead = chain.BlockTree.HeadHash;
        UInt256          timestamp    = Timestamper.UnixTime.Seconds;
        Keccak           random       = Keccak.Zero;
        Address          feeRecipient = Address.Zero;

        BoostExecutionPayloadV1?sentItem = null;

        boostRelay.When(b => b.SendPayload(Arg.Any <BoostExecutionPayloadV1>(), Arg.Any <CancellationToken>()))
        .Do(c => sentItem = c.Arg <BoostExecutionPayloadV1>());

        ManualResetEvent wait = new(false);

        chain.PayloadPreparationService.BlockImproved += (_, _) => wait.Set();

        string payloadId = rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(startingHead, Keccak.Zero, startingHead),
                                                          new PayloadAttributes {
            Timestamp = timestamp, SuggestedFeeRecipient = feeRecipient, PrevRandao = random
        }).Result.Data
                           .PayloadId !;

        await wait.WaitOneAsync(100, CancellationToken.None);

        ResultWrapper <ExecutionPayloadV1?> response = await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId));

        ExecutionPayloadV1 executionPayloadV1 = response.Data !;

        executionPayloadV1.FeeRecipient.Should().Be(TestItem.AddressA);
        executionPayloadV1.PrevRandao.Should().Be(TestItem.KeccakA);
        executionPayloadV1.GasLimit.Should().Be(10_000_000L);
        executionPayloadV1.Should().BeEquivalentTo(sentItem !.Block);
        sentItem.Profit.Should().Be(0);
    }