-
Notifications
You must be signed in to change notification settings - Fork 694
Expand file tree
/
Copy pathEngineModuleTests.V1.cs
More file actions
2129 lines (1805 loc) · 113 KB
/
EngineModuleTests.V1.cs
File metadata and controls
2129 lines (1805 loc) · 113 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using FluentAssertions;
using Nethermind.Blockchain;
using Nethermind.Blockchain.Find;
using Nethermind.Blockchain.Synchronization;
using Nethermind.Consensus.Processing;
using Nethermind.Consensus.Producers;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Extensions;
using Nethermind.Core.Specs;
using Nethermind.Core.Test.Builders;
using Nethermind.Core.Test.Container;
using Nethermind.Crypto;
using Nethermind.Facade.Eth;
using Nethermind.HealthChecks;
using Nethermind.Int256;
using Nethermind.JsonRpc;
using Nethermind.JsonRpc.Modules;
using Nethermind.JsonRpc.Test;
using Nethermind.JsonRpc.Test.Modules;
using Nethermind.Logging;
using Nethermind.Merge.Plugin.Data;
using Nethermind.Merge.Plugin.Handlers;
using Nethermind.Serialization.Json;
using Nethermind.Specs;
using Nethermind.Specs.ChainSpecStyle;
using Nethermind.Specs.Forks;
using Nethermind.State;
using NSubstitute;
using NUnit.Framework;
namespace Nethermind.Merge.Plugin.Test;
public partial class EngineModuleTests
{
[TestCase(
"0xb1b3b07ef3832bd409a04fdea9bf2bfa83d7af0f537ff25f4a3d2eb632ebfb0f",
"0x1c53bdbf457025f80c6971a9cf50986974eed02f0a9acaeeb49cafef10efd133",
"0x5adf9b330b6c3fe0")]
public virtual async Task processing_block_should_serialize_valid_responses(string blockHash, string latestValidHash, string payloadId)
{
using MergeTestBlockchain chain = await CreateBlockchain(null, new MergeConfig()
{
TerminalTotalDifficulty = "0"
});
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
Hash256 prevRandao = Keccak.Zero;
Address feeRecipient = TestItem.AddressC;
UInt256 timestamp = Timestamper.UnixTime.Seconds;
var forkChoiceUpdatedParams = new
{
headBlockHash = startingHead.ToString(),
safeBlockHash = startingHead.ToString(),
finalizedBlockHash = Keccak.Zero.ToString(),
};
var preparePayloadParams = new
{
timestamp = timestamp.ToHexString(true),
prevRandao = prevRandao.ToString(),
suggestedFeeRecipient = feeRecipient.ToString(),
};
string?[] parameters =
{
JsonSerializer.Serialize(forkChoiceUpdatedParams),
JsonSerializer.Serialize(preparePayloadParams)
};
// prepare a payload
string result = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV1", parameters!);
byte[] expectedPayloadId = Bytes.FromHexString(payloadId);
result.Should().Be($"{{\"jsonrpc\":\"2.0\",\"result\":{{\"payloadStatus\":{{\"status\":\"VALID\",\"latestValidHash\":\"{latestValidHash}\",\"validationError\":null}},\"payloadId\":\"{expectedPayloadId.ToHexString(true)}\"}},\"id\":67}}");
Hash256 expectedBlockHash = new(blockHash);
string? expectedPayload = chain.JsonSerializer.Serialize(new ExecutionPayload
{
BaseFeePerGas = 0,
BlockHash = expectedBlockHash,
BlockNumber = 1,
ExtraData = Bytes.FromHexString("0x4e65746865726d696e64"), // Nethermind
FeeRecipient = feeRecipient,
GasLimit = chain.BlockTree.Head!.GasLimit,
GasUsed = 0,
LogsBloom = Bloom.Empty,
ParentHash = startingHead,
PrevRandao = prevRandao,
ReceiptsRoot = chain.BlockTree.Head!.ReceiptsRoot!,
StateRoot = chain.BlockTree.Head!.StateRoot!,
Timestamp = timestamp.ToUInt64(null),
Transactions = []
});
// get the payload
result = await RpcTest.TestSerializedRequest(rpc, "engine_getPayloadV1", expectedPayloadId.ToHexString(true));
result.Should().Be($"{{\"jsonrpc\":\"2.0\",\"result\":{expectedPayload},\"id\":67}}");
// execute the payload
result = await RpcTest.TestSerializedRequest(rpc, "engine_newPayloadV1", expectedPayload);
result.Should().Be($"{{\"jsonrpc\":\"2.0\",\"result\":{{\"status\":\"VALID\",\"latestValidHash\":\"{expectedBlockHash}\",\"validationError\":null}},\"id\":67}}");
forkChoiceUpdatedParams = new
{
headBlockHash = expectedBlockHash.ToString(true),
safeBlockHash = expectedBlockHash.ToString(true),
finalizedBlockHash = startingHead.ToString(true),
};
parameters = new[] { JsonSerializer.Serialize(forkChoiceUpdatedParams), null };
// update the fork choice
result = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV1", parameters!);
result.Should().Be("{\"jsonrpc\":\"2.0\",\"result\":{\"payloadStatus\":{\"status\":\"VALID\",\"latestValidHash\":\"" +
expectedBlockHash +
"\",\"validationError\":null},\"payloadId\":null},\"id\":67}");
}
[Test]
public async Task can_parse_forkchoiceUpdated_with_implicit_null_payloadAttributes()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
var forkChoiceUpdatedParams = new
{
headBlockHash = Keccak.Zero.ToString(),
safeBlockHash = Keccak.Zero.ToString(),
finalizedBlockHash = Keccak.Zero.ToString(),
};
string[] parameters = new[] { JsonSerializer.Serialize(forkChoiceUpdatedParams) };
string? result = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV1", parameters);
result.Should().Be("{\"jsonrpc\":\"2.0\",\"result\":{\"payloadStatus\":{\"status\":\"SYNCING\",\"latestValidHash\":null,\"validationError\":null},\"payloadId\":null},\"id\":67}");
}
[Test]
public void ForkchoiceV1_ToString_returns_correct_results()
{
ForkchoiceStateV1 forkchoiceState = new(TestItem.KeccakA, TestItem.KeccakF, TestItem.KeccakC);
forkchoiceState.ToString().Should().Be("ForkChoice: 0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760, Safe: 0x017e667f4b8c174291d1543c466717566e206df1bfd6f30271055ddafdb18f72, Finalized: 0xe61d9a3d3848fb2cdd9a2ab61e2f21a10ea431275aed628a0557f9dee697c37a");
}
[Test]
public void ForkchoiceV1_ToString_with_block_numbers_returns_correct_results()
{
ForkchoiceStateV1 forkchoiceState = new(TestItem.KeccakA, TestItem.KeccakF, TestItem.KeccakC);
forkchoiceState.ToString(1, 2, 3).Should().Be("ForkChoice: 1 (0x03783f...35b760), Safe: 2 (0x017e66...b18f72), Finalized: 3 (0xe61d9a...97c37a)");
}
[Test]
public async Task engine_forkchoiceUpdatedV1_with_payload_attributes_should_create_block_on_top_of_genesis_and_not_change_head()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ulong timestamp = 30;
Hash256 random = Keccak.Zero;
Address feeRecipient = TestItem.AddressD;
ExecutionPayload? executionPayloadV1 = await BuildAndGetPayloadResult(rpc, chain, startingHead,
Keccak.Zero, startingHead, timestamp, random, feeRecipient);
ExecutionPayload expected = CreateParentBlockRequestOnHead(chain.BlockTree);
expected.GasLimit = 4000000L;
expected.BlockHash = ExpectedBlockHash;
expected.LogsBloom = Bloom.Empty;
expected.FeeRecipient = feeRecipient;
expected.BlockNumber = 1;
expected.PrevRandao = random;
expected.ParentHash = startingHead;
expected.SetTransactions([]);
expected.Timestamp = timestamp;
expected.PrevRandao = random;
expected.ExtraData = Encoding.UTF8.GetBytes("Nethermind");
executionPayloadV1.Should().BeEquivalentTo(expected, static o => o.IgnoringCyclicReferences());
Hash256 actualHead = chain.BlockTree.HeadHash;
actualHead.Should().NotBe(expected.BlockHash);
actualHead.Should().Be(startingHead);
}
protected virtual Hash256 ExpectedBlockHash => new("0x3accc4186d73f4826acf1a8da3f7c696f16c3863e4f76b1315d65daa88fe28ff");
[Test]
public async Task forkchoiceUpdatedV1_should_not_create_block_or_change_head_with_unknown_parent()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
Hash256 notExistingHash = TestItem.KeccakH;
ulong timestamp = Timestamper.UnixTime.Seconds;
Hash256 random = Keccak.Zero;
Address feeRecipient = Address.Zero;
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedV1Response = await rpc.engine_forkchoiceUpdatedV1(
new ForkchoiceStateV1(notExistingHash, Keccak.Zero, notExistingHash),
new PayloadAttributes { Timestamp = timestamp, SuggestedFeeRecipient = feeRecipient, PrevRandao = random });
forkchoiceUpdatedV1Response.Data.PayloadStatus.Status.Should()
.Be(PayloadStatus.Syncing); // ToDo wait for final PostMerge sync
byte[] payloadId = Bytes.FromHexString("0x5d071947bfcc3e65");
ResultWrapper<ExecutionPayload?> getResponse = await rpc.engine_getPayloadV1(payloadId);
getResponse.ErrorCode.Should().Be(MergeErrorCodes.UnknownPayload);
Hash256 actualHead = chain.BlockTree.HeadHash;
actualHead.Should().NotBe(notExistingHash);
actualHead.Should().Be(startingHead);
}
[Test]
public async Task executePayloadV1_accepts_previously_assembled_block_multiple_times([Values(1, 3)] int times)
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
BlockHeader startingBestSuggestedHeader = chain.BlockTree.BestSuggestedHeader!;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
getPayloadResult.ParentHash.Should().Be(startingHead);
for (int i = 0; i < times; i++)
{
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Valid);
}
Hash256 bestSuggestedHeaderHash = chain.BlockTree.BestSuggestedHeader!.Hash!;
bestSuggestedHeaderHash.Should().Be(getPayloadResult.BlockHash);
bestSuggestedHeaderHash.Should().NotBe(startingBestSuggestedHeader!.Hash!);
}
[Test]
public async Task executePayloadV1_accepts_previously_prepared_block_multiple_times([Values(1, 3)] int times)
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
BlockHeader startingBestSuggestedHeader = chain.BlockTree.BestSuggestedHeader!;
ExecutionPayload getPayloadResult = await PrepareAndGetPayloadResultV1(chain, rpc);
getPayloadResult.ParentHash.Should().Be(startingHead);
for (int i = 0; i < times; i++)
{
ResultWrapper<PayloadStatusV1>? executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Valid);
}
Hash256 bestSuggestedHeaderHash = chain.BlockTree.BestSuggestedHeader!.Hash!;
bestSuggestedHeaderHash.Should().Be(getPayloadResult.BlockHash);
bestSuggestedHeaderHash.Should().NotBe(startingBestSuggestedHeader!.Hash!);
}
[Test]
public async Task block_should_not_be_canonical_before_forkchoiceUpdatedV1()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
Hash256 newHead = getPayloadResult.BlockHash!;
await rpc.engine_newPayloadV1(getPayloadResult);
chain.BlockTree.FindBlock(newHead, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(newHead, BlockTreeLookupOptions.None).Should().NotBeNull();
await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(newHead, Keccak.Zero, Keccak.Zero));
chain.BlockTree.FindBlock(newHead, BlockTreeLookupOptions.RequireCanonical).Should().NotBeNull();
chain.BlockTree.FindBlock(newHead, BlockTreeLookupOptions.None).Should().NotBeNull();
}
[Test]
public async Task block_should_not_be_canonical_after_reorg()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
Hash256 finalizedHash = Keccak.Zero;
ulong timestamp = 30;
Hash256 random = Keccak.Zero;
Address feeRecipientA = TestItem.AddressD;
Address feeRecipientB = TestItem.AddressE;
ExecutionPayload getPayloadResultA = await BuildAndGetPayloadResult(rpc, chain, startingHead,
finalizedHash, startingHead, timestamp, random, feeRecipientA);
Hash256 blochHashA = getPayloadResultA.BlockHash!;
ExecutionPayload getPayloadResultB = await BuildAndGetPayloadResult(rpc, chain, startingHead,
finalizedHash, startingHead, timestamp, random, feeRecipientB);
Hash256 blochHashB = getPayloadResultB.BlockHash!;
await rpc.engine_newPayloadV1(getPayloadResultA);
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.None).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.None).Should().BeNull();
await rpc.engine_newPayloadV1(getPayloadResultB);
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.None).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.None).Should().NotBeNull();
await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(blochHashA, finalizedHash, startingHead));
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.RequireCanonical).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.None).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.None).Should().NotBeNull();
await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(blochHashB, finalizedHash, startingHead));
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.RequireCanonical).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.None).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.None).Should().NotBeNull();
await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(blochHashA, finalizedHash, startingHead));
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.RequireCanonical).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.RequireCanonical).Should().BeNull();
chain.BlockTree.FindBlock(blochHashA, BlockTreeLookupOptions.None).Should().NotBeNull();
chain.BlockTree.FindBlock(blochHashB, BlockTreeLookupOptions.None).Should().NotBeNull();
}
private async Task<ExecutionPayload> PrepareAndGetPayloadResultV1(MergeTestBlockchain chain,
IEngineRpcModule rpc)
{
Hash256 startingHead = chain.BlockTree.HeadHash;
ulong timestamp = Timestamper.UnixTime.Seconds;
Hash256 random = Keccak.Zero;
Address feeRecipient = Address.Zero;
return await PrepareAndGetPayloadResultV1(rpc, startingHead, timestamp, random, feeRecipient);
}
private async Task<ExecutionPayload> PrepareAndGetPayloadResultV1(
IEngineRpcModule rpc, Hash256 currentHead, ulong timestamp, Hash256 random, Address feeRecipient)
{
PayloadAttributes? payloadAttributes = new()
{
PrevRandao = random,
SuggestedFeeRecipient = feeRecipient,
Timestamp = timestamp
};
ForkchoiceStateV1? forkchoiceStateV1 = new(currentHead, currentHead, currentHead);
ResultWrapper<ForkchoiceUpdatedV1Result>? forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1, payloadAttributes);
byte[] payloadId = Bytes.FromHexString(forkchoiceUpdatedResult.Data.PayloadId!);
ResultWrapper<ExecutionPayload?> getPayloadResult = await rpc.engine_getPayloadV1(payloadId);
return getPayloadResult.Data!;
}
public static IEnumerable WrongInputTestsV1
{
get
{
yield return GetNewBlockRequestBadDataTestCase(static r => r.ReceiptsRoot, TestItem.KeccakD);
yield return GetNewBlockRequestBadDataTestCase(static r => r.StateRoot, TestItem.KeccakD);
Bloom bloom = new();
bloom.Add([
Build.A.LogEntry.WithAddress(TestItem.AddressA).WithTopics(TestItem.KeccakG).TestObject
]);
yield return GetNewBlockRequestBadDataTestCase(static r => r.LogsBloom, bloom);
yield return GetNewBlockRequestBadDataTestCase(static r => r.Transactions, [[1]]);
yield return GetNewBlockRequestBadDataTestCase(static r => r.GasUsed, 1);
}
}
[Test]
public async Task executePayloadV1_unknown_parentHash_return_syncing()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
Hash256 blockHash = getPayloadResult.BlockHash;
getPayloadResult.ParentHash = TestItem.KeccakF;
if (blockHash == getPayloadResult.BlockHash && TryCalculateHash(getPayloadResult, out Hash256? hash))
{
getPayloadResult.BlockHash = hash;
}
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Syncing);
}
[TestCaseSource(nameof(WrongInputTestsV1))]
public async Task executePayloadV1_rejects_incorrect_input(Action<ExecutionPayload> breakerAction)
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
breakerAction(getPayloadResult);
if (TryCalculateHash(getPayloadResult, out Hash256? hash))
{
getPayloadResult.BlockHash = hash;
}
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Invalid);
}
[Test]
public async Task executePayloadV1_rejects_invalid_blockHash()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
getPayloadResult.BlockHash = TestItem.KeccakC;
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Invalid);
}
[Test]
public async Task executePayloadV1_rejects_block_with_invalid_timestamp()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
getPayloadResult.Timestamp = chain.BlockTree.Head!.Timestamp - 1;
Block? block = getPayloadResult.TryGetBlock().Block;
getPayloadResult.BlockHash = block!.Header.CalculateHash();
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Invalid);
}
[Test]
public async Task executePayloadV1_rejects_block_with_invalid_receiptsRoot()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload getPayloadResult = await BuildAndGetPayloadResult(chain, rpc);
getPayloadResult.ReceiptsRoot = TestItem.KeccakA;
Block? block = getPayloadResult.TryGetBlock().Block;
getPayloadResult.BlockHash = block!.Header.CalculateHash();
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(getPayloadResult);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Invalid);
chain.BlockFinder.SearchForBlock(new BlockParameter(getPayloadResult.BlockHash)).IsError.Should().BeTrue();
}
[Test]
public async Task executePayloadV1_result_is_fail_when_blockchain_processor_reports_exception()
{
using MergeTestBlockchain chain = await CreateBaseBlockchain()
.Build(new TestSingleReleaseSpecProvider(London.Instance));
IEngineRpcModule rpc = chain.EngineRpcModule;
((TestBranchProcessorInterceptor)chain.BranchProcessor).ExceptionToThrow =
new Exception("unexpected exception");
ExecutionPayload executionPayload = CreateBlockRequest(chain, CreateParentBlockRequestOnHead(chain.BlockTree), TestItem.AddressD);
ResultWrapper<PayloadStatusV1> resultWrapper = await rpc.engine_newPayloadV1(executionPayload);
resultWrapper.Result.ResultType.Should().Be(ResultType.Failure);
}
[TestCase(true)]
[TestCase(false)]
[CancelAfter(30000)]
public virtual async Task executePayloadV1_accepts_already_known_block(bool throttleBlockProcessor, CancellationToken cancellationToken)
{
using MergeTestBlockchain chain = await CreateBaseBlockchain()
.ThrottleBlockProcessor(throttleBlockProcessor ? 100 : 0)
.Build(new TestSingleReleaseSpecProvider(London.Instance));
IEngineRpcModule rpc = chain.EngineRpcModule;
Block block = Build.A.Block.WithNumber(1).WithParent(chain.BlockTree.Head!).WithDifficulty(0).WithNonce(0)
.WithStateRoot(new Hash256("0x1ef7300d8961797263939a3d29bbba4ccf1702fabf02d8ad7a20b454edb6fd2f"))
.TestObject;
block.Header.IsPostMerge = true;
block.Header.Hash = block.CalculateHash();
using SemaphoreSlim bestBlockProcessed = new(0);
chain.BlockTree.NewHeadBlock += (s, e) =>
{
if (e.Block.Hash == block!.Hash)
bestBlockProcessed.Release(1);
};
await chain.BlockTree.SuggestBlockAsync(block!);
await bestBlockProcessed.WaitAsync(cancellationToken);
ExecutionPayload blockRequest = ExecutionPayload.Create(block);
ResultWrapper<PayloadStatusV1> executePayloadResult = await rpc.engine_newPayloadV1(blockRequest);
executePayloadResult.Data.Status.Should().Be(PayloadStatus.Valid);
}
[Test]
public async Task forkchoiceUpdatedV1_should_work_with_zero_keccak_for_finalization()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash!, Keccak.Zero, startingHead);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
forkchoiceUpdatedResult.Data.PayloadId.Should().Be(null);
Hash256 actualHead = chain.BlockTree.HeadHash;
actualHead.Should().NotBe(startingHead);
actualHead.Should().Be(newHeadHash);
AssertExecutionStatusChanged(chain.BlockFinder, newHeadHash!, Keccak.Zero, startingHead);
}
[Test]
public async Task forkchoiceUpdatedV1_should_update_finalized_block_hash()
{
using MergeTestBlockchain chain = await CreateBlockchain();
TestRpcBlockchain testRpc = await CreateTestRpc(chain);
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash!, startingHead, startingHead!);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
forkchoiceUpdatedResult.Data.PayloadId.Should().Be(null);
Hash256? actualFinalizedHash = chain.BlockTree.FinalizedHash;
actualFinalizedHash.Should().NotBeNull();
actualFinalizedHash.Should().Be(startingHead);
BlockForRpc blockForRpc = testRpc.EthRpcModule.eth_getBlockByNumber(BlockParameter.Finalized).Data;
blockForRpc.Should().NotBeNull();
actualFinalizedHash = blockForRpc.Hash;
actualFinalizedHash.Should().NotBeNull();
actualFinalizedHash.Should().Be(startingHead);
Assert.That(chain.BlockFinalizationManager.LastFinalizedHash, Is.EqualTo(actualFinalizedHash));
AssertExecutionStatusChanged(chain.BlockFinder, newHeadHash!, startingHead, startingHead);
}
[Test]
public async Task forkchoiceUpdatedV1_should_update_safe_block_hash()
{
using MergeTestBlockchain chain = await CreateBlockchain();
TestRpcBlockchain testRpc = await CreateTestRpc(chain);
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash!, startingHead, startingHead!);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
forkchoiceUpdatedResult.Data.PayloadId.Should().Be(null);
Hash256? actualSafeHash = chain.BlockTree.SafeHash;
actualSafeHash.Should().NotBeNull();
actualSafeHash.Should().Be(startingHead);
BlockForRpc blockForRpc = testRpc.EthRpcModule.eth_getBlockByNumber(BlockParameter.Safe).Data;
blockForRpc.Should().NotBeNull();
actualSafeHash = blockForRpc.Hash;
actualSafeHash.Should().NotBeNull();
actualSafeHash.Should().Be(startingHead);
AssertExecutionStatusChanged(chain.BlockFinder, newHeadHash!, startingHead, startingHead);
}
[TestCase(3, TestName = "2 blocks behind head — within PruningBoundary")]
[TestCase(33, TestName = "32 blocks behind head — within PruningBoundary (default 64)")]
public async Task forkchoiceUpdatedV1_WhenHeadIsCanonicalAncestorWithinPruningBoundary_ReorgsToAncestor(int chainLength)
{
// Spec PR #786: MUST reorg to a canonical ancestor when reorg depth <= PruningBoundary (default 64).
// No finalized block is set — the MAY-skip (spec point 2) never fires; the reorg proceeds
// because depth < PruningBoundary, not because of any old "32-block limit".
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, chainLength, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: false);
Hash256 b1Hash = branch[0].BlockHash;
Hash256 headHash = branch[chainLength - 1].BlockHash;
// Advance head to the last block without setting finalized
(await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(headHash, Keccak.Zero, Keccak.Zero))).Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.HeadHash.Should().Be(headHash, $"precondition: head is at H={chainLength}");
ForkchoiceStateV1 fcuToAncestor = new(b1Hash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuToAncestor);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.HeadHash.Should().Be(b1Hash, $"head must reorg to b1 — depth {chainLength - 1} is within PruningBoundary (default 64)");
}
[Test]
public async Task forkchoiceUpdatedV1_WhenHeadIsAncestorOfFinalizedBlock_SkipsUpdate()
{
// Spec PR #786 point 2: client MAY skip the update when headBlockHash is a valid ancestor of the
// latest known finalized block. Build 34 blocks, explicitly finalize b34, then FCU to b1
// (H=1 <= H=34, canonical) — the MAY-skip fires, not any depth limit.
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, 34, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: false);
Hash256 b1Hash = branch[0].BlockHash;
Hash256 b34Hash = branch[33].BlockHash;
// Set head to b34 and explicitly finalize it
(await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(b34Hash, b34Hash, b34Hash))).Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.HeadHash.Should().Be(b34Hash, "precondition: head is at H=34");
chain.BlockTree.FinalizedHash.Should().Be(b34Hash, "precondition: b34 is finalized");
ForkchoiceStateV1 fcuToAncestorOfFinalized = new(b1Hash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuToAncestorOfFinalized);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
result.Data.PayloadStatus.LatestValidHash.Should().Be(b1Hash, "spec mandates latestValidHash == forkchoiceState.headBlockHash when skipping");
result.Data.PayloadId.Should().BeNull("spec mandates payloadId: null when skipping");
chain.BlockTree.HeadHash.Should().Be(b34Hash, "Nethermind skips the update — b1 is a canonical ancestor of the finalized block, skip is permitted by spec");
}
[Test]
public async Task forkchoiceUpdatedV1_WhenHeadIsOnDifferentBranch_ReorgsRegardlessOfDepth()
{
// Spec PR #786: the MAY-skip (spec point 2) only fires when headBlockHash is a canonical ancestor
// of the finalized block. A non-canonical (fork) block is NOT eligible for the skip.
// FindMainChainAncestorNumber returns 0 (genesis) for side-chain blocks, giving a large reported
// depth, but here depth (34) < PruningBoundary (64) so -38006 doesn't fire either — the reorg
// always proceeds. Builds a canonical chain of 34 blocks, then a side block off genesis.
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
// Capture genesis as parent before building canonical chain
ExecutionPayload genesisAsParent = CreateParentBlockRequestOnHead(chain.BlockTree);
// Build canonical chain: genesis → b1 → b2 → ... → b34 (head at H=34)
IReadOnlyList<ExecutionPayload> canonical = await ProduceBranchV1(rpc, chain, 34, genesisAsParent, setHead: true);
Hash256 b34Hash = canonical[33].BlockHash;
chain.BlockTree.HeadHash.Should().Be(b34Hash, "precondition: canonical head is at H=34");
// Build a side block off genesis (H=1, different branch)
ExecutionPayload sideBlock = CreateBlockRequest(chain, genesisAsParent, TestItem.AddressA);
await rpc.engine_newPayloadV1(sideBlock);
Hash256 sideHash = sideBlock.BlockHash;
chain.BlockTree.IsMainChain(chain.BlockTree.FindHeader(sideHash, BlockTreeLookupOptions.None)!).Should().BeFalse("precondition: side block is not on canonical chain");
// FCU to the side block — it's on a different branch, so it must reorg regardless of depth
ForkchoiceStateV1 fcuToSide = new(sideHash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuToSide);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.HeadHash.Should().Be(sideHash, "different-branch FCU must always reorg — MAY-skip only applies to canonical ancestors of the finalized block");
}
[Test]
public async Task forkchoiceUpdatedV1_WhenReorgDepthExceedsPruningBoundary_ReturnsTooDeepReorg()
{
// Spec PR #786 point 6: MUST return -38006 when reorg depth > client-specific limit.
// Disable SnapServing so PruningTrieStateFactory.AdviseConfig doesn't bump the boundary
// to SnapServingMaxDepth=128 (TestBlockchain wiring auto-flips SnapServingEnabled on for
// HalfPath key schemes via WorldStateModule; setting it to false here keeps the auto-flip
// a no-op since `|=` only fires when the value is null).
// AdviseConfig also enforces a hard floor of 64 on PruningBoundary, so the smallest
// chain that exercises the -38006 path is 66 blocks: reorgDepth = 66 - 1 = 65 > 64 → -38006.
using MergeTestBlockchain chain = await CreateBlockchain(configurer: b => b
.Intercept<ISyncConfig>(cfg => cfg.SnapServingEnabled = false));
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, 66, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: false);
Hash256 b1Hash = branch[0].BlockHash;
Hash256 b66Hash = branch[65].BlockHash;
(await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(b66Hash, Keccak.Zero, Keccak.Zero))).Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.HeadHash.Should().Be(b66Hash, "precondition: head is at H=66");
ForkchoiceStateV1 fcuTooDeep = new(b1Hash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuTooDeep);
result.ErrorCode.Should().Be(MergeErrorCodes.TooDeepReorg, "reorg depth 65 exceeds PruningBoundary 64 — must return -38006");
chain.BlockTree.HeadHash.Should().Be(b66Hash, "head must not change when -38006 is returned");
}
[Test]
public async Task forkchoiceUpdatedV1_WhenZeroFinalizedAndSafeHash_ReturnsValidWithoutError()
{
// Spec: zero safeBlockHash and finalizedBlockHash mean "unknown" — must not return -38002.
// Models CL checkpoint-syncing from a non-finalized state where safe/finalized are unknown.
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, 3, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: true);
Hash256 headHash = branch[2].BlockHash;
ForkchoiceStateV1 fcuWithUnknownFinality = new(headHash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuWithUnknownFinality);
result.ErrorCode.Should().Be(0, "zero safe/finalized hashes must not produce an error");
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.HeadHash.Should().Be(headHash);
}
[Test]
public async Task forkchoiceUpdatedV1_WhenZeroFinalizedHash_PreservesKnownFinalizedHash()
{
// Spec PR #760: when finalizedBlockHash is zero, client MUST use the latest known finalized hash — not overwrite it with zero.
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, 2, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: false);
Hash256 b1Hash = branch[0].BlockHash;
Hash256 b2Hash = branch[1].BlockHash;
// First FCU: set head to b2 and finalize b1
ForkchoiceStateV1 fcuWithFinalized = new(b2Hash, b1Hash, b1Hash);
await rpc.engine_forkchoiceUpdatedV1(fcuWithFinalized);
chain.BlockTree.FinalizedHash.Should().Be(b1Hash, "precondition: b1 is finalized after first FCU");
// Second FCU: zero finalizedBlockHash — must preserve b1 as finalized
ForkchoiceStateV1 fcuWithZeroFinalized = new(b2Hash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuWithZeroFinalized);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockTree.FinalizedHash.Should().Be(b1Hash, "zero finalizedBlockHash must preserve the previously known finalized hash");
chain.BlockTree.SafeHash.Should().Be(b1Hash, "zero safeBlockHash must preserve the previously known safe hash");
}
[Test]
public async Task forkchoiceUpdatedV1_WhenZeroFinalizedHash_PreservesKnownFinalizedHash_WithPayloadAttributes()
{
// Spec PR #760: ResolveZeroHash is called in StartBuildingPayload too — verify preservation on that path.
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, 2, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: false);
Hash256 b1Hash = branch[0].BlockHash;
Hash256 b2Hash = branch[1].BlockHash;
// First FCU: set head to b2 and finalize b1
ForkchoiceStateV1 fcuWithFinalized = new(b2Hash, b1Hash, b1Hash);
await rpc.engine_forkchoiceUpdatedV1(fcuWithFinalized);
chain.BlockTree.FinalizedHash.Should().Be(b1Hash, "precondition: b1 is finalized after first FCU");
// Second FCU with payload attributes — exercises the StartBuildingPayload call site of ResolveZeroHash
PayloadAttributes payloadAttributes = new()
{
Timestamp = branch[1].Timestamp + 1,
PrevRandao = Keccak.Zero,
SuggestedFeeRecipient = Address.Zero
};
ForkchoiceStateV1 fcuWithZeroFinalized = new(b2Hash, Keccak.Zero, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuWithZeroFinalized, payloadAttributes);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
result.Data.PayloadId.Should().NotBeNull("payload build must be started when attributes are provided");
chain.BlockTree.FinalizedHash.Should().Be(b1Hash, "zero finalizedBlockHash must preserve the previously known finalized hash via StartBuildingPayload");
chain.BlockTree.SafeHash.Should().Be(b1Hash, "zero safeBlockHash must preserve the previously known safe hash via StartBuildingPayload");
}
[Test]
public async Task forkchoiceUpdatedV1_WhenNonZeroUnknownFinalizedHash_ReturnsInvalidForkchoiceState()
{
// Spec: -38002 must only fire for non-zero hashes that are unknown, not for zero hashes.
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
IReadOnlyList<ExecutionPayload> branch = await ProduceBranchV1(rpc, chain, 1, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: true);
Hash256 headHash = branch[0].BlockHash;
ForkchoiceStateV1 fcuWithUnknownFinalized = new(headHash, TestItem.KeccakA, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(fcuWithUnknownFinalized);
result.ErrorCode.Should().Be(MergeErrorCodes.InvalidForkchoiceState,
"non-zero unknown finalizedBlockHash must return -38002");
}
[Test]
public async Task forkchoiceUpdatedV1_should_work_with_zero_keccak_as_safe_block()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash!;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash, newHeadHash, Keccak.Zero);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
forkchoiceUpdatedResult.Data.PayloadId.Should().Be(null);
Hash256 actualHead = chain.BlockTree.HeadHash;
actualHead.Should().NotBe(startingHead);
actualHead.Should().Be(newHeadHash);
AssertExecutionStatusChanged(chain.BlockFinder, newHeadHash!, newHeadHash, Keccak.Zero);
}
[Test]
public async Task forkchoiceUpdatedV1_with_no_payload_attributes_should_change_head()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash!;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash, startingHead, startingHead);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
forkchoiceUpdatedResult.Data.PayloadId.Should().Be(null);
Hash256 actualHead = chain.BlockTree.HeadHash;
actualHead.Should().NotBe(startingHead);
actualHead.Should().Be(newHeadHash);
AssertExecutionStatusChanged(chain.BlockFinder, newHeadHash, startingHead, startingHead);
}
[Test]
public async Task forkChoiceUpdatedV1_to_unknown_block_fails()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ForkchoiceStateV1 forkchoiceStateV1 = new(TestItem.KeccakF, TestItem.KeccakF, TestItem.KeccakF);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(nameof(PayloadStatus.Syncing).ToUpper()); // ToDo wait for final PostMerge sync
AssertExecutionStatusNotChanged(chain.BlockFinder, TestItem.KeccakF, TestItem.KeccakF, TestItem.KeccakF);
}
[Test]
public async Task forkChoiceUpdatedV1_to_unknown_safeBlock_hash_should_fail()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash!;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash, startingHead, TestItem.KeccakF);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1, null);
forkchoiceUpdatedResult.ErrorCode.Should().Be(MergeErrorCodes.InvalidForkchoiceState);
Hash256 actualHead = chain.BlockTree.HeadHash;
actualHead.Should().NotBe(newHeadHash);
}
[Test]
public async Task forkChoiceUpdatedV1_no_common_branch_fails()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256? startingHead = chain.BlockTree.HeadHash;
Block parent = Build.A.Block.WithNumber(2).WithParentHash(TestItem.KeccakA).WithNonce(0).WithDifficulty(0).TestObject;
Block block = Build.A.Block.WithNumber(3).WithParent(parent).WithNonce(0).WithDifficulty(0).TestObject;
await rpc.engine_newPayloadV1(ExecutionPayload.Create(parent));
ForkchoiceStateV1 forkchoiceStateV1 = new(parent.Hash!, startingHead, startingHead);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be("SYNCING");
await rpc.engine_newPayloadV1(ExecutionPayload.Create(block));
ForkchoiceStateV1 forkchoiceStateV11 = new(parent.Hash!, startingHead, startingHead);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult_1 = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV11);
forkchoiceUpdatedResult_1.Data.PayloadStatus.Status.Should().Be("SYNCING");
AssertExecutionStatusNotChanged(chain.BlockFinder, block.Hash!, startingHead, startingHead);
}
[Test, NonParallelizable]
public async Task forkChoiceUpdatedV1_block_still_processing()
{
using MergeTestBlockchain chain = await CreateBlockchain(mergeConfig: new MergeConfig()
{
NewPayloadBlockProcessingTimeout = 100
});
IEngineRpcModule rpc = chain.EngineRpcModule;
Hash256 startingHead = chain.BlockTree.HeadHash;
Block blockTreeHead = chain.BlockTree.Head!;
Block block = Build.A.Block.WithNumber(blockTreeHead.Number + 1).WithParent(blockTreeHead).WithNonce(0).WithDifficulty(0).TestObject;
chain.ThrottleBlockProcessor(1000);
ManualResetEventSlim processingStarted = new(false);
((TestBranchProcessorInterceptor)chain.BranchProcessor).ProcessingStarted = processingStarted;
// Directly enqueue a block to occupy the processor (bypasses the RPC semaphore),
// ensuring subsequent blocks route through the recovery queue (slow path)
Block occupyBlock = Build.A.Block.WithNumber(blockTreeHead.Number + 1).WithParent(blockTreeHead)
.WithNonce(0).WithDifficulty(0).WithStateRoot(blockTreeHead.StateRoot!).TestObject;
occupyBlock.Header.TotalDifficulty = blockTreeHead.TotalDifficulty;
_ = Task.Run(async () => await chain.BlockProcessingQueue.Enqueue(
occupyBlock, ProcessingOptions.ForceProcessing | ProcessingOptions.DoNotUpdateHead));
processingStarted.Wait(TimeSpan.FromSeconds(5));
ResultWrapper<PayloadStatusV1> newPayloadV1 =
await rpc.engine_newPayloadV1(ExecutionPayload.Create(block));
newPayloadV1.Data.Status.Should().Be("SYNCING");
ForkchoiceStateV1 forkchoiceStateV1 = new(block.Hash!, startingHead, startingHead);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult =
await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be("SYNCING");
AssertExecutionStatusNotChanged(chain.BlockFinder, block.Hash!, startingHead, startingHead);
}
[Test, NonParallelizable]
public async Task AlreadyKnown_not_cached_block_should_return_valid()
{
using MergeTestBlockchain? chain = await CreateBlockchain(mergeConfig: new MergeConfig()
{
NewPayloadBlockProcessingTimeout = 100
});
IEngineRpcModule? rpc = chain.EngineRpcModule;
Block? head = chain.BlockTree.Head!;
Block? b4 = Build.A.Block
.WithNumber(head.Number + 1)
.WithParent(head)
.WithNonce(0)
.WithDifficulty(0)
.WithStateRoot(head.StateRoot!)
.WithBeneficiary(Build.An.Address.TestObject)
.TestObject;
(await rpc.engine_newPayloadV1(ExecutionPayload.Create(b4))).Data.Status.Should().Be(PayloadStatus.Valid);
Block? b5 = Build.A.Block
.WithNumber(b4.Number + 1)
.WithParent(b4)
.WithNonce(0)
.WithDifficulty(0)
.WithStateRoot(b4.StateRoot!)
.TestObject;
(await rpc.engine_newPayloadV1(ExecutionPayload.Create(b5))).Data.Status.Should().Be(PayloadStatus.Valid);
(await rpc.engine_newPayloadV1(ExecutionPayload.Create(b5))).Data.Status.Should().Be(PayloadStatus.Valid);
}
[Test, NonParallelizable]
public async Task Invalid_block_on_processing_wont_be_accepted_if_sent_twice_in_a_row_when_block_processing_queue_is_not_empty()
{
using MergeTestBlockchain? chain = await CreateBlockchain(mergeConfig: new MergeConfig()
{
NewPayloadBlockProcessingTimeout = 100
});
IEngineRpcModule? rpc = chain.EngineRpcModule;
Block? head = chain.BlockTree.Head!;
// make sure AddressA has enough balance to send tx
chain.ReadOnlyState.GetBalance(TestItem.AddressA).Should().BeGreaterThan(UInt256.One);
// block is an invalid block, but it is impossible to detect until we process it.
// it is invalid because after you process its transactions, the root of the state trie
// doesn't match the state root in the block
Block? block = Build.A.Block
.WithNumber(head.Number + 1)
.WithParent(head)
.WithNonce(0)
.WithDifficulty(0)
.WithTransactions(
Build.A.Transaction
.WithTo(TestItem.AddressD)
.WithValue(100.GWei)
.SignedAndResolved(TestItem.PrivateKeyA)
.TestObject
)
.WithGasUsed(21000)
.WithStateRoot(head.StateRoot!) // after processing transaction, this state root is wrong
.TestObject;
chain.ThrottleBlockProcessor(1000); // throttle the block processor enough so that the block processing queue is never empty
ManualResetEventSlim processingStarted = new(false);
((TestBranchProcessorInterceptor)chain.BranchProcessor).ProcessingStarted = processingStarted;
// Directly enqueue a block to occupy the processor (bypasses the RPC semaphore),
// ensuring subsequent blocks route through the recovery queue (slow path)
Block occupyBlock = Build.A.Block.WithNumber(head.Number + 1).WithParent(head)
.WithNonce(0).WithDifficulty(0).WithStateRoot(head.StateRoot!).TestObject;
occupyBlock.Header.TotalDifficulty = head.TotalDifficulty;
_ = Task.Run(async () => await chain.BlockProcessingQueue.Enqueue(
occupyBlock, ProcessingOptions.ForceProcessing | ProcessingOptions.DoNotUpdateHead));
processingStarted.Wait(TimeSpan.FromSeconds(5));
(await rpc.engine_newPayloadV1(ExecutionPayload.Create(block))).Data.Status.Should().Be(PayloadStatus.Syncing);
(await rpc.engine_newPayloadV1(ExecutionPayload.Create(block))).Data.Status.Should().BeOneOf(PayloadStatus.Syncing);
}
[Test]
public async Task forkchoiceUpdatedV1_should_change_head_when_all_parameters_are_the_newHeadHash()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpc = chain.EngineRpcModule;
ExecutionPayload executionPayload = await SendNewBlockV1(rpc, chain);
Hash256 newHeadHash = executionPayload.BlockHash;
ForkchoiceStateV1 forkchoiceStateV1 = new(newHeadHash, newHeadHash, newHeadHash);
ResultWrapper<ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult =
await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1, null);
forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
forkchoiceUpdatedResult.Data.PayloadId.Should().Be(null);