From ff51fd7d08ade1e47cf1612dd8d8f3ce6e04bc90 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:26:02 +0000 Subject: [PATCH 01/15] refactor: move envelope helpers --- beacon_chain/consensus_object_pools/envelope_quarantine.nim | 3 --- beacon_chain/gossip_processing/gossip_validation.nim | 4 ++-- beacon_chain/spec/forks.nim | 6 ++++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/beacon_chain/consensus_object_pools/envelope_quarantine.nim b/beacon_chain/consensus_object_pools/envelope_quarantine.nim index 12894266df..27da7f8970 100644 --- a/beacon_chain/consensus_object_pools/envelope_quarantine.nim +++ b/beacon_chain/consensus_object_pools/envelope_quarantine.nim @@ -25,9 +25,6 @@ type func init*(T: typedesc[EnvelopeQuarantine]): T = T() -template root(v: SignedExecutionPayloadEnvelope): Eth2Digest = - v.message.beacon_block_root - func addMissing*( self: var EnvelopeQuarantine, root: Eth2Digest) = diff --git a/beacon_chain/gossip_processing/gossip_validation.nim b/beacon_chain/gossip_processing/gossip_validation.nim index cebb3cbd71..326812a04e 100644 --- a/beacon_chain/gossip_processing/gossip_validation.nim +++ b/beacon_chain/gossip_processing/gossip_validation.nim @@ -1030,8 +1030,8 @@ proc validateExecutionPayload*( # [REJECT] block passes validation. let blck = block: - let forkedBlock = dag.getForkedBlock(BlockId( - root: envelope.beacon_block_root, slot: envelope.slot)).valueOr: + let forkedBlock = dag.getForkedBlock( + signed_execution_payload_envelope.toBlockId()).valueOr: return dag.checkedReject("ExecutionPayload: invalid block") withBlck(forkedBlock): when consensusFork >= ConsensusFork.Gloas: diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index a07ed06afe..b897511d27 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -1310,6 +1310,9 @@ template root*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Eth2Digest = withBlck(x): forkyBlck.root +template root*(v: gloas.SignedExecutionPayloadEnvelope): Eth2Digest = + v.message.beacon_block_root + template slot*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Slot = withBlck(x): forkyBlck.message.slot @@ -1823,6 +1826,9 @@ func toBlockId*(blck: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): BlockId = withBlck(blck): BlockId(root: forkyBlck.root, slot: forkyBlck.message.slot) +func toBlockId*(envelope: gloas.SignedExecutionPayloadEnvelope): BlockId = + BlockId(root: envelope.message.beacon_block_root, slot: envelope.message.slot) + func historical_summaries*(state: ForkedHashedBeaconState): HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT] = withState(state): From a2bb927e5cd57a57caa06e3da6e8503180bcf5fe Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:11:56 +0000 Subject: [PATCH 02/15] feat: init EnvelopeQuarantine --- beacon_chain/gossip_processing/block_processor.nim | 5 +++++ beacon_chain/gossip_processing/eth2_processor.nim | 2 ++ beacon_chain/nimbus_beacon_node.nim | 9 ++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index fab7728103..60b72727ae 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -29,6 +29,8 @@ from ../consensus_object_pools/block_quarantine import remove, startProcessing, clearProcessing, UnviableKind from ../consensus_object_pools/blob_quarantine import BlobQuarantine, ColumnQuarantine, popSidecars, put +from ../consensus_object_pools/envelope_quarantine import + EnvelopeQuarantine, addMissing, popOrphan from ../validators/validator_monitor import MsgSource, ValidatorMonitor, registerAttestationInBlock, registerBeaconBlock, registerSyncAggregateInBlock @@ -97,6 +99,7 @@ type blobQuarantine: ref BlobQuarantine dataColumnQuarantine*: ref ColumnQuarantine + envelopeQuarantine*: ref EnvelopeQuarantine verifier: BatchVerifier lastPayload: Slot @@ -121,6 +124,7 @@ proc new*(T: type BlockProcessor, validatorMonitor: ref ValidatorMonitor, blobQuarantine: ref BlobQuarantine, dataColumnQuarantine: ref ColumnQuarantine, + envelopeQuarantine: ref EnvelopeQuarantine, getBeaconTime: GetBeaconTimeFn, invalidBlockRoots: seq[Eth2Digest] = @[]): ref BlockProcessor = if invalidBlockRoots.len > 0: @@ -137,6 +141,7 @@ proc new*(T: type BlockProcessor, validatorMonitor: validatorMonitor, blobQuarantine: blobQuarantine, dataColumnQuarantine: dataColumnQuarantine, + envelopeQuarantine: envelopeQuarantine, getBeaconTime: getBeaconTime, verifier: batchVerifier[] ) diff --git a/beacon_chain/gossip_processing/eth2_processor.nim b/beacon_chain/gossip_processing/eth2_processor.nim index e660bc9a33..d658bcd0b0 100644 --- a/beacon_chain/gossip_processing/eth2_processor.nim +++ b/beacon_chain/gossip_processing/eth2_processor.nim @@ -199,6 +199,7 @@ proc new*(T: type Eth2Processor, quarantine: ref Quarantine, blobQuarantine: ref BlobQuarantine, dataColumnQuarantine: ref ColumnQuarantine, + envelopeQuarantine: ref EnvelopeQuarantine, rng: ref HmacDrbgContext, getBeaconTime: GetBeaconTimeFn, taskpool: Taskpool @@ -219,6 +220,7 @@ proc new*(T: type Eth2Processor, quarantine: quarantine, blobQuarantine: blobQuarantine, dataColumnQuarantine: dataColumnQuarantine, + envelopeQuarantine: envelopeQuarantine, getCurrentBeaconTime: getBeaconTime, batchCrypto: BatchCrypto.new( rng, dag.cfg.timeParams, diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 854827a306..9fb3b60f54 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -17,7 +17,8 @@ import eth/enr/enr, eth/p2p/discoveryv5/random2, ./consensus_object_pools/[ - blob_quarantine, blockchain_list, execution_payload_pool], + blob_quarantine, blockchain_list, envelope_quarantine, + execution_payload_pool], ./consensus_object_pools/vanity_logs/vanity_logs, ./networking/[topic_params, network_metadata_downloads], ./rpc/[rest_api, state_ttl_cache], @@ -402,6 +403,7 @@ proc initFullNode( let quarantine = newClone( Quarantine.init(dag.cfg)) + envelopeQuarantine = newClone(EnvelopeQuarantine.init()) attestationPool = newClone(AttestationPool.init( dag, quarantine, onPhase0AttestationReceived, onSingleAttestationReceived)) @@ -442,7 +444,7 @@ proc initFullNode( blockProcessor = BlockProcessor.new( config.dumpEnabled, config.dumpDirInvalid, config.dumpDirIncoming, batchVerifier, consensusManager, node.validatorMonitor, - blobQuarantine, dataColumnQuarantine, getBeaconTime, + blobQuarantine, dataColumnQuarantine, envelopeQuarantine, getBeaconTime, config.invalidBlockRoots) blockVerifier = proc(signedBlock: ForkedSignedBeaconBlock, blobs: Opt[BlobSidecars], maybeFinalized: bool): @@ -531,7 +533,8 @@ proc initFullNode( config.doppelgangerDetection, blockProcessor, node.validatorMonitor, dag, attestationPool, validatorChangePool, node.attachedValidators, syncCommitteeMsgPool, - lightClientPool, executionPayloadBidPool, quarantine, blobQuarantine, dataColumnQuarantine, + lightClientPool, executionPayloadBidPool, + quarantine, blobQuarantine, dataColumnQuarantine, envelopeQuarantine, rng, getBeaconTime, taskpool) syncManagerFlags = if node.config.longRangeSync != LongRangeSyncMode.Lenient: From 5a11365ecec8e93861db15e0ee7ff43c4d531c6e Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:37:09 +0000 Subject: [PATCH 03/15] feat: enable gossip --- beacon_chain/networking/eth2_network.nim | 3 ++- beacon_chain/nimbus_beacon_node.nim | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/beacon_chain/networking/eth2_network.nim b/beacon_chain/networking/eth2_network.nim index 531ce04848..00c85f94f1 100644 --- a/beacon_chain/networking/eth2_network.nim +++ b/beacon_chain/networking/eth2_network.nim @@ -849,7 +849,8 @@ template gossipMaxSize(T: untyped): uint32 = elif T is bellatrix.SignedBeaconBlock or T is capella.SignedBeaconBlock or T is deneb.SignedBeaconBlock or T is electra.SignedBeaconBlock or T is fulu.SignedBeaconBlock or T is fulu.DataColumnSidecar or - T is gloas.SignedBeaconBlock or T is gloas.DataColumnSidecar: + T is gloas.SignedBeaconBlock or T is gloas.DataColumnSidecar or + T is gloas.SignedExecutionPayloadEnvelope: MAX_PAYLOAD_SIZE # TODO https://github.com/status-im/nim-ssz-serialization/issues/20 for # Attestation, AttesterSlashing, and SignedAggregateAndProof, which all diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 9fb3b60f54..f64c019646 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -2351,6 +2351,19 @@ proc installMessageValidators(node: BeaconNode) = node.processor[].processExecutionPayloadBid( MsgSource.gossip, signedBid))) + # execution_payload + # https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/p2p-interface.md#execution_payload + when consensusFork >= ConsensusFork.Gloas: + node.network.addValidator( + getExecutionPayloadTopic(digest), proc ( + signedEnvelope: SignedExecutionPayloadEnvelope, + src: PeerId, + ): ValidationResult = + toValidationResult( + node.processor[].processExecutionPayloadEnvelope( + MsgSource.gossip, signedEnvelope)) + ) + # beacon_attestation_{subnet_id} # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.5/specs/phase0/p2p-interface.md#beacon_attestation_subnet_id # https://github.com/ethereum/consensus-specs/blob/v1.6.0-beta.0/specs/gloas/p2p-interface.md#beacon_attestation_subnet_id From 386abf3596d1b9676e10e076ea2c82b2b85ad0e6 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:08:44 +0000 Subject: [PATCH 04/15] feat: add envelope arg --- .../gossip_processing/block_processor.nim | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index 60b72727ae..1a7c492c3b 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -111,7 +111,13 @@ type NoSidecars | Opt[BlobSidecars] | Opt[fulu.DataColumnSidecars] | Opt[gloas.DataColumnSidecars] -const noSidecars* = default(NoSidecars) + NoEnvelope = typeof(()) + SomeOptEnvelope = + NoEnvelope | Opt[gloas.SignedExecutionPayloadEnvelope] + +const + noSidecars* = default(NoSidecars) + noEnvelope = default(NoEnvelope) # Initialization # ------------------------------------------------------------------------------ @@ -183,6 +189,7 @@ from ../consensus_object_pools/block_clearance import proc verifySidecars( signedBlock: ForkySignedBeaconBlock, + envelopeOpt: SomeOptEnvelope, sidecarsOpt: SomeOptSidecars, ): Result[void, VerifierError] = const consensusFork = typeof(signedBlock).kind @@ -230,6 +237,12 @@ proc verifySidecars( ok() +proc verifySidecars( + signedBlock: ForkySignedBeaconBlock, + sidecarsOpt: SomeOptSidecars, +): Result[void, VerifierError] = + verifySidecars(signedBlock, noEnvelope, sidecarsOpt) + proc storeSidecars(self: BlockProcessor, sidecarsOpt: Opt[BlobSidecars]) = if sidecarsOpt.isSome(): for b in sidecarsOpt[]: @@ -464,7 +477,9 @@ proc onBlockAdded*( ) proc verifyPayload( - self: ref BlockProcessor, signedBlock: ForkySignedBeaconBlock + self: ref BlockProcessor, + signedBlock: ForkySignedBeaconBlock, + envelopeOpt: SomeOptEnvelope, ): Result[OptimisticStatus, VerifierError] = const consensusFork = typeof(signedBlock).kind # When the execution layer is not available to verify the payload, we do the @@ -510,6 +525,11 @@ proc verifyPayload( else: ok OptimisticStatus.valid +proc verifyPayload( + self: ref BlockProcessor, signedBlock: ForkySignedBeaconBlock +): Result[OptimisticStatus, VerifierError] = + verifyPayload(self, signedBlock, noEnvelope) + proc enqueueFromDb(self: ref BlockProcessor, root: Eth2Digest) = # TODO This logic can be removed if the database schema is extended # to store non-canonical heads on top of the canonical head and learns to keep From cb4bbb40e2d69dd0e53c91fb992811d22296707a Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:12:47 +0000 Subject: [PATCH 05/15] feat: enqueue block payload --- .../gossip_processing/block_processor.nim | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index 1a7c492c3b..aa954946e1 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -30,7 +30,7 @@ from ../consensus_object_pools/block_quarantine import from ../consensus_object_pools/blob_quarantine import BlobQuarantine, ColumnQuarantine, popSidecars, put from ../consensus_object_pools/envelope_quarantine import - EnvelopeQuarantine, addMissing, popOrphan + EnvelopeQuarantine, addMissing, popOrphan, addOrphan from ../validators/validator_monitor import MsgSource, ValidatorMonitor, registerAttestationInBlock, registerBeaconBlock, registerSyncAggregateInBlock @@ -259,6 +259,8 @@ proc storeSidecars( proc storeSidecars(self: BlockProcessor, sidecarsOpt: NoSidecars) = discard +proc enqueuePayload*(self: ref BlockProcessor, blck: gloas.SignedBeaconBlock) + proc storeBackfillBlock( self: var BlockProcessor, signedBlock: ForkySignedBeaconBlock, @@ -863,3 +865,81 @@ proc addBlock*( err(res.error()) of VerifierError.Duplicate: err(res.error()) + +proc storePayload( + self: ref BlockProcessor, + signedBlock: gloas.SignedBeaconBlock, + signedEnvelope: gloas.SignedExecutionPayloadEnvelope, + sidecarsOpt: Opt[gloas.DataColumnSidecars], +): Future[Result[void, VerifierError]] {.async: (raises: [CancelledError]).} = + let optimisticStatus = + ?verifyPayload(self, signedBlock, Opt.some(signedEnvelope)) + + ?verifySidecars(signedBlock, Opt.some(signedEnvelope), sidecarsOpt) + + debugGloasComment("verify and process") + ok() + +proc enqueuePayload*( + self: ref BlockProcessor, + blck: gloas.SignedBeaconBlock, + envelope: gloas.SignedExecutionPayloadEnvelope, + sidecarsOpt: Opt[gloas.DataColumnSidecars], +) = + if blck.message.slot <= self.consensusManager.dag.finalizedHead.slot: + debugGloasComment("backfilling") + + discard self.storePayload(blck, envelope, sidecarsOpt) + +proc enqueuePayload*(self: ref BlockProcessor, blck: gloas.SignedBeaconBlock) = + ## Enqueue payload processing by block that is a valid block. + + let + envelope = self.envelopeQuarantine[].popOrphan(blck).valueOr: + # We have not received the envelope yet so mark it as missing. + self.envelopeQuarantine[].addMissing(blck.root) + return + sidecarsOpt = + block: + let sidecarsOpt = + if envelope.message.blob_kzg_commitments.len() == 0: + Opt.some(default(gloas.DataColumnSidecars)) + else: + debugGloasComment("pop from ColumnQuarantine") + Opt.none(gloas.DataColumnSidecars) + if sidecarsOpt.isNone(): + # As sidecars are missing, put envelope back to quarantine. + self.consensusManager.quarantine[].addSidecarless(blck) + self.envelopeQuarantine[].addOrphan(envelope) + return + sidecarsOpt + + self.enqueuePayload(blck, envelope, sidecarsOpt) + +proc enqueuePayload*(self: ref BlockProcessor, blockRoot: Eth2Digest) = + ## Enqueue payload processing by block root. If the block is not valid, it + ## will be discarded. + + let + blockRef = self.consensusManager.dag.getBlockRef(blockRoot).valueOr: + return + blck = + block: + let forkedBlock = self.consensusManager.dag.getForkedBlock( + blockRef.bid).valueOr: + # We have checked that the block exists in the chain. There might be + # issues in reading the database or data in the memory is broken. + # Since no result is returned, we log for investigation. + debug "Enqueue payload from envelope. Block is missing in DB", + bid = shortLog(blockRef.bid) + return + withBlck(forkedBlock): + when consensusFork >= ConsensusFork.Gloas: + forkyBlck.asSigned() + else: + # Incorrect fork which shouldn't be happening. + debug "Enqueue payload from envelope. Block is in incorrect fork", + bid = shortLog(blockRef.bid) + return + + self.enqueuePayload(blck) From e201a733abdb627e4ca3448c4bad28ed0883d8e6 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:13:36 +0000 Subject: [PATCH 06/15] feat: payload processing --- .../gossip_processing/block_processor.nim | 58 ++++++++++++++----- .../gossip_processing/eth2_processor.nim | 18 +++--- beacon_chain/nimbus_beacon_node.nim | 3 +- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index aa954946e1..a61d50279f 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -194,10 +194,24 @@ proc verifySidecars( ): Result[void, VerifierError] = const consensusFork = typeof(signedBlock).kind - when consensusFork == ConsensusFork.Gloas: - # For Gloas, we still need to store the columns if they're provided - # but skip validation since we don't have kzg_commitments in the block - debugGloasComment "potentially validate against payload envelope" + when consensusFork >= ConsensusFork.Gloas: + if sidecarsOpt.isSome: + let + signedEnvelope = envelopeOpt.valueOr: + return err(VerifierError.Invalid) + columns = sidecarsOpt.get() + kzgCommits = signedEnvelope.message.blob_kzg_commitments.asSeq + if columns.len > 0 and kzgCommits.len > 0: + for i in 0 ..< columns.len: + let r = verify_data_column_sidecar_kzg_proofs(columns[i][]) + if r.isErr(): + debug "data column validation failed", + blockRoot = shortLog(signedBlock.root), + column_sidecar = shortLog(columns[i][]), + blck = shortLog(signedBlock.message), + signature = shortLog(signedBlock.signature), + msg = r.error() + return err(VerifierError.Invalid) elif consensusFork == ConsensusFork.Fulu: if sidecarsOpt.isSome: let columns = sidecarsOpt.get() @@ -262,7 +276,7 @@ proc storeSidecars(self: BlockProcessor, sidecarsOpt: NoSidecars) = proc enqueuePayload*(self: ref BlockProcessor, blck: gloas.SignedBeaconBlock) proc storeBackfillBlock( - self: var BlockProcessor, + self: ref BlockProcessor, signedBlock: ForkySignedBeaconBlock, sidecarsOpt: SomeOptSidecars, ): Result[void, VerifierError] = @@ -270,7 +284,10 @@ proc storeBackfillBlock( # In case the block was added to any part of the quarantine.. quarantine[].remove(signedBlock) - ?verifySidecars(signedBlock, sidecarsOpt) + const consensusFork = typeof(signedBlock).kind + + when consensusFork <= ConsensusFork.Fulu: + ?verifySidecars(signedBlock, sidecarsOpt) let res = self.consensusManager.dag.addBackfillBlock(signedBlock) @@ -300,8 +317,13 @@ proc storeBackfillBlock( of VerifierError.Duplicate: res else: - # Only store side cars after successfully establishing block viability. - self.storeSidecars(sidecarsOpt) + when consensusFork >= ConsensusFork.Gloas: + # Columns are in quarantine as they didn't pop from `rmanBlockVerifier`, + # we simply enqueue with the valid block. + self.enqueuePayload(signedBlock) + else: + # Only store side cars after successfully establishing block viability. + self[].storeSidecars(sidecarsOpt) res @@ -387,7 +409,7 @@ proc enqueueBlock*( if blck.message.slot <= self.consensusManager.dag.finalizedHead.slot: # let backfill blocks skip the queue - these are always "fast" to process # because there are no state rewinds to deal with - discard self[].storeBackfillBlock(blck, sidecarsOpt) + discard self.storeBackfillBlock(blck, sidecarsOpt) return # `discard` here means that the `async` task will continue running even though @@ -411,9 +433,8 @@ proc enqueueQuarantine(self: ref BlockProcessor, parent: BlockRef) = debug "Block from quarantine", parent, quarantined = shortLog(quarantined.root) withBlck(quarantined): - when consensusFork == ConsensusFork.Gloas: - debugGloasComment "" - const sidecarsOpt = noSidecars + when consensusFork >= ConsensusFork.Gloas: + let sidecarsOpt = Opt.none(gloas.DataColumnSidecars) elif consensusFork == ConsensusFork.Fulu: let sidecarsOpt = if len(forkyBlck.message.body.blob_kzg_commitments) == 0: @@ -649,7 +670,8 @@ proc storeBlock( let newPayloadTick = Moment.now() - ?verifySidecars(signedBlock, sidecarsOpt) + when consensusFork <= ConsensusFork.Fulu: + ?verifySidecars(signedBlock, sidecarsOpt) let blck = ?dag.addHeadBlockWithParent( @@ -667,7 +689,8 @@ proc storeBlock( self[].lastPayload = signedBlock.message.slot # write blobs now that block has been written. - self[].storeSidecars(sidecarsOpt) + when consensusFork <= ConsensusFork.Fulu: + self[].storeSidecars(sidecarsOpt) let addHeadBlockTick = Moment.now() @@ -717,6 +740,11 @@ proc storeBlock( blck = shortLog(blck), validationDur, queueDur, newPayloadDur, addHeadBlockDur, updateHeadDur + when consensusFork >= ConsensusFork.Gloas: + # Enqueue payload here instead of `addBlock` for the consistency of payload + # processing with backfilling. + self.enqueuePayload(signedBlock) + ok(blck) proc addBlock*( @@ -749,7 +777,7 @@ proc addBlock*( if blck.message.slot <= dag.finalizedHead.slot: # let backfill blocks skip the queue - these are always "fast" to process # because there are no state rewinds to deal with - return self[].storeBackfillBlock(blck, sidecarsOpt) + return self.storeBackfillBlock(blck, sidecarsOpt) let queueTick = Moment.now() let res = diff --git a/beacon_chain/gossip_processing/eth2_processor.nim b/beacon_chain/gossip_processing/eth2_processor.nim index d658bcd0b0..4ba99cc398 100644 --- a/beacon_chain/gossip_processing/eth2_processor.nim +++ b/beacon_chain/gossip_processing/eth2_processor.nim @@ -282,10 +282,11 @@ proc processSignedBeaconBlock*( if not (isNil(self.dag.onBlockGossipAdded)): self.dag.onBlockGossipAdded(ForkedSignedBeaconBlock.init(signedBlock)) - when consensusFork == ConsensusFork.Gloas: - debugGloasComment "" - # gloas needs proper data column handling - let sidecarsOpt = Opt.some(default(seq[ref gloas.DataColumnSidecar])) + when consensusFork >= ConsensusFork.Gloas: + # Passing none for Gloas to disable processing sidecars at block time. They + # should be retrieved from quarantine and processed at the end of + # `storeBlock` or `storeBackfillBlock`. + let sidecarsOpt = Opt.none(gloas.DataColumnSidecars) elif consensusFork == ConsensusFork.Fulu: let sidecarsOpt = if len(signedBlock.message.body.blob_kzg_commitments) == 0: @@ -340,7 +341,8 @@ proc processExecutionPayloadEnvelope*( execution_payload_envelopes_dropped.inc(1, [$error[0]]) return err(error) - debugGloasComment("process execution payload") + self.envelopeQuarantine[].addOrphan(signedEnvelope) + self.blockProcessor.enqueuePayload(signedEnvelope.root) execution_payload_envelopes_received.inc() execution_payload_envelope_delay.observe(delay.toFloatSeconds()) @@ -478,10 +480,8 @@ proc processDataColumnSidecar*( data_column_sidecars_dropped.inc(1, [$v.error[0]]) return v - debugGloasComment "" - # TODO: Implement quarantine logic for Gloas - # For now, just validate and drop - debug "Data column validated (not stored - quarantine TODO)" + debugGloasComment("put into ColumnQuarantine") + self.blockProcessor.enqueuePayload(dataColumnSidecar.beacon_block_root) data_column_sidecars_received.inc() v diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index f64c019646..22242974e1 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -475,8 +475,7 @@ proc initFullNode( maybeFinalized: bool): Future[Result[void, VerifierError]] {.async: (raises: [CancelledError]).} = withBlck(signedBlock): - when consensusFork == ConsensusFork.Gloas: - debugGloasComment "no blob_kzg_commitments field for gloas" + when consensusFork >= ConsensusFork.Gloas: let sidecarsOpt = Opt.none(gloas.DataColumnSidecars) elif consensusFork == ConsensusFork.Fulu: let sidecarsOpt = From 5dc241dc0356c8bd004a54a99bfddca46e17f017 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:37:31 +0000 Subject: [PATCH 07/15] fix: check envelope --- beacon_chain/gossip_processing/block_processor.nim | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index a61d50279f..10b10818c1 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -195,10 +195,9 @@ proc verifySidecars( const consensusFork = typeof(signedBlock).kind when consensusFork >= ConsensusFork.Gloas: - if sidecarsOpt.isSome: + if envelopeOpt.isSome and sidecarsOpt.isSome: let - signedEnvelope = envelopeOpt.valueOr: - return err(VerifierError.Invalid) + signedEnvelope = envelopeOpt.get() columns = sidecarsOpt.get() kzgCommits = signedEnvelope.message.blob_kzg_commitments.asSeq if columns.len > 0 and kzgCommits.len > 0: From e4acad95659d72c77c823a54a86a6ae07ab67a5c Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:00:01 +0000 Subject: [PATCH 08/15] fix: test --- tests/test_block_processor.nim | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_block_processor.nim b/tests/test_block_processor.nim index 624abb270b..b18081ecce 100644 --- a/tests/test_block_processor.nim +++ b/tests/test_block_processor.nim @@ -18,7 +18,7 @@ import ../beacon_chain/gossip_processing/block_processor, ../beacon_chain/consensus_object_pools/[ attestation_pool, blockchain_dag, blob_quarantine, block_quarantine, - block_clearance, consensus_manager, + block_clearance, consensus_manager, envelope_quarantine, ], ../beacon_chain/el/el_manager, ./[testblockutil, testdbutil, testutil] @@ -53,6 +53,7 @@ suite "Block processor" & preset(): quarantine = newClone(Quarantine.init(cfg)) blobQuarantine = newClone(BlobQuarantine()) dataColumnQuarantine = newClone(ColumnQuarantine()) + envelopeQuarantine = newClone(EnvelopeQuarantine()) attestationPool = newClone(AttestationPool.init(dag, quarantine)) elManager = new ELManager # TODO: initialise this properly actionTracker = default(ActionTracker) @@ -83,7 +84,7 @@ suite "Block processor" & preset(): let processor = BlockProcessor.new( false, "", "", batchVerifier, consensusManager, validatorMonitor, - blobQuarantine, dataColumnQuarantine, getTimeFn, + blobQuarantine, dataColumnQuarantine, envelopeQuarantine, getTimeFn, ) b1 = addTestBlock(state[], cache, cfg = cfg).bellatrixData b2 = addTestBlock(state[], cache, cfg = cfg).bellatrixData @@ -145,7 +146,7 @@ suite "Block processor" & preset(): processor = BlockProcessor.new( false, "", "", batchVerifier, consensusManager, validatorMonitor, blobQuarantine, dataColumnQuarantine, - getTimeFn, invalidBlockRoots = @[b2.root]) + envelopeQuarantine, getTimeFn, invalidBlockRoots = @[b2.root]) block: let res = await processor.addBlock(MsgSource.gossip, b2, noSidecars) @@ -175,8 +176,8 @@ suite "Block processor" & preset(): asyncTest "Process a block from each fork (without blobs)" & preset(): let processor = BlockProcessor.new( - false, "", "", batchVerifier, consensusManager, validatorMonitor, blobQuarantine, - dataColumnQuarantine, getTimeFn, + false, "", "", batchVerifier, consensusManager, validatorMonitor, + blobQuarantine, dataColumnQuarantine, envelopeQuarantine, getTimeFn, ) debugGloasComment "TODO testing" From efd792f5a2834ec4b0bc9431407c48e27545f9c5 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:39:10 +0000 Subject: [PATCH 09/15] bump: nim-web3 --- vendor/nim-web3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-web3 b/vendor/nim-web3 index 141907cd95..cf7325af55 160000 --- a/vendor/nim-web3 +++ b/vendor/nim-web3 @@ -1 +1 @@ -Subproject commit 141907cd958d7ee3b554ec94bc9ac7ec692e546b +Subproject commit cf7325af55df9c1b16c1d84427b871fa4f332759 From dccd0cf85e7641dc44516c6ccef4df985f973d6e Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:15:45 +0000 Subject: [PATCH 10/15] refactor: move noEnvelope --- beacon_chain/gossip_processing/block_processor.nim | 5 +---- beacon_chain/spec/forks.nim | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index 10b10818c1..efd273d4e4 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -111,13 +111,10 @@ type NoSidecars | Opt[BlobSidecars] | Opt[fulu.DataColumnSidecars] | Opt[gloas.DataColumnSidecars] - NoEnvelope = typeof(()) SomeOptEnvelope = NoEnvelope | Opt[gloas.SignedExecutionPayloadEnvelope] -const - noSidecars* = default(NoSidecars) - noEnvelope = default(NoEnvelope) +const noSidecars* = default(NoSidecars) # Initialization # ------------------------------------------------------------------------------ diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index b897511d27..9a0ce4ab94 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -380,6 +380,10 @@ type fuluInt: ForkDigest bpos: seq[(Epoch, ConsensusFork, ForkDigest)] + NoEnvelope* = typeof(()) + +const noEnvelope* = default(NoEnvelope) + template kind*( x: typedesc[ phase0.BeaconState | From 26c7cf345064a2bbb007681594335e3dccec275e Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:44:58 +0000 Subject: [PATCH 11/15] feat: pass envelope to funcs and helpers --- beacon_chain/el/el_manager.nim | 29 +++++++++++++---- .../gossip_processing/eth2_processor.nim | 6 ++-- beacon_chain/spec/helpers.nim | 11 +++++++ beacon_chain/spec/helpers_el.nim | 32 ++++++++++++++----- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/beacon_chain/el/el_manager.nim b/beacon_chain/el/el_manager.nim index 22c2cd792b..966fec1297 100644 --- a/beacon_chain/el/el_manager.nim +++ b/beacon_chain/el/el_manager.nim @@ -827,27 +827,44 @@ proc sendGetBlobsV2*( proc sendNewPayload*( m: ELManager, blck: SomeForkyBeaconBlock, + envelope: NoEnvelope | gloas.ExecutionPayloadEnvelope, deadline: DeadlineFuture, retry: bool, ): Future[Opt[PayloadExecutionStatus]] {.async: (raises: [CancelledError]).} = + const consensusFork = typeof(blck).kind + + template forkyExecutionPayload(): auto = + when consensusFork >= ConsensusFork.Gloas: + envelope.payload + else: + blck.body.execution_payload + template forkyExecutionRequests(): auto = + when consensusFork >= ConsensusFork.Gloas: + envelope.execution_requests + else: + blck.body.execution_requests + template forkyKzgCommitments(): auto = + when consensusFork >= ConsensusFork.Gloas: + envelope.blob_kzg_commitments + elif consensusFork >= ConsensusFork.Deneb: + blck.body.blob_kzg_commitments + if m.elConnections.len == 0: info "No execution client configured; cannot process block payloads", - executionPayload = shortLog(blck.body.execution_payload) + executionPayload = shortLog(forkyExecutionPayload) return Opt.none(PayloadExecutionStatus) - const consensusFork = typeof(blck).kind - let startTime = Moment.now() - payload = blck.body.execution_payload.asEngineExecutionPayload + payload = forkyExecutionPayload.asEngineExecutionPayload() when consensusFork >= ConsensusFork.Deneb: let - versioned_hashes = blck.body.blob_kzg_commitments.asEngineVersionedHashes() + versioned_hashes = forkyKzgCommitments.asEngineVersionedHashes() parent_root = blck.parent_root.to(Hash32) when consensusFork >= ConsensusFork.Electra: - let execution_requests = blck.body.execution_requests.asEngineExecutionRequests() + let execution_requests = forkyExecutionRequests.asEngineExecutionRequests() var responseProcessor = ELConsensusViolationDetector.init() diff --git a/beacon_chain/gossip_processing/eth2_processor.nim b/beacon_chain/gossip_processing/eth2_processor.nim index 4ba99cc398..6b8dd6b2f2 100644 --- a/beacon_chain/gossip_processing/eth2_processor.nim +++ b/beacon_chain/gossip_processing/eth2_processor.nim @@ -283,10 +283,8 @@ proc processSignedBeaconBlock*( self.dag.onBlockGossipAdded(ForkedSignedBeaconBlock.init(signedBlock)) when consensusFork >= ConsensusFork.Gloas: - # Passing none for Gloas to disable processing sidecars at block time. They - # should be retrieved from quarantine and processed at the end of - # `storeBlock` or `storeBackfillBlock`. - let sidecarsOpt = Opt.none(gloas.DataColumnSidecars) + # Disable processing sidecars at block time. + const sidecarsOpt = noSidecars elif consensusFork == ConsensusFork.Fulu: let sidecarsOpt = if len(signedBlock.message.body.blob_kzg_commitments) == 0: diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 32a560d07f..55f9ce4242 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -569,6 +569,17 @@ func compute_execution_block_hash*( func compute_execution_block_hash*(blck: ForkyBeaconBlock): Eth2Digest = blck.body.compute_execution_block_hash(blck.parent_root) +func compute_execution_block_hash*( + blck: gloas.BeaconBlock, + envelope: gloas.ExecutionPayloadEnvelope): Eth2Digest = + const consensusFork = typeof(blck).kind + compute_execution_block_hash( + consensusFork, + envelope.payload, + blck.parent_root, + Opt.some envelope.execution_requests.computeRequestsHash(), + ) + # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/beacon-chain.md#new-is_builder_payment_withdrawable func is_builder_payment_withdrawable*( state: gloas.BeaconState, diff --git a/beacon_chain/spec/helpers_el.nim b/beacon_chain/spec/helpers_el.nim index cf85c97c69..85e27a3896 100644 --- a/beacon_chain/spec/helpers_el.nim +++ b/beacon_chain/spec/helpers_el.nim @@ -20,22 +20,38 @@ func readExecutionTransaction( err("Invalid transaction: " & exc.msg) # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.4/specs/deneb/beacon-chain.md#is_valid_versioned_hashes -func is_valid_versioned_hashes*(blck: ForkyBeaconBlock): Result[void, string] = - static: doAssert typeof(blck).kind >= ConsensusFork.Deneb - template transactions: untyped = blck.body.execution_payload.transactions - template commitments: untyped = blck.body.blob_kzg_commitments +func is_valid_versioned_hashes*( + blck: ForkyBeaconBlock, + envelope: NoEnvelope | gloas.ExecutionPayloadEnvelope, +): Result[void, string] = + const consensusFork = typeof(blck).kind + static: doAssert consensusFork >= ConsensusFork.Deneb + + template forkyTransactions: untyped = + when consensusFork >= ConsensusFork.Gloas: + envelope.payload.transactions + else: + blck.body.execution_payload.transactions + template forkyCommitments: untyped = + when consensusFork >= ConsensusFork.Gloas: + envelope.blob_kzg_commitments + else: + blck.body.blob_kzg_commitments var i = 0 - for txBytes in transactions: + for txBytes in forkyTransactions: if txBytes.len == 0 or txBytes[0] != TxEip4844.byte: continue # Only blob transactions may have blobs let tx = ? txBytes.readExecutionTransaction() for vHash in tx.versionedHashes: - if commitments.len <= i: + if forkyCommitments.len <= i: return err("Extra blobs without matching `blob_kzg_commitments`") - if vHash != kzg_commitment_to_versioned_hash(commitments[i]): + if vHash != kzg_commitment_to_versioned_hash(forkyCommitments[i]): return err("Invalid `blob_versioned_hash` at index " & $i) inc i - if i != commitments.len: + if i != forkyCommitments.len: return err("Extra `blob_kzg_commitments` without matching blobs") ok() + +func is_valid_versioned_hashes*(blck: ForkyBeaconBlock): Result[void, string] = + is_valid_versioned_hashes(blck, noEnvelope) From 49828135fdea06d25d80c75c2362c70527888c4f Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:58:24 +0000 Subject: [PATCH 12/15] refactor: cleanup and forky --- .../gossip_processing/block_processor.nim | 102 +++++++++++------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index efd273d4e4..21d542a6b1 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -186,17 +186,16 @@ from ../consensus_object_pools/block_clearance import proc verifySidecars( signedBlock: ForkySignedBeaconBlock, - envelopeOpt: SomeOptEnvelope, + envelope: NoEnvelope | gloas.SignedExecutionPayloadEnvelope, sidecarsOpt: SomeOptSidecars, ): Result[void, VerifierError] = const consensusFork = typeof(signedBlock).kind when consensusFork >= ConsensusFork.Gloas: - if envelopeOpt.isSome and sidecarsOpt.isSome: + if sidecarsOpt.isSome: let - signedEnvelope = envelopeOpt.get() columns = sidecarsOpt.get() - kzgCommits = signedEnvelope.message.blob_kzg_commitments.asSeq + kzgCommits = envelope.message.blob_kzg_commitments.asSeq if columns.len > 0 and kzgCommits.len > 0: for i in 0 ..< columns.len: let r = verify_data_column_sidecar_kzg_proofs(columns[i][]) @@ -247,12 +246,6 @@ proc verifySidecars( ok() -proc verifySidecars( - signedBlock: ForkySignedBeaconBlock, - sidecarsOpt: SomeOptSidecars, -): Result[void, VerifierError] = - verifySidecars(signedBlock, noEnvelope, sidecarsOpt) - proc storeSidecars(self: BlockProcessor, sidecarsOpt: Opt[BlobSidecars]) = if sidecarsOpt.isSome(): for b in sidecarsOpt[]: @@ -283,7 +276,7 @@ proc storeBackfillBlock( const consensusFork = typeof(signedBlock).kind when consensusFork <= ConsensusFork.Fulu: - ?verifySidecars(signedBlock, sidecarsOpt) + ?verifySidecars(signedBlock, noEnvelope, sidecarsOpt) let res = self.consensusManager.dag.addBackfillBlock(signedBlock) @@ -331,44 +324,58 @@ from ../consensus_object_pools/spec_cache import get_attesting_indices proc newExecutionPayload*( elManager: ELManager, blck: SomeForkyBeaconBlock, + envelope: NoEnvelope | gloas.ExecutionPayloadEnvelope, deadline: DeadlineFuture, retry: bool, ): Future[Opt[PayloadExecutionStatus]] {.async: (raises: [CancelledError]).} = - template executionPayload: untyped = blck.body.execution_payload + template forkyExecutionPayload: untyped = + when typeof(blck).kind >= ConsensusFork.Gloas: + envelope.payload + else: + blck.body.execution_payload debug "newPayload: inserting block into execution engine", - executionPayload = shortLog(executionPayload) + executionPayload = shortLog(forkyExecutionPayload) - let payloadStatus = ?await elManager.sendNewPayload(blck, deadline, retry) + let payloadStatus = ?await elManager.sendNewPayload( + blck, envelope, deadline, retry) debug "newPayload: succeeded", - parentHash = executionPayload.parent_hash, - blockHash = executionPayload.block_hash, - blockNumber = executionPayload.block_number, + parentHash = forkyExecutionPayload.parent_hash, + blockHash = forkyExecutionPayload.block_hash, + blockNumber = forkyExecutionPayload.block_number, payloadStatus = payloadStatus Opt.some payloadStatus +debugGloasComment("check calls to this, mostly LC") proc newExecutionPayload*( elManager: ELManager, blck: SomeForkyBeaconBlock ): Future[Opt[PayloadExecutionStatus]] {. async: (raises: [CancelledError], raw: true).} = newExecutionPayload( - elManager, blck, sleepAsync(FORKCHOICEUPDATED_TIMEOUT), true) + elManager, blck, noEnvelope, sleepAsync(FORKCHOICEUPDATED_TIMEOUT), true) proc getExecutionValidity( elManager: ELManager, - blck: bellatrix.SignedBeaconBlock | capella.SignedBeaconBlock | - deneb.SignedBeaconBlock | electra.SignedBeaconBlock | - fulu.SignedBeaconBlock, + blck: ForkySignedBeaconBlock, + envelope: NoEnvelope | gloas.SignedExecutionPayloadEnvelope, deadline: DeadlineFuture, retry: bool, ): Future[Opt[OptimisticStatus]] {.async: (raises: [CancelledError]).} = if not blck.message.is_execution_block: return Opt.some(OptimisticStatus.valid) # vacuously - let status = (await elManager.newExecutionPayload(blck.message, deadline, retry)).valueOr: + const consensusFork = typeof(blck).kind + template forkyEnvelope(): auto = + when consensusFork >= ConsensusFork.Gloas: + envelope.message + else: + envelope + + let status = (await elManager.newExecutionPayload( + blck.message, forkyEnvelope, deadline, retry)).valueOr: return Opt.none(OptimisticStatus) let optimisticStatus = status.to(OptimisticStatus) @@ -378,9 +385,14 @@ proc getExecutionValidity( # former case, they've passed libp2p gossip validation which implies # correct signature for correct proposer,which makes spam expensive, # while for the latter, spam is limited by the request manager. + template forkyExecutionPayload(): auto = + when consensusFork >= ConsensusFork.Gloas: + envelope.message.payload + else: + blck.message.body.execution_payload info "execution payload invalid from EL client newPayload", executionPayloadStatus = status, - executionPayload = shortLog(blck.message.body.execution_payload), + executionPayload = shortLog(forkyExecutionPayload), blck = shortLog(blck) Opt.some(optimisticStatus) @@ -498,7 +510,7 @@ proc onBlockAdded*( proc verifyPayload( self: ref BlockProcessor, signedBlock: ForkySignedBeaconBlock, - envelopeOpt: SomeOptEnvelope, + signedEnvelope: NoEnvelope | gloas.SignedExecutionPayloadEnvelope, ): Result[OptimisticStatus, VerifierError] = const consensusFork = typeof(signedBlock).kind # When the execution layer is not available to verify the payload, we do the @@ -544,11 +556,6 @@ proc verifyPayload( else: ok OptimisticStatus.valid -proc verifyPayload( - self: ref BlockProcessor, signedBlock: ForkySignedBeaconBlock -): Result[OptimisticStatus, VerifierError] = - verifyPayload(self, signedBlock, noEnvelope) - proc enqueueFromDb(self: ref BlockProcessor, root: Eth2Digest) = # TODO This logic can be removed if the database schema is extended # to store non-canonical heads on top of the canonical head and learns to keep @@ -564,8 +571,9 @@ proc enqueueFromDb(self: ref BlockProcessor, root: Eth2Digest) = var sidecarsOk = true let sidecarsOpt = - when consensusFork >= ConsensusFork.Fulu: - debugGloasComment "" + when consensusFork >= ConsensusFork.Gloas: + noSidecars + elif consensusFork == ConsensusFork.Fulu: var data_column_sidecars: fulu.DataColumnSidecars for i in self.dataColumnQuarantine[].custodyColumns: let data_column = fulu.DataColumnSidecar.new() @@ -648,18 +656,30 @@ proc storeBlock( # progress in its own sync. Opt.none(OptimisticStatus) else: - when consensusFork == ConsensusFork.Gloas: - debugGloasComment "need getExecutionValidity on gloas blocks" - Opt.some OptimisticStatus.valid + when consensusFork >= ConsensusFork.Gloas: + # It is mainly for disabling the `updateExecutionHead` call. As we are + # not sure if there is a valid envelope (execution payload), the + # execution head should be updated after we get one and validate it. + Opt.none(OptimisticStatus) elif consensusFork >= ConsensusFork.Bellatrix: func shouldRetry(): bool = not dag.is_optimistic(dag.head.bid) await self.consensusManager.elManager.getExecutionValidity( - signedBlock, deadline, shouldRetry()) + signedBlock, noEnvelope, deadline, shouldRetry()) else: Opt.some(OptimisticStatus.valid) # vacuously - let optimisticStatus = ?(optimisticStatusRes or verifyPayload(self, signedBlock)) + let optimisticStatus = + when consensusFork >= ConsensusFork.Gloas: + # The execution payload validity is not known yet at block time as an + # envelope will be processed after its valid block. So always return + # `notValidated` and skip verifying payload. + # + # TODO may need a new value of `OptimisticStatus` to distinguish between + # not validated and pending? + OptimisticStatus.notValidated + else: + ?(optimisticStatusRes or verifyPayload(self, signedBlock, noEnvelope)) if OptimisticStatus.invalidated == optimisticStatus: return err(VerifierError.Invalid) @@ -667,7 +687,7 @@ proc storeBlock( let newPayloadTick = Moment.now() when consensusFork <= ConsensusFork.Fulu: - ?verifySidecars(signedBlock, sidecarsOpt) + ?verifySidecars(signedBlock, noEnvelope, sidecarsOpt) let blck = ?dag.addHeadBlockWithParent( @@ -857,8 +877,9 @@ proc addBlock*( if sidecarsOpt.isSome: self.dataColumnQuarantine[].put(blockRoot, sidecarsOpt.get) elif sidecarsOpt is Opt[gloas.DataColumnSidecars]: - if sidecarsOpt.isSome: - debugGloasComment "" + # In Gloas, block is enqueued with NoSidecar so this should be + # non-reachable code. + discard elif sidecarsOpt is NoSidecars: discard else: @@ -899,9 +920,10 @@ proc storePayload( let optimisticStatus = ?verifyPayload(self, signedBlock, Opt.some(signedEnvelope)) - ?verifySidecars(signedBlock, Opt.some(signedEnvelope), sidecarsOpt) + ?verifySidecars(signedBlock, signedEnvelope, sidecarsOpt) debugGloasComment("verify and process") + debugGloasComment("update optimistic status") ok() proc enqueuePayload*( From da2d214391026c8f74fb4822a541e1b1e71f15a5 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:00:57 +0000 Subject: [PATCH 13/15] Revert "bump: nim-web3" This reverts commit efd792f5a2834ec4b0bc9431407c48e27545f9c5. --- vendor/nim-web3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-web3 b/vendor/nim-web3 index cf7325af55..141907cd95 160000 --- a/vendor/nim-web3 +++ b/vendor/nim-web3 @@ -1 +1 @@ -Subproject commit cf7325af55df9c1b16c1d84427b871fa4f332759 +Subproject commit 141907cd958d7ee3b554ec94bc9ac7ec692e546b From 16b35d41bbb82c90443acc1480b6838b082e81b9 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:43:41 +0000 Subject: [PATCH 14/15] feat: verify payload and simplify forky logic --- .../gossip_processing/block_processor.nim | 85 ++++++++++++------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index 61fb01dd12..e07f643898 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -191,26 +191,15 @@ proc verifySidecars( ): Result[void, VerifierError] = const consensusFork = typeof(signedBlock).kind - when consensusFork >= ConsensusFork.Gloas: - if sidecarsOpt.isSome: - let - columns = sidecarsOpt.get() - kzgCommits = envelope.message.blob_kzg_commitments.asSeq - if columns.len > 0 and kzgCommits.len > 0: - for i in 0 ..< columns.len: - let r = verify_data_column_sidecar_kzg_proofs(columns[i][]) - if r.isErr(): - debug "data column validation failed", - blockRoot = shortLog(signedBlock.root), - column_sidecar = shortLog(columns[i][]), - blck = shortLog(signedBlock.message), - signature = shortLog(signedBlock.signature), - msg = r.error() - return err(VerifierError.Invalid) - elif consensusFork == ConsensusFork.Fulu: + when consensusFork >= ConsensusFork.Fulu: if sidecarsOpt.isSome: + template forkyKzgCommits(): auto = + when consensusFork >= ConsensusFork.Gloas: + envelope.message.blob_kzg_commitments + else: + signedBlock.message.body.blob_kzg_commitments let columns = sidecarsOpt.get() - let kzgCommits = signedBlock.message.body.blob_kzg_commitments.asSeq + let kzgCommits = forkyKzgCommits.asSeq if columns.len > 0 and kzgCommits.len > 0: for i in 0 ..< columns.len: let r = verify_data_column_sidecar_kzg_proofs(columns[i][]) @@ -517,30 +506,40 @@ proc verifyPayload( # required checks on the CL instead and proceed as if the EL was syncing # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/bellatrix/beacon-chain.md#verify_and_notify_new_payload # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/deneb/beacon-chain.md#modified-verify_and_notify_new_payload - when consensusFork == ConsensusFork.Gloas: - debugGloasComment "no exection payload field for gloas" - ok OptimisticStatus.valid - elif consensusFork >= ConsensusFork.Bellatrix: + when consensusFork >= ConsensusFork.Bellatrix: + # Since Gloas, is_execution_block should always be true. if signedBlock.message.is_execution_block: - template payload(): auto = - signedBlock.message.body.execution_payload + template forkyPayload(): auto = + when consensusFork >= ConsensusFork.Gloas: + signedEnvelope.message.payload + else: + signedBlock.message.body.execution_payload template returnWithError(msg: string, extraMsg = ""): untyped = if extraMsg != "": - debug msg, reason = extraMsg, executionPayload = shortLog(payload) + debug msg, reason = extraMsg, executionPayload = shortLog(forkyPayload) else: - debug msg, executionPayload = shortLog(payload) + debug msg, executionPayload = shortLog(forkyPayload) return err(VerifierError.Invalid) - if payload.transactions.anyIt(it.len == 0): + if forkyPayload.transactions.anyIt(it.len == 0): returnWithError "Execution block contains zero length transactions" - if payload.block_hash != signedBlock.message.compute_execution_block_hash(): + let computedBlockHash = + when consensusFork >= ConsensusFork.Gloas: + signedBlock.message.compute_execution_block_hash(signedEnvelope.message) + else: + signedBlock.message.compute_execution_block_hash() + if forkyPayload.block_hash != computedBlockHash: returnWithError "Execution block hash validation failed" # [New in Deneb:EIP4844] when consensusFork >= ConsensusFork.Deneb: - let blobsRes = signedBlock.message.is_valid_versioned_hashes + let blobsRes = + when consensusFork >= ConsensusFork.Gloas: + signedBlock.message.is_valid_versioned_hashes(signedEnvelope.message) + else: + signedBlock.message.is_valid_versioned_hashes() if blobsRes.isErr: returnWithError "Blob versioned hashes invalid", blobsRes.error else: @@ -918,12 +917,34 @@ proc storePayload( signedEnvelope: gloas.SignedExecutionPayloadEnvelope, sidecarsOpt: Opt[gloas.DataColumnSidecars], ): Future[Result[void, VerifierError]] {.async: (raises: [CancelledError]).} = - let optimisticStatus = - ?verifyPayload(self, signedBlock, Opt.some(signedEnvelope)) + let + dag = self.consensusManager.dag + wallTime = self.getBeaconTime() + wallSlot = wallTime.slotOrZero(dag.timeParams) + deadlineTime = + block: + let slotTime = + (wallSlot + 1).start_beacon_time(dag.timeParams) - chronos.seconds(1) + if slotTime <= wallTime: + chronos.seconds(0) + else: + chronos.nanoseconds((slotTime - wallTime).nanoseconds) + deadline = sleepAsync(deadlineTime) + + let + optimisticStatusRes = + block: + debugGloasComment("handle (maybe)finalized slot") + func shouldRetry(): bool = + not dag.is_optimistic(dag.head.bid) + await self.consensusManager.elManager.getExecutionValidity( + signedBlock, signedEnvelope, deadline, shouldRetry()) + optimisticStatus = + ?(optimisticStatusRes or verifyPayload(self, signedBlock, signedEnvelope)) ?verifySidecars(signedBlock, signedEnvelope, sidecarsOpt) - debugGloasComment("verify and process") + debugGloasComment("process and store") debugGloasComment("update optimistic status") ok() From b1ad20efe58add9e4d21fa323f43a27f64c0e0c8 Mon Sep 17 00:00:00 2001 From: Sam Shum <4080806+ahshum@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:59:32 +0000 Subject: [PATCH 15/15] refactor: rename vars --- beacon_chain/el/el_manager.nim | 34 +++++++++-------- .../gossip_processing/block_processor.nim | 38 ++++++++++--------- beacon_chain/spec/helpers_el.nim | 12 +++--- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/beacon_chain/el/el_manager.nim b/beacon_chain/el/el_manager.nim index 966fec1297..9bed34e368 100644 --- a/beacon_chain/el/el_manager.nim +++ b/beacon_chain/el/el_manager.nim @@ -833,38 +833,42 @@ proc sendNewPayload*( ): Future[Opt[PayloadExecutionStatus]] {.async: (raises: [CancelledError]).} = const consensusFork = typeof(blck).kind - template forkyExecutionPayload(): auto = + template executionPayload(): auto = when consensusFork >= ConsensusFork.Gloas: envelope.payload else: blck.body.execution_payload - template forkyExecutionRequests(): auto = - when consensusFork >= ConsensusFork.Gloas: - envelope.execution_requests - else: - blck.body.execution_requests - template forkyKzgCommitments(): auto = - when consensusFork >= ConsensusFork.Gloas: - envelope.blob_kzg_commitments - elif consensusFork >= ConsensusFork.Deneb: - blck.body.blob_kzg_commitments if m.elConnections.len == 0: info "No execution client configured; cannot process block payloads", - executionPayload = shortLog(forkyExecutionPayload) + executionPayload = shortLog(executionPayload) return Opt.none(PayloadExecutionStatus) let startTime = Moment.now() - payload = forkyExecutionPayload.asEngineExecutionPayload() + payload = executionPayload.asEngineExecutionPayload() when consensusFork >= ConsensusFork.Deneb: let - versioned_hashes = forkyKzgCommitments.asEngineVersionedHashes() + versioned_hashes = + block: + let kzgCommitments = + when consensusFork >= ConsensusFork.Gloas: + envelope.blob_kzg_commitments + elif consensusFork >= ConsensusFork.Deneb: + blck.body.blob_kzg_commitments + kzgCommitments.asEngineVersionedHashes() parent_root = blck.parent_root.to(Hash32) when consensusFork >= ConsensusFork.Electra: - let execution_requests = forkyExecutionRequests.asEngineExecutionRequests() + let execution_requests = + block: + let executionRequests = + when consensusFork >= ConsensusFork.Gloas: + envelope.execution_requests + else: + blck.body.execution_requests + executionRequests.asEngineExecutionRequests() var responseProcessor = ELConsensusViolationDetector.init() diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index e07f643898..c977d08bac 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -193,13 +193,15 @@ proc verifySidecars( when consensusFork >= ConsensusFork.Fulu: if sidecarsOpt.isSome: - template forkyKzgCommits(): auto = - when consensusFork >= ConsensusFork.Gloas: - envelope.message.blob_kzg_commitments - else: - signedBlock.message.body.blob_kzg_commitments let columns = sidecarsOpt.get() - let kzgCommits = forkyKzgCommits.asSeq + let kzgCommits = + block: + let kzgCommits = + when consensusFork >= ConsensusFork.Gloas: + envelope.message.blob_kzg_commitments + else: + signedBlock.message.body.blob_kzg_commitments + kzgCommits.asSeq if columns.len > 0 and kzgCommits.len > 0: for i in 0 ..< columns.len: let r = verify_data_column_sidecar_kzg_proofs(columns[i][]) @@ -317,22 +319,22 @@ proc newExecutionPayload*( deadline: DeadlineFuture, retry: bool, ): Future[Opt[PayloadExecutionStatus]] {.async: (raises: [CancelledError]).} = - template forkyExecutionPayload: untyped = + template executionPayload: untyped = when typeof(blck).kind >= ConsensusFork.Gloas: envelope.payload else: blck.body.execution_payload debug "newPayload: inserting block into execution engine", - executionPayload = shortLog(forkyExecutionPayload) + executionPayload = shortLog(executionPayload) let payloadStatus = ?await elManager.sendNewPayload( blck, envelope, deadline, retry) debug "newPayload: succeeded", - parentHash = forkyExecutionPayload.parent_hash, - blockHash = forkyExecutionPayload.block_hash, - blockNumber = forkyExecutionPayload.block_number, + parentHash = executionPayload.parent_hash, + blockHash = executionPayload.block_hash, + blockNumber = executionPayload.block_number, payloadStatus = payloadStatus Opt.some payloadStatus @@ -374,14 +376,14 @@ proc getExecutionValidity( # former case, they've passed libp2p gossip validation which implies # correct signature for correct proposer,which makes spam expensive, # while for the latter, spam is limited by the request manager. - template forkyExecutionPayload(): auto = + template executionPayload(): auto = when consensusFork >= ConsensusFork.Gloas: envelope.message.payload else: blck.message.body.execution_payload info "execution payload invalid from EL client newPayload", executionPayloadStatus = status, - executionPayload = shortLog(forkyExecutionPayload), + executionPayload = shortLog(executionPayload), blck = shortLog(blck) Opt.some(optimisticStatus) @@ -509,7 +511,7 @@ proc verifyPayload( when consensusFork >= ConsensusFork.Bellatrix: # Since Gloas, is_execution_block should always be true. if signedBlock.message.is_execution_block: - template forkyPayload(): auto = + template paylaod(): auto = when consensusFork >= ConsensusFork.Gloas: signedEnvelope.message.payload else: @@ -517,12 +519,12 @@ proc verifyPayload( template returnWithError(msg: string, extraMsg = ""): untyped = if extraMsg != "": - debug msg, reason = extraMsg, executionPayload = shortLog(forkyPayload) + debug msg, reason = extraMsg, executionPayload = shortLog(paylaod) else: - debug msg, executionPayload = shortLog(forkyPayload) + debug msg, executionPayload = shortLog(paylaod) return err(VerifierError.Invalid) - if forkyPayload.transactions.anyIt(it.len == 0): + if paylaod.transactions.anyIt(it.len == 0): returnWithError "Execution block contains zero length transactions" let computedBlockHash = @@ -530,7 +532,7 @@ proc verifyPayload( signedBlock.message.compute_execution_block_hash(signedEnvelope.message) else: signedBlock.message.compute_execution_block_hash() - if forkyPayload.block_hash != computedBlockHash: + if paylaod.block_hash != computedBlockHash: returnWithError "Execution block hash validation failed" # [New in Deneb:EIP4844] diff --git a/beacon_chain/spec/helpers_el.nim b/beacon_chain/spec/helpers_el.nim index 85e27a3896..03897a1d05 100644 --- a/beacon_chain/spec/helpers_el.nim +++ b/beacon_chain/spec/helpers_el.nim @@ -27,29 +27,29 @@ func is_valid_versioned_hashes*( const consensusFork = typeof(blck).kind static: doAssert consensusFork >= ConsensusFork.Deneb - template forkyTransactions: untyped = + template transactions: untyped = when consensusFork >= ConsensusFork.Gloas: envelope.payload.transactions else: blck.body.execution_payload.transactions - template forkyCommitments: untyped = + template commitments: untyped = when consensusFork >= ConsensusFork.Gloas: envelope.blob_kzg_commitments else: blck.body.blob_kzg_commitments var i = 0 - for txBytes in forkyTransactions: + for txBytes in transactions: if txBytes.len == 0 or txBytes[0] != TxEip4844.byte: continue # Only blob transactions may have blobs let tx = ? txBytes.readExecutionTransaction() for vHash in tx.versionedHashes: - if forkyCommitments.len <= i: + if commitments.len <= i: return err("Extra blobs without matching `blob_kzg_commitments`") - if vHash != kzg_commitment_to_versioned_hash(forkyCommitments[i]): + if vHash != kzg_commitment_to_versioned_hash(commitments[i]): return err("Invalid `blob_versioned_hash` at index " & $i) inc i - if i != forkyCommitments.len: + if i != commitments.len: return err("Extra `blob_kzg_commitments` without matching blobs") ok()