Skip to content

Commit f58a5a9

Browse files
authored
Create execution payload gossip validation (#7623)
* feat: disable execution payload validation for beacon block on gloas * chore: include gloas spec for beacon_block * feat: full block pool * fix: case * feat: check block by root * feat: add validation for execution payload * feat: init full block pool * feat: create execution payload gossip processor * refactor: move toBlockId under gloas * test: add full block pool * fix: simplify checks * chore: copyright year * fix: validation * test: update case name * feat: log bid for fallback investigation * fix: specify beacon block type * chore: todo * refactor: remove processing status * chore: free unused var * chore: import ordering * fix: beacon time from cfg * refactor: check seen block from existing data * fix: beacon time from time params * fix: time params * fix: time params * fix: use table * refactor: remove unused function * refactor: rename processor * refactor: remove unused block execution enabled impl * revert: gloas beacon_block validation * revert: undo full block pool * refactor: execution payload gossip validation * revert: extract changes * feat: add envelope quarantine * revert: envelope block id * feat: add envelope table * feat: update gossip handler * test: envelope quarantine * fix: func to use * fix: verify sig * fix: add missing by root * chore: cleanup * fix: style and comment * fix: func call
1 parent 139abcd commit f58a5a9

File tree

8 files changed

+353
-6
lines changed

8 files changed

+353
-6
lines changed

AllTests-mainnet.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,13 @@ AllTests-mainnet
590590
+ Roundtrip engine RPC V2 and capella ExecutionPayload representations OK
591591
+ Roundtrip engine RPC V3 and deneb ExecutionPayload representations OK
592592
```
593+
## Envelope Quarantine
594+
```diff
595+
+ Add missing OK
596+
+ Add orphan OK
597+
+ Clean up orphans OK
598+
+ Pop orphan OK
599+
```
593600
## Eth1 monitor
594601
```diff
595602
+ Rewrite URLs OK

beacon_chain/beacon_chain_db.nim

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ type
121121

122122
stateRoots: KvStoreRef # (Slot, BlockRoot) -> StateRoot
123123

124+
envelopes: KvStoreRef # (BlockRoot -> SignedExecutionPayloadEnvelope)
125+
124126
statesNoVal: array[ConsensusFork, KvStoreRef] # StateRoot -> ForkBeaconStateNoImmutableValidators
125127

126128
stateDiffs: KvStoreRef ##\
@@ -604,6 +606,10 @@ proc new*(T: type BeaconChainDB,
604606
if cfg.FULU_FORK_EPOCH != FAR_FUTURE_EPOCH:
605607
columns = kvStore db.openKvStore("fulu_columns").expectDb()
606608

609+
var envelopes: KvStoreRef
610+
if cfg.GLOAS_FORK_EPOCH != FAR_FUTURE_EPOCH:
611+
envelopes = kvStore db.openKvStore("gloas_envelopes").expectDb()
612+
607613
let quarantine = db.initQuarantineDB().expectDb()
608614

609615
# Versions prior to 1.4.0 (altair) stored validators in `immutable_validators`
@@ -642,6 +648,7 @@ proc new*(T: type BeaconChainDB,
642648
blocks: blocks,
643649
blobs: blobs,
644650
columns: columns,
651+
envelopes: envelopes,
645652
stateRoots: stateRoots,
646653
statesNoVal: statesNoVal,
647654
stateDiffs: stateDiffs,
@@ -866,6 +873,14 @@ proc delDataColumnSidecar*(
866873
root: Eth2Digest, index: ColumnIndex): bool =
867874
db.columns.del(columnkey(root, index)).expectDb()
868875

876+
proc putExecutionPayloadEnvelope*(
877+
db: BeaconChainDB, value: SignedExecutionPayloadEnvelope) =
878+
template key: untyped = value.message.beacon_block_root
879+
db.envelopes.putSZSSZ(key.data, value)
880+
881+
proc delExecutionPayloadEnvelope*(db: BeaconChainDB, root: Eth2Digest): bool =
882+
db.envelopes.del(root.data).expectDb()
883+
869884
proc updateImmutableValidators*(
870885
db: BeaconChainDB, validators: openArray[Validator]) =
871886
# Must be called before storing a state that references the new validators
@@ -1078,6 +1093,13 @@ proc getDataColumnSidecar*(db: BeaconChainDB, root: Eth2Digest, index: ColumnInd
10781093
return false
10791094
db.columns.getSZSSZ(columnkey(root, index), value) == GetResult.found
10801095

1096+
proc getExecutionPayloadEnvelope*(
1097+
db: BeaconChainDB, root: Eth2Digest,
1098+
value: var TrustedSignedExecutionPayloadEnvelope): bool =
1099+
if db.envelopes == nil:
1100+
return false
1101+
db.envelopes.getSZSSZ(root.data, value) == GetResult.found
1102+
10811103
proc getBlockSZ*[X: ForkyTrustedSignedBeaconBlock](
10821104
db: BeaconChainDB, key: Eth2Digest,
10831105
data: var seq[byte], T: typedesc[X]): bool =
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# beacon_chain
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed and distributed under either of
4+
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
5+
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
6+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
7+
8+
{.push raises: [], gcsafe.}
9+
10+
import std/tables
11+
import ../spec/[digest, forks]
12+
13+
type
14+
EnvelopeQuarantine* = object
15+
orphans*: Table[Eth2Digest, Table[uint64, SignedExecutionPayloadEnvelope]]
16+
## Envelopes that we have received but did not have a block yet. In the
17+
## ideal scenario, block should arrive before envelope but that is not
18+
## guaranteed.
19+
20+
missing*: HashSet[Eth2Digest]
21+
## List of block roots that we would like to have the envelopes but we
22+
## have not got yet. Missing envelopes should usually be found when we
23+
## received a block, blob or data column.
24+
25+
func init*(T: typedesc[EnvelopeQuarantine]): T =
26+
T()
27+
28+
template root(v: SignedExecutionPayloadEnvelope): Eth2Digest =
29+
v.message.beacon_block_root
30+
31+
func addMissing*(
32+
self: var EnvelopeQuarantine,
33+
root: Eth2Digest) =
34+
self.missing.incl(root)
35+
36+
func addOrphan*(
37+
self: var EnvelopeQuarantine,
38+
envelope: SignedExecutionPayloadEnvelope) =
39+
discard self.orphans
40+
.mgetOrPut(envelope.root)
41+
.hasKeyOrPut(envelope.message.builder_index, envelope)
42+
43+
func popOrphan*(
44+
self: var EnvelopeQuarantine,
45+
blck: gloas.SignedBeaconBlock,
46+
): Opt[SignedExecutionPayloadEnvelope] =
47+
if blck.root notin self.orphans:
48+
return Opt.none(SignedExecutionPayloadEnvelope)
49+
50+
template builderIdx: untyped =
51+
blck.message.body.signed_execution_payload_bid.message.builder_index
52+
try:
53+
var envelope: SignedExecutionPayloadEnvelope
54+
if self.orphans[blck.root].pop(builderIdx, envelope):
55+
Opt.some(envelope)
56+
else:
57+
Opt.none(SignedExecutionPayloadEnvelope)
58+
except KeyError:
59+
Opt.none(SignedExecutionPayloadEnvelope)
60+
finally:
61+
# After popping an envelope by block, the rest will no longer be valid due to
62+
# the mismatch builder index.
63+
self.orphans.del(blck.root)
64+
65+
func cleanupOrphans*(self: var EnvelopeQuarantine, finalizedSlot: Slot) =
66+
var toDel: seq[Eth2Digest]
67+
68+
for k, v in self.orphans:
69+
for _, e in v:
70+
if finalizedSlot >= e.message.slot:
71+
toDel.add(k)
72+
# check only the first envelope as slot should be the same by block root.
73+
break
74+
75+
for k in toDel:
76+
self.orphans.del(k)

beacon_chain/gossip_processing/eth2_processor.nim

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import
1616
../el/el_manager,
1717
../spec/[helpers, forks],
1818
../consensus_object_pools/[
19-
blob_quarantine, block_clearance, block_quarantine, blockchain_dag,
20-
attestation_pool, execution_payload_pool, light_client_pool,
21-
sync_committee_msg_pool, validator_change_pool],
19+
attestation_pool, blob_quarantine, block_clearance, block_quarantine,
20+
blockchain_dag, envelope_quarantine, execution_payload_pool,
21+
light_client_pool, sync_committee_msg_pool, validator_change_pool],
2222
../validators/validator_pool,
2323
../beacon_clock,
2424
"."/[gossip_validation, block_processor, batch_validation],
@@ -45,6 +45,10 @@ declareCounter beacon_blocks_received,
4545
"Number of valid blocks processed by this node"
4646
declareCounter beacon_blocks_dropped,
4747
"Number of invalid blocks dropped by this node", labels = ["reason"]
48+
declareCounter execution_payload_envelopes_received,
49+
"Number of valid execution payload envelope processed by this node"
50+
declareCounter execution_payload_envelopes_dropped,
51+
"Number of invalid execution payload envelope dropped by this node", labels = ["reason"]
4852
declareCounter blob_sidecars_received,
4953
"Number of valid blobs processed by this node"
5054
declareCounter blob_sidecars_dropped,
@@ -86,7 +90,7 @@ declareCounter beacon_execution_payload_bids_received,
8690
"Number of valid execution payload bids processed by this node"
8791

8892
declareCounter beacon_execution_payload_bids_dropped,
89-
"Number of invalid execution payload bids dropped by this node",
93+
"Number of invalid execution payload bids dropped by this node",
9094
labels = ["reason"]
9195

9296
const delayBuckets = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, Inf]
@@ -100,6 +104,9 @@ declareHistogram beacon_aggregate_delay,
100104
declareHistogram beacon_block_delay,
101105
"Time(s) between slot start and beacon block reception", buckets = delayBuckets
102106

107+
declareHistogram execution_payload_envelope_delay,
108+
"Time(s) between slot start and execution payload envelope reception", buckets = delayBuckets
109+
103110
declareHistogram blob_sidecar_delay,
104111
"Time(s) between slot start and blob sidecar reception", buckets = delayBuckets
105112

@@ -165,6 +172,8 @@ type
165172

166173
dataColumnQuarantine*: ref ColumnQuarantine
167174

175+
envelopeQuarantine*: ref EnvelopeQuarantine
176+
168177
# Application-provided current time provider (to facilitate testing)
169178
getCurrentBeaconTime*: GetBeaconTimeFn
170179

@@ -305,6 +314,37 @@ proc processSignedBeaconBlock*(
305314

306315
ok()
307316

317+
proc processExecutionPayloadEnvelope*(
318+
self: var Eth2Processor, src: MsgSource,
319+
signedEnvelope: SignedExecutionPayloadEnvelope): ValidationRes =
320+
let
321+
wallTime = self.getCurrentBeaconTime()
322+
(afterGenesis, wallSlot) = wallTime.toSlot(self.dag.timeParams)
323+
324+
logScope:
325+
blockRoot = shortLog(signedEnvelope.message.beacon_block_root)
326+
envelope = shortLog(signedEnvelope.message)
327+
wallSlot
328+
329+
if not afterGenesis:
330+
notice "Execution payload envelope before genesis"
331+
return errIgnore("Execution payload envelope before genesis")
332+
333+
let delay = wallTime -
334+
signedEnvelope.message.slot.start_beacon_time(self.dag.timeParams)
335+
336+
self.dag.validateExecutionPayload(
337+
self.quarantine, self.envelopeQuarantine, signedEnvelope).isOkOr:
338+
execution_payload_envelopes_dropped.inc(1, [$error[0]])
339+
return err(error)
340+
341+
debugGloasComment("process execution payload")
342+
343+
execution_payload_envelopes_received.inc()
344+
execution_payload_envelope_delay.observe(delay.toFloatSeconds())
345+
346+
ok()
347+
308348
proc processBlobSidecar*(
309349
self: var Eth2Processor, src: MsgSource,
310350
blobSidecar: deneb.BlobSidecar, subnet_id: BlobId): ValidationRes =

beacon_chain/gossip_processing/gossip_validation.nim

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import
1818
helpers, network, signatures, peerdas_helpers],
1919
../consensus_object_pools/[
2020
attestation_pool, blockchain_dag, blob_quarantine, block_clearance,
21-
block_quarantine, execution_payload_pool, spec_cache, light_client_pool,
22-
sync_committee_msg_pool, validator_change_pool],
21+
block_quarantine, envelope_quarantine, execution_payload_pool,
22+
light_client_pool, spec_cache, sync_committee_msg_pool,
23+
validator_change_pool],
2324
".."/[beacon_clock],
2425
./batch_validation
2526

@@ -980,6 +981,94 @@ proc validateBeaconBlock*(
980981

981982
ok()
982983

984+
# https://github.com/ethereum/consensus-specs/blob/v1.6.0/specs/gloas/p2p-interface.md#execution_payload
985+
proc validateExecutionPayload*(
986+
dag: ChainDAGRef, quarantine: ref Quarantine,
987+
envelopeQuarantine: ref EnvelopeQuarantine,
988+
signed_execution_payload_envelope: SignedExecutionPayloadEnvelope):
989+
Result[void, ValidationError] =
990+
template envelope: untyped = signed_execution_payload_envelope.message
991+
992+
# [IGNORE] The envelope's block root envelope.block_root has been seen (via
993+
# gossip or non-gossip sources) (a client MAY queue payload for processing
994+
# once the block is retrieved).
995+
let blockSeen =
996+
block:
997+
var seen =
998+
envelope.beacon_block_root in quarantine.unviable or
999+
envelope.beacon_block_root in quarantine.missing or
1000+
dag.getBlockRef(envelope.beacon_block_root).isSome()
1001+
if not seen:
1002+
for k, _ in quarantine.orphans:
1003+
if k[0] == envelope.beacon_block_root:
1004+
seen = true
1005+
break
1006+
seen
1007+
if not blockSeen:
1008+
discard quarantine[].addMissing(envelope.beacon_block_root)
1009+
envelopeQuarantine[].addOrphan(signed_execution_payload_envelope)
1010+
return errIgnore("ExecutionPayload: block not found")
1011+
1012+
# [IGNORE] The node has not seen another valid SignedExecutionPayloadEnvelope
1013+
# for this block root from this builder.
1014+
#
1015+
# Validation of an envelope requires a valid block. There is a check to ensure
1016+
# that the builder index are the same from the envelope and the bid from the
1017+
# block. Meaning that checking builder index here would not be helpful due to
1018+
# the check later.
1019+
var validEnvelope: TrustedSignedExecutionPayloadEnvelope
1020+
if dag.db.getExecutionPayloadEnvelope(
1021+
envelope.beacon_block_root, validEnvelope):
1022+
return errIgnore("ExecutionPayload: already seen")
1023+
1024+
# [IGNORE] The envelope is from a slot greater than or equal to the latest
1025+
# finalized slot -- i.e. validate that `envelope.slot >=
1026+
# compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)`
1027+
if not (envelope.slot >= dag.finalizedHead.slot):
1028+
return errIgnore("ExecutionPayload: slot already finalized")
1029+
1030+
# [REJECT] block passes validation.
1031+
let blck =
1032+
block:
1033+
let forkedBlock = dag.getForkedBlock(BlockId(
1034+
root: envelope.beacon_block_root, slot: envelope.slot)).valueOr:
1035+
return dag.checkedReject("ExecutionPayload: invalid block")
1036+
withBlck(forkedBlock):
1037+
when consensusFork >= ConsensusFork.Gloas:
1038+
forkyBlck.asSigned().message
1039+
else:
1040+
return dag.checkedReject("ExecutionPayload: invalid fork")
1041+
1042+
# [REJECT] block.slot equals envelope.slot.
1043+
if not (blck.slot == envelope.slot):
1044+
return dag.checkedReject("ExecutionPayload: slot mismatch")
1045+
1046+
template bid: untyped = blck.body.signed_execution_payload_bid.message
1047+
1048+
# [REJECT] envelope.builder_index == bid.builder_index
1049+
if not (envelope.builder_index == bid.builder_index):
1050+
return dag.checkedReject("ExecutionPayload: builder index mismatch")
1051+
1052+
# [REJECT] payload.block_hash == bid.block_hash
1053+
if not (envelope.payload.block_hash == bid.block_hash):
1054+
return dag.checkedReject("ExecutionPayload: block hash mismatch")
1055+
1056+
# [REJECT] signed_execution_payload_envelope.signature is valid with respect
1057+
# to the builder's public key.
1058+
if dag.headState.kind >= ConsensusFork.Gloas:
1059+
if not verify_execution_payload_envelope_signature(
1060+
dag.forkAtEpoch(envelope.slot.epoch),
1061+
dag.genesis_validators_root,
1062+
envelope.slot.epoch,
1063+
signed_execution_payload_envelope.message,
1064+
dag.validatorKey(envelope.builder_index).get(),
1065+
signed_execution_payload_envelope.signature):
1066+
return dag.checkedReject("ExecutionPayload: invalid builder signature")
1067+
else:
1068+
return dag.checkedReject("ExecutionPayload: invalid fork")
1069+
1070+
ok()
1071+
9831072
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/p2p-interface.md#beacon_attestation_subnet_id
9841073
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.5/specs/deneb/p2p-interface.md#beacon_aggregate_and_proof
9851074
proc validateAttestation*(

beacon_chain/spec/datatypes/gloas.nim

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,24 @@ type
102102
blob_kzg_commitments*: KzgCommitments
103103
state_root*: Eth2Digest
104104

105+
TrustedExecutionPayloadEnvelope* = object
106+
payload*: deneb.ExecutionPayload
107+
execution_requests*: ExecutionRequests
108+
builder_index*: uint64
109+
beacon_block_root*: Eth2Digest
110+
slot*: Slot
111+
blob_kzg_commitments*: KzgCommitments
112+
state_root*: Eth2Digest
113+
105114
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/beacon-chain.md#signedexecutionpayloadenvelope
106115
SignedExecutionPayloadEnvelope* = object
107116
message*: ExecutionPayloadEnvelope
108117
signature*: ValidatorSig
109118

119+
TrustedSignedExecutionPayloadEnvelope* = object
120+
message*: TrustedExecutionPayloadEnvelope
121+
signature*: ValidatorSig
122+
110123
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/beacon-chain.md#payloadattestationdata
111124
PayloadAttestationData* = object
112125
beacon_block_root*: Eth2Digest
@@ -616,11 +629,23 @@ func shortLog*(v: ExecutionPayloadBid): auto =
616629
blob_kzg_commitments_root: shortLog(v.blob_kzg_commitments_root),
617630
)
618631

632+
func shortLog*(v: ExecutionPayloadEnvelope): auto =
633+
(
634+
beacon_block_root: shortLog(v.beacon_block_root),
635+
slot: v.slot,
636+
builder_index: v.builder_index,
637+
state_root: shortLog(v.state_root)
638+
)
639+
619640
template asSigned*(
620641
x: SigVerifiedSignedBeaconBlock |
621642
TrustedSignedBeaconBlock): SignedBeaconBlock =
622643
isomorphicCast[SignedBeaconBlock](x)
623644

645+
template asSigned*(
646+
x: TrustedSignedExecutionPayloadEnvelope): SignedExecutionPayloadEnvelope =
647+
isomorphicCast[SignedExecutionPayloadEnvelope](x)
648+
624649
template asSigVerified*(
625650
x: SignedBeaconBlock |
626651
TrustedSignedBeaconBlock): SigVerifiedSignedBeaconBlock =

0 commit comments

Comments
 (0)