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 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 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 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 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);
    }