-
Notifications
You must be signed in to change notification settings - Fork 804
/
Copy pathengine.ts
1292 lines (1177 loc) · 45.8 KB
/
engine.ts
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
import { Block } from '@ethereumjs/block'
import { Hardfork } from '@ethereumjs/common'
import { BlobEIP4844Transaction } from '@ethereumjs/tx'
import {
bigIntToHex,
bytesToHex,
bytesToUnprefixedHex,
equalsBytes,
hexToBytes,
toBytes,
zeros,
} from '@ethereumjs/util'
import { PendingBlock } from '../../miner'
import { short } from '../../util'
import { INTERNAL_ERROR, INVALID_PARAMS, TOO_LARGE_REQUEST } from '../error-code'
import { CLConnectionManager, middleware as cmMiddleware } from '../util/CLConnectionManager'
import { middleware, validators } from '../validation'
import type { Chain } from '../../blockchain'
import type { EthereumClient } from '../../client'
import type { Config } from '../../config'
import type { VMExecution } from '../../execution'
import type { BlobsBundle } from '../../miner'
import type { FullEthereumService } from '../../service'
import type { ExecutionPayload } from '@ethereumjs/block'
import type { VM } from '@ethereumjs/vm'
const zeroBlockHash = zeros(32)
export enum Status {
ACCEPTED = 'ACCEPTED',
INVALID = 'INVALID',
INVALID_BLOCK_HASH = 'INVALID_BLOCK_HASH',
SYNCING = 'SYNCING',
VALID = 'VALID',
}
type Bytes8 = string
type Bytes20 = string
type Bytes32 = string
// type Root = Bytes32
type Blob = Bytes32
type Bytes48 = string
type Uint64 = string
type Uint256 = string
type WithdrawalV1 = Exclude<ExecutionPayload['withdrawals'], undefined>[number]
// ExecutionPayload has higher version fields as optionals to make it easy for typescript
export type ExecutionPayloadV1 = ExecutionPayload
export type ExecutionPayloadV2 = ExecutionPayloadV1 & { withdrawals: WithdrawalV1[] }
// parentBeaconBlockRoot comes separate in new payloads and needs to be added to payload data
export type ExecutionPayloadV3 = ExecutionPayloadV2 & { excessDataGas: Uint64; dataGasUsed: Uint64 }
export type ForkchoiceStateV1 = {
headBlockHash: Bytes32
safeBlockHash: Bytes32
finalizedBlockHash: Bytes32
}
// PayloadAttributes has higher version fields as optionals to make it easy for typescript
type PayloadAttributes = {
timestamp: Uint64
prevRandao: Bytes32
suggestedFeeRecipient: Bytes20
// add higher version fields as optionals to make it easy for typescript
withdrawals?: WithdrawalV1[]
parentBeaconBlockRoot?: Bytes32
}
type PayloadAttributesV1 = Omit<PayloadAttributes, 'withdrawals' | 'parentBeaconBlockRoot'>
type PayloadAttributesV2 = PayloadAttributesV1 & { withdrawals: WithdrawalV1[] }
type PayloadAttributesV3 = PayloadAttributesV2 & { parentBeaconBlockRoot: Bytes32 }
export type PayloadStatusV1 = {
status: Status
latestValidHash: Bytes32 | null
validationError: string | null
}
export type ForkchoiceResponseV1 = {
payloadStatus: PayloadStatusV1
payloadId: Bytes8 | null
}
type TransitionConfigurationV1 = {
terminalTotalDifficulty: Uint256
terminalBlockHash: Bytes32
terminalBlockNumber: Uint64
}
type BlobsBundleV1 = {
commitments: Bytes48[]
blobs: Blob[]
proofs: Bytes48[]
}
type ExecutionPayloadBodyV1 = {
transactions: string[]
withdrawals: WithdrawalV1[] | null
}
const EngineError = {
UnknownPayload: {
code: -32001,
message: 'Unknown payload',
},
}
const executionPayloadV1FieldValidators = {
parentHash: validators.blockHash,
feeRecipient: validators.address,
stateRoot: validators.bytes32,
receiptsRoot: validators.bytes32,
logsBloom: validators.bytes256,
prevRandao: validators.bytes32,
blockNumber: validators.uint64,
gasLimit: validators.uint64,
gasUsed: validators.uint64,
timestamp: validators.uint64,
extraData: validators.variableBytes32,
baseFeePerGas: validators.uint256,
blockHash: validators.blockHash,
transactions: validators.array(validators.hex),
}
const executionPayloadV2FieldValidators = {
...executionPayloadV1FieldValidators,
withdrawals: validators.array(validators.withdrawal()),
}
const executionPayloadV3FieldValidators = {
...executionPayloadV2FieldValidators,
dataGasUsed: validators.uint64,
excessDataGas: validators.uint64,
}
const forkchoiceFieldValidators = {
headBlockHash: validators.blockHash,
safeBlockHash: validators.blockHash,
finalizedBlockHash: validators.blockHash,
}
const payloadAttributesFieldValidatorsV1 = {
timestamp: validators.uint64,
prevRandao: validators.bytes32,
suggestedFeeRecipient: validators.address,
}
const payloadAttributesFieldValidatorsV2 = {
...payloadAttributesFieldValidatorsV1,
// withdrawals is optional in V2 because its backward forward compatible with V1
withdrawals: validators.optional(validators.array(validators.withdrawal())),
}
const payloadAttributesFieldValidatorsV3 = {
...payloadAttributesFieldValidatorsV1,
withdrawals: validators.array(validators.withdrawal()),
parentBeaconBlockRoot: validators.bytes32,
}
/**
* Formats a block to {@link ExecutionPayloadV1}.
*/
export const blockToExecutionPayload = (block: Block, value: bigint, bundle?: BlobsBundle) => {
const blockJson = block.toJSON()
const header = blockJson.header!
const transactions = block.transactions.map((tx) => bytesToHex(tx.serialize())) ?? []
const withdrawalsArr = blockJson.withdrawals ? { withdrawals: blockJson.withdrawals } : {}
const blobsBundle: BlobsBundleV1 | undefined = bundle
? {
commitments: bundle.commitments.map(bytesToHex),
blobs: bundle.blobs.map(bytesToHex),
proofs: bundle.proofs.map(bytesToHex),
}
: undefined
const executionPayload: ExecutionPayload = {
blockNumber: header.number!,
parentHash: header.parentHash!,
feeRecipient: header.coinbase!,
stateRoot: header.stateRoot!,
receiptsRoot: header.receiptTrie!,
logsBloom: header.logsBloom!,
gasLimit: header.gasLimit!,
gasUsed: header.gasUsed!,
timestamp: header.timestamp!,
extraData: header.extraData!,
baseFeePerGas: header.baseFeePerGas!,
dataGasUsed: header.dataGasUsed,
excessDataGas: header.excessDataGas,
blockHash: bytesToHex(block.hash()),
prevRandao: header.mixHash!,
transactions,
...withdrawalsArr,
}
// ethereumjs doesnot provide any transaction censoring detection (yet) to suggest
// overriding builder/mev-boost blocks
const shouldOverrideBuilder = false
return { executionPayload, blockValue: bigIntToHex(value), blobsBundle, shouldOverrideBuilder }
}
const pruneCachedBlocks = (
chain: Chain,
remoteBlocks: Map<String, Block>,
executedBlocks: Map<String, Block>
) => {
const finalized = chain.blocks.finalized
if (finalized !== null) {
// prune remoteBlocks
const pruneRemoteBlocksTill = finalized.header.number
for (const blockHash of remoteBlocks.keys()) {
const block = remoteBlocks.get(blockHash)
if (block !== undefined && block.header.number <= pruneRemoteBlocksTill) {
remoteBlocks.delete(blockHash)
}
}
// prune executedBlocks
const vm = chain.blocks.vm
if (vm !== null) {
const pruneExecutedBlocksTill =
vm.header.number < finalized.header.number ? vm.header.number : finalized.header.number
for (const blockHash of executedBlocks.keys()) {
const block = executedBlocks.get(blockHash)
if (block !== undefined && block.header.number <= pruneExecutedBlocksTill) {
executedBlocks.delete(blockHash)
}
}
}
}
}
/**
* Recursively finds parent blocks starting from the parentHash.
*/
const recursivelyFindParents = async (
vmHeadHash: Uint8Array,
parentHash: Uint8Array,
chain: Chain,
maxDepth: number
) => {
if (equalsBytes(parentHash, vmHeadHash) || equalsBytes(parentHash, new Uint8Array(32))) {
return []
}
const parentBlocks = []
const block = await chain.getBlock(parentHash)
parentBlocks.push(block)
while (!equalsBytes(parentBlocks[parentBlocks.length - 1].hash(), vmHeadHash)) {
const block: Block = await chain.getBlock(
parentBlocks[parentBlocks.length - 1].header.parentHash
)
parentBlocks.push(block)
// throw error if lookups have exceeded maxDepth
if (parentBlocks.length > maxDepth) {
throw Error(`recursivelyFindParents lookups deeper than maxDepth=${maxDepth}`)
}
}
return parentBlocks.reverse()
}
/**
* Returns the block hash as a 0x-prefixed hex string if found valid in the blockchain, otherwise returns null.
*/
const validHash = async (hash: Uint8Array, chain: Chain): Promise<string | null> => {
try {
await chain.getBlock(hash)
} catch (error: any) {
return null
}
return bytesToHex(hash)
}
/**
* Returns the block hash as a 0x-prefixed hex string if found valid in the blockchain, otherwise returns null.
*/
const validExecutedChainBlock = async (
blockOrHash: Uint8Array | Block,
chain: Chain
): Promise<Block | null> => {
try {
const block = blockOrHash instanceof Block ? blockOrHash : await chain.getBlock(blockOrHash)
const vmHead = await chain.blockchain.getIteratorHead()
if (vmHead.header.number >= block.header.number) {
// check if block is canonical
const canonicalHash = await chain.blockchain.safeNumberToHash(block.header.number)
if (canonicalHash instanceof Uint8Array && equalsBytes(block.hash(), canonicalHash)) {
return block
}
}
// if the block was canonical and executed we would have returned by now
return null
} catch (error: any) {
return null
}
}
/**
* Validates that the block satisfies post-merge conditions.
*/
const validateTerminalBlock = async (block: Block, chain: Chain): Promise<boolean> => {
const ttd = chain.config.chainCommon.hardforkTTD(Hardfork.Paris)
if (ttd === null) return false
const blockTd = await chain.getTd(block.hash(), block.header.number)
// Block is terminal if its td >= ttd and its parent td < ttd.
// In case the Genesis block has td >= ttd it is the terminal block
if (block.isGenesis()) return blockTd >= ttd
const parentBlockTd = await chain.getTd(block.header.parentHash, block.header.number - BigInt(1))
return blockTd >= ttd && parentBlockTd < ttd
}
/**
* Returns a block from a payload.
* If errors, returns {@link PayloadStatusV1}
*/
const assembleBlock = async (
payload: ExecutionPayload,
chain: Chain
): Promise<{ block?: Block; error?: PayloadStatusV1 }> => {
const { blockNumber, timestamp } = payload
const { config } = chain
const common = config.chainCommon.copy()
// This is a post merge block, so set its common accordingly
// Can't use setHardfork flag, as the transactions will need to be deserialized
// first before the header can be constucted with their roots
const ttd = common.hardforkTTD(Hardfork.Paris)
common.setHardforkBy({ blockNumber, td: ttd !== null ? ttd : undefined, timestamp })
try {
const block = await Block.fromExecutionPayload(payload, { common })
// TODO: validateData is also called in applyBlock while runBlock, may be it can be optimized
// by removing/skipping block data validation from there
await block.validateData()
return { block }
} catch (error) {
const validationError = `Error assembling block during from payload: ${error}`
config.logger.error(validationError)
const latestValidHash = await validHash(hexToBytes(payload.parentHash), chain)
const response = {
status: `${error}`.includes('Invalid blockHash') ? Status.INVALID_BLOCK_HASH : Status.INVALID,
latestValidHash,
validationError,
}
return { error: response }
}
}
const getPayloadBody = (block: Block): ExecutionPayloadBodyV1 => {
const transactions = block.transactions.map((tx) => bytesToHex(tx.serialize()))
const withdrawals = block.withdrawals?.map((wt) => wt.toJSON()) ?? null
return {
transactions,
withdrawals,
}
}
/**
* engine_* RPC module
* @memberof module:rpc/modules
*/
export class Engine {
private client: EthereumClient
private execution: VMExecution
private service: FullEthereumService
private chain: Chain
private config: Config
private vm: VM
private pendingBlock: PendingBlock
private remoteBlocks: Map<String, Block>
private executedBlocks: Map<String, Block>
private connectionManager: CLConnectionManager
private lastNewPayloadHF: string = ''
private lastForkchoiceUpdatedHF: string = ''
/**
* Create engine_* RPC module
* @param client Client to which the module binds
*/
constructor(client: EthereumClient) {
this.client = client
this.service = client.services.find((s) => s.name === 'eth') as FullEthereumService
this.chain = this.service.chain
this.config = this.chain.config
if (this.service.execution === undefined) {
throw Error('execution required for engine module')
}
this.execution = this.service.execution
this.vm = this.execution.vm
this.connectionManager = new CLConnectionManager({ config: this.chain.config })
this.pendingBlock = new PendingBlock({ config: this.config, txPool: this.service.txPool })
this.remoteBlocks = new Map()
this.executedBlocks = new Map()
this.newPayloadV1 = cmMiddleware(
middleware(this.newPayloadV1.bind(this), 1, [
[validators.object(executionPayloadV1FieldValidators)],
]),
([payload], response) => this.connectionManager.lastNewPayload({ payload, response })
)
this.newPayloadV2 = cmMiddleware(
middleware(this.newPayloadV2.bind(this), 1, [
[
validators.either(
validators.object(executionPayloadV1FieldValidators),
validators.object(executionPayloadV2FieldValidators)
),
],
]),
([payload], response) => this.connectionManager.lastNewPayload({ payload, response })
)
this.newPayloadV3 = cmMiddleware(
middleware(
this.newPayloadV3.bind(this),
3,
[
[validators.object(executionPayloadV3FieldValidators)],
[validators.array(validators.bytes32)],
[validators.bytes32],
],
['executionPayload', 'versionedHashes', 'parentBeaconBlockRoot']
),
([payload], response) => this.connectionManager.lastNewPayload({ payload, response })
)
const forkchoiceUpdatedResponseCMHandler = (
[state]: ForkchoiceStateV1[],
response?: ForkchoiceResponseV1 & { headBlock?: Block },
error?: string
) => {
this.connectionManager.lastForkchoiceUpdate({
state,
response,
headBlock: response?.headBlock,
error,
})
// Remove the headBlock from the response object as headBlock is bundled only for connectionManager
delete response?.headBlock
}
this.forkchoiceUpdatedV1 = cmMiddleware(
middleware(this.forkchoiceUpdatedV1.bind(this), 1, [
[validators.object(forkchoiceFieldValidators)],
[validators.optional(validators.object(payloadAttributesFieldValidatorsV1))],
]),
forkchoiceUpdatedResponseCMHandler
)
this.forkchoiceUpdatedV2 = cmMiddleware(
middleware(this.forkchoiceUpdatedV2.bind(this), 1, [
[validators.object(forkchoiceFieldValidators)],
[validators.optional(validators.object(payloadAttributesFieldValidatorsV2))],
]),
forkchoiceUpdatedResponseCMHandler
)
this.forkchoiceUpdatedV3 = cmMiddleware(
middleware(this.forkchoiceUpdatedV3.bind(this), 1, [
[validators.object(forkchoiceFieldValidators)],
[validators.optional(validators.object(payloadAttributesFieldValidatorsV3))],
]),
forkchoiceUpdatedResponseCMHandler
)
this.getPayloadV1 = cmMiddleware(
middleware(this.getPayloadV1.bind(this), 1, [[validators.bytes8]]),
() => this.connectionManager.updateStatus()
)
this.getPayloadV2 = cmMiddleware(
middleware(this.getPayloadV2.bind(this), 1, [[validators.bytes8]]),
() => this.connectionManager.updateStatus()
)
this.getPayloadV3 = cmMiddleware(
middleware(this.getPayloadV3.bind(this), 1, [[validators.bytes8]]),
() => this.connectionManager.updateStatus()
)
this.exchangeTransitionConfigurationV1 = cmMiddleware(
middleware(this.exchangeTransitionConfigurationV1.bind(this), 1, [
[
validators.object({
terminalTotalDifficulty: validators.uint256,
terminalBlockHash: validators.bytes32,
terminalBlockNumber: validators.uint64,
}),
],
]),
() => this.connectionManager.updateStatus()
)
this.exchangeCapabilities = cmMiddleware(
middleware(this.exchangeCapabilities.bind(this), 0, []),
() => this.connectionManager.updateStatus()
)
this.getPayloadBodiesByHashV1 = cmMiddleware(
middleware(this.getPayloadBodiesByHashV1.bind(this), 1, [
[validators.array(validators.bytes32)],
]),
() => this.connectionManager.updateStatus()
)
this.getPayloadBodiesByRangeV1 = cmMiddleware(
middleware(this.getPayloadBodiesByRangeV1.bind(this), 2, [
[validators.bytes8],
[validators.bytes8],
]),
() => this.connectionManager.updateStatus()
)
}
/**
* Verifies the payload according to the execution environment
* rule set (EIP-3675) and returns the status of the verification.
*
* @param params An array of one parameter:
* 1. An object as an instance of {@link ExecutionPayloadV1}
* @returns An object of shape {@link PayloadStatusV1}:
* 1. status: String - the result of the payload execution
* VALID - given payload is valid
* INVALID - given payload is invalid
* SYNCING - sync process is in progress
* ACCEPTED - blockHash is valid, doesn't extend the canonical chain, hasn't been fully validated
* INVALID_BLOCK_HASH - blockHash validation failed
* 2. latestValidHash: DATA|null - the hash of the most recent
* valid block in the branch defined by payload and its ancestors
* 3. validationError: String|null - validation error message
*/
private async newPayload(
params: [ExecutionPayload, (Bytes32[] | null)?, (Bytes32 | null)?]
): Promise<PayloadStatusV1> {
const [payload, versionedHashes, parentBeaconBlockRoot] = params
if (this.config.synchronized) {
this.connectionManager.newPayloadLog()
}
const { parentHash, blockHash } = payload
// newpayloadv3 comes with parentBeaconBlockRoot out of the payload
const { block, error } = await assembleBlock(
{
...payload,
// ExecutionPayload only handles undefined
parentBeaconBlockRoot: parentBeaconBlockRoot ?? undefined,
},
this.chain
)
if (!block || error) {
let response = error
if (!response) {
const validationError = `Error assembling block during init`
this.config.logger.debug(validationError)
const latestValidHash = await validHash(hexToBytes(payload.parentHash), this.chain)
response = { status: Status.INVALID, latestValidHash, validationError }
}
return response
}
if (block.common.isActivatedEIP(4844)) {
let validationError: string | null = null
if (versionedHashes === undefined || versionedHashes === null) {
validationError = `Error verifying versionedHashes: received none`
} else {
// Collect versioned hashes in the flat array `txVersionedHashes` to match with received
const txVersionedHashes = []
for (const tx of block.transactions) {
if (tx instanceof BlobEIP4844Transaction) {
for (const vHash of tx.versionedHashes) {
txVersionedHashes.push(vHash)
}
}
}
if (versionedHashes.length !== txVersionedHashes.length) {
validationError = `Error verifying versionedHashes: expected=${txVersionedHashes.length} received=${versionedHashes.length}`
} else {
// match individual hashes
for (let vIndex = 0; vIndex < versionedHashes.length; vIndex++) {
// if mismatch, record error and break
if (!equalsBytes(hexToBytes(versionedHashes[vIndex]), txVersionedHashes[vIndex])) {
validationError = `Error verifying versionedHashes: mismatch at index=${vIndex} expected=${short(
txVersionedHashes[vIndex]
)} received=${short(versionedHashes[vIndex])}`
break
}
}
}
}
// if there was a validation error return invalid
if (validationError !== null) {
this.config.logger.debug(validationError)
const latestValidHash = await validHash(hexToBytes(parentHash), this.chain)
const response = { status: Status.INVALID, latestValidHash, validationError }
return response
}
} else if (versionedHashes !== undefined && versionedHashes !== null) {
const validationError = `Invalid versionedHashes before EIP-4844 is activated`
const latestValidHash = await validHash(hexToBytes(parentHash), this.chain)
const response = { status: Status.INVALID, latestValidHash, validationError }
return response
}
this.connectionManager.updatePayloadStats(block)
const hardfork = block.common.hardfork()
if (hardfork !== this.lastNewPayloadHF && this.lastNewPayloadHF !== '') {
this.config.logger.info(
`Hardfork change along new payload block number=${block.header.number} hash=${short(
block.hash()
)} old=${this.lastNewPayloadHF} new=${hardfork}`
)
}
this.lastNewPayloadHF = hardfork
// This optimistic lookup keeps skeleton updated even if for e.g. beacon sync might not have
// been initialized here but a batch of blocks new payloads arrive, most likely during sync
// We still can't switch to beacon sync here especially if the chain is pre merge and there
// is pow block which this client would like to mint and attempt proposing it
const optimisticLookup = await this.service.beaconSync?.extendChain(block)
// we should check if the block exits executed in remoteBlocks or in chain as a check that stateroot
// exists in statemanager is not sufficient because an invalid crafted block with valid block hash with
// some pre-executed stateroot can be send
const executedBlockExists =
this.executedBlocks.get(blockHash.slice(2)) ??
(await validExecutedChainBlock(hexToBytes(blockHash), this.chain))
if (executedBlockExists) {
const response = {
status: Status.VALID,
latestValidHash: blockHash,
validationError: null,
}
return response
}
try {
// get the parent from beacon skeleton or from remoteBlocks cache or from the chain
// to run basic validations based on parent
const parent =
(await this.service.beaconSync?.skeleton.getBlockByHash(hexToBytes(parentHash), true)) ??
this.remoteBlocks.get(parentHash.slice(2)) ??
(await this.chain.getBlock(hexToBytes(parentHash)))
// Validations with parent
if (!parent.common.gteHardfork(Hardfork.Paris)) {
const validTerminalBlock = await validateTerminalBlock(parent, this.chain)
if (!validTerminalBlock) {
const response = {
status: Status.INVALID,
validationError: null,
latestValidHash: bytesToHex(zeros(32)),
}
return response
}
}
// validate 4844 transactions and fields as these validations generally happen on putBlocks
// when parent is confirmed to be in the chain. But we can do it here early
if (block.common.isActivatedEIP(4844)) {
try {
block.validateBlobTransactions(parent.header)
} catch (error: any) {
const validationError = `Invalid 4844 transactions: ${error}`
const latestValidHash = await validHash(hexToBytes(parentHash), this.chain)
const response = { status: Status.INVALID, latestValidHash, validationError }
return response
}
}
const executedParentExists =
this.executedBlocks.get(parentHash.slice(2)) ??
(await validExecutedChainBlock(hexToBytes(parentHash), this.chain))
// If the parent is not executed throw an error, it will be caught and return SYNCING or ACCEPTED.
if (!executedParentExists) {
throw new Error(`Parent block not yet executed number=${parent.header.number}`)
}
} catch (error: any) {
const status =
// If the transitioned to beacon sync and this block can extend beacon chain then
optimisticLookup === true ? Status.SYNCING : Status.ACCEPTED
if (status === Status.ACCEPTED) {
// Stash the block for a potential forced forkchoice update to it later.
this.remoteBlocks.set(bytesToUnprefixedHex(block.hash()), block)
}
const response = { status, validationError: null, latestValidHash: null }
return response
}
const vmHead = await this.chain.blockchain.getIteratorHead()
let blocks: Block[]
try {
// find parents till vmHead but limit lookups till engineParentLookupMaxDepth
blocks = await recursivelyFindParents(
vmHead.hash(),
block.header.parentHash,
this.chain,
this.chain.config.engineParentLookupMaxDepth
)
} catch (error) {
const response = { status: Status.SYNCING, latestValidHash: null, validationError: null }
return response
}
blocks.push(block)
let lastBlock: Block
try {
for (const [i, block] of blocks.entries()) {
lastBlock = block
const bHash = block.hash()
const isBlockExecuted =
(this.executedBlocks.get(bytesToUnprefixedHex(bHash)) ??
(await validExecutedChainBlock(bHash, this.chain))) !== null
if (!isBlockExecuted) {
// Only execute if number of blocks pending to be executed are within limit
// else return SYNCING/ACCEPTED and let skeleton led chain execution catch up
const executed =
blocks.length - i <= this.chain.config.engineNewpayloadMaxExecute
? await this.execution.runWithoutSetHead({
block,
root: (i > 0 ? blocks[i - 1] : await this.chain.getBlock(block.header.parentHash))
.header.stateRoot,
setHardfork: this.chain.headers.td,
})
: false
if (!executed) {
// determind status to be returned depending on if block could extend chain or not
const status = optimisticLookup === true ? Status.SYNCING : Status.ACCEPTED
const response = { status, latestValidHash: null, validationError: null }
return response
} else {
this.executedBlocks.set(bytesToUnprefixedHex(block.hash()), block)
}
}
}
} catch (error) {
const validationError = `Error verifying block while running: ${error}`
this.config.logger.error(validationError)
const latestValidHash = await validHash(block.header.parentHash, this.chain)
const response = { status: Status.INVALID, latestValidHash, validationError }
try {
await this.chain.blockchain.delBlock(lastBlock!.hash())
// eslint-disable-next-line no-empty
} catch {}
try {
await this.service.beaconSync?.skeleton.deleteBlock(lastBlock!)
// eslint-disable-next-line no-empty
} catch {}
return response
}
this.remoteBlocks.set(bytesToUnprefixedHex(block.hash()), block)
const response = {
status: Status.VALID,
latestValidHash: bytesToHex(block.hash()),
validationError: null,
}
return response
}
async newPayloadV1(params: [ExecutionPayloadV1]): Promise<PayloadStatusV1> {
const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai)
const ts = parseInt(params[0].timestamp)
if (shanghaiTimestamp !== null && ts >= shanghaiTimestamp) {
throw {
code: INVALID_PARAMS,
message: 'NewPayloadV2 MUST be used after Shanghai is activated',
}
}
return this.newPayload(params)
}
async newPayloadV2(params: [ExecutionPayloadV2 | ExecutionPayloadV1]): Promise<PayloadStatusV1> {
const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai)
const eip4844Timestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Cancun)
const ts = parseInt(params[0].timestamp)
const withdrawals = (params[0] as ExecutionPayloadV2).withdrawals
if (eip4844Timestamp !== null && ts >= eip4844Timestamp) {
throw {
code: INVALID_PARAMS,
message: 'NewPayloadV3 MUST be used after Cancun is activated',
}
} else if (shanghaiTimestamp === null || parseInt(params[0].timestamp) < shanghaiTimestamp) {
if (withdrawals !== undefined && withdrawals !== null) {
throw {
code: INVALID_PARAMS,
message: 'ExecutionPayloadV1 MUST be used before Shanghai is activated',
}
}
} else if (parseInt(params[0].timestamp) >= shanghaiTimestamp) {
if (withdrawals === undefined || withdrawals === null) {
throw {
code: INVALID_PARAMS,
message: 'ExecutionPayloadV2 MUST be used after Shanghai is activated',
}
}
}
const newPayloadRes = await this.newPayload(params)
if (newPayloadRes.status === Status.INVALID_BLOCK_HASH) {
newPayloadRes.status = Status.INVALID
}
return newPayloadRes
}
async newPayloadV3(params: [ExecutionPayloadV3, Bytes32[], Bytes32]): Promise<PayloadStatusV1> {
const eip4844Timestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Cancun)
const ts = parseInt(params[0].timestamp)
if (eip4844Timestamp === null || ts < eip4844Timestamp) {
throw {
code: INVALID_PARAMS,
message: 'NewPayloadV{1|2} MUST be used before Cancun is activated',
}
}
const newPayloadRes = await this.newPayload(params)
if (newPayloadRes.status === Status.INVALID_BLOCK_HASH) {
newPayloadRes.status = Status.INVALID
}
return newPayloadRes
}
/**
* Propagates the change in the fork choice to the execution client.
*
* @param params An array of one parameter:
* 1. An object - The state of the fork choice:
* headBlockHash - block hash of the head of the canonical chain
* safeBlockHash - the "safe" block hash of the canonical chain under certain synchrony
* and honesty assumptions. This value MUST be either equal to or an ancestor of headBlockHash
* finalizedBlockHash - block hash of the most recent finalized block
* 2. An object or null - instance of {@link PayloadAttributesV1}
* @returns An object:
* 1. payloadStatus: {@link PayloadStatusV1}; values of the `status` field in the context of this method are restricted to the following subset::
* VALID
* INVALID
* SYNCING
* 2. payloadId: DATA|null - 8 Bytes - identifier of the payload build process or `null`
* 3. headBlock: Block|undefined - Block corresponding to headBlockHash if found
*/
private async forkchoiceUpdated(
params: [forkchoiceState: ForkchoiceStateV1, payloadAttributes: PayloadAttributes | undefined]
): Promise<ForkchoiceResponseV1 & { headBlock?: Block }> {
const { headBlockHash, finalizedBlockHash, safeBlockHash } = params[0]
const payloadAttributes = params[1]
const safe = toBytes(safeBlockHash)
const finalized = toBytes(finalizedBlockHash)
if (!equalsBytes(finalized, zeroBlockHash) && equalsBytes(safe, zeroBlockHash)) {
throw {
code: INVALID_PARAMS,
message: 'safe block can not be zero if finalized is not zero',
}
}
if (this.config.synchronized) {
this.connectionManager.newForkchoiceLog()
}
// It is possible that newPayload didn't start beacon sync as the payload it was asked to
// evaluate didn't require syncing beacon. This can happen if the EL<>CL starts and CL
// starts from a bit behind like how lodestar does
if (!this.service.beaconSync && !this.config.disableBeaconSync) {
await this.service.switchToBeaconSync()
}
/*
* Process head block
*/
let headBlock: Block | undefined
try {
const head = toBytes(headBlockHash)
headBlock =
this.remoteBlocks.get(headBlockHash.slice(2)) ??
(await this.service.beaconSync?.skeleton.getBlockByHash(head, true)) ??
(await this.chain.getBlock(head))
} catch (error) {
this.config.logger.debug(`Forkchoice requested unknown head hash=${short(headBlockHash)}`)
const payloadStatus = {
status: Status.SYNCING,
latestValidHash: null,
validationError: null,
}
const response = { payloadStatus, payloadId: null }
return response
}
const hardfork = headBlock.common.hardfork()
if (hardfork !== this.lastForkchoiceUpdatedHF && this.lastForkchoiceUpdatedHF !== '') {
this.config.logger.info(
`Hardfork change along forkchoice head block update number=${
headBlock.header.number
} hash=${short(headBlock.hash())} old=${this.lastForkchoiceUpdatedHF} new=${hardfork}`
)
}
this.lastForkchoiceUpdatedHF = hardfork
// Always keep beaconSync skeleton updated so that it stays updated with any skeleton sync
// requirements that might come later because of reorg or CL restarts
this.config.logger.debug(
`Forkchoice requested update to new head number=${headBlock.header.number} hash=${short(
headBlock.hash()
)}`
)
await this.service.beaconSync?.setHead(headBlock)
// Only validate this as terminal block if this block's difficulty is non-zero,
// else this is a PoS block but its hardfork could be indeterminable if the skeleton
// is not yet connected.
if (!headBlock.common.gteHardfork(Hardfork.Paris) && headBlock.header.difficulty > BigInt(0)) {
const validTerminalBlock = await validateTerminalBlock(headBlock, this.chain)
if (!validTerminalBlock) {
const response = {
payloadStatus: {
status: Status.INVALID,
validationError: null,
latestValidHash: bytesToHex(zeros(32)),
},
payloadId: null,
}
return response
}
}
const isHeadExecuted = await this.vm.stateManager.hasStateRoot(headBlock.header.stateRoot)
if (!isHeadExecuted) {
// execution has not yet caught up, so lets just return sync
const payloadStatus = {
status: Status.SYNCING,
latestValidHash: null,
validationError: null,
}
const response = { payloadStatus, payloadId: null }
return response
}
/*
* Process safe and finalized block since headBlock has been found to be executed
* Allowed to have zero value while transition block is finalizing
*/
let safeBlock, finalizedBlock
if (!equalsBytes(safe, zeroBlockHash)) {
if (equalsBytes(safe, headBlock.hash())) {
safeBlock = headBlock
} else {
try {
// Right now only check if the block is available, canonicality check is done
// in setHead after chain.putBlocks so as to reflect latest canonical chain
safeBlock =
(await this.service.beaconSync?.skeleton.getBlockByHash(safe, true)) ??
(await this.chain.getBlock(safe))
} catch (_error: any) {
throw {
code: INVALID_PARAMS,
message: 'safe block not available',
}
}
}
} else {
safeBlock = undefined
}
if (!equalsBytes(finalized, zeroBlockHash)) {
try {
// Right now only check if the block is available, canonicality check is done
// in setHead after chain.putBlocks so as to reflect latest canonical chain
finalizedBlock =
(await this.service.beaconSync?.skeleton.getBlockByHash(finalized, true)) ??
(await this.chain.getBlock(finalized))
} catch (error: any) {
throw {
message: 'finalized block not available',
code: INVALID_PARAMS,
}
}
} else {
finalizedBlock = undefined
}
const vmHeadHash = (await this.chain.blockchain.getIteratorHead()).hash()
if (!equalsBytes(vmHeadHash, headBlock.hash())) {
let parentBlocks: Block[] = []
if (this.chain.headers.latest && this.chain.headers.latest.number < headBlock.header.number) {
try {
parentBlocks = await recursivelyFindParents(
vmHeadHash,
headBlock.header.parentHash,