From 5a93cbdf778f298e67d38635e73e459a202f6e0c Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 7 May 2026 12:50:19 +0000 Subject: [PATCH] debug + fix: store envelope for DuplicateFullyImported blocks After checkpoint sync, blocks between the finalized checkpoint and head are already in fork choice but their envelopes aren't in the store. When range sync downloads these blocks, they get skipped as DuplicateFullyImported without persisting the envelope. The next new block then fails load_parent because it can't find its parent's envelope. Fix: persist the envelope in the DuplicateFullyImported path, same as the existing WouldRevertFinalizedSlot path. --- beacon_node/beacon_chain/src/beacon_chain.rs | 34 ++++++++++++++++++- .../beacon_chain/src/block_verification.rs | 13 ++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4a148596a6..e76c258e0c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3060,7 +3060,34 @@ impl BeaconChain { // // Note that `check_block_relevancy` is incapable of returning // `DuplicateImportStatusUnknown` so we don't need to handle that case here. - Err(BlockError::DuplicateFullyImported(_)) => continue, + Err(BlockError::DuplicateFullyImported(_)) => { + debug!( + block_root = %block_root, + slot = %block.slot(), + "Skipping DuplicateFullyImported block in chain segment", + ); + // For Gloas blocks, persist the envelope even though we're + // skipping the block. After checkpoint sync, blocks between + // the finalized checkpoint and the head are already in fork + // choice but their envelopes aren't in the store. + if let RangeSyncBlock::Gloas { + envelope: Some(ref available_envelope), + .. + } = block + { + let (signed_envelope, _columns) = available_envelope.clone().deconstruct(); + if let Err(e) = self + .store + .put_payload_envelope(&block_root, &signed_envelope) + { + return Err(Box::new(ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::BeaconChainError(Box::new(e.into())), + })); + } + } + continue; + } // If the block is the genesis block, simply ignore this block. Err(BlockError::GenesisBlock) => continue, // If the block is is for a finalized slot, simply ignore this block. @@ -3077,6 +3104,11 @@ impl BeaconChain { // However, we will potentially get a `ParentUnknown` on a later block. The sync // protocol will need to ensure this is handled gracefully. Err(BlockError::WouldRevertFinalizedSlot { .. }) => { + debug!( + block_root = %block_root, + slot = %block.slot(), + "Skipping WouldRevertFinalizedSlot block in chain segment", + ); // For Gloas blocks, persist the envelope even though we're skipping // the block. This is needed after checkpoint sync: the checkpoint // block's envelope must be in the store so that `load_parent` can diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 6417493605..a5fc59a828 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2054,10 +2054,15 @@ fn load_parent>( .gloas_enabled() && parent_block.slot() > finalized_slot { - let _envelope = chain - .store - .get_payload_envelope(&root)? - .ok_or(BlockError::ParentEnvelopeUnknown { parent_root: root })?; + if chain.store.get_payload_envelope(&root)?.is_none() { + debug!( + parent_root = %root, + parent_slot = %parent_block.slot(), + %finalized_slot, + "load_parent: parent envelope not in store", + ); + return Err(BlockError::ParentEnvelopeUnknown { parent_root: root }); + } } // Load the parent block's state from the database, returning an error if it is not found.