mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-29 19:04:27 +00:00
Handle payload-present attestations before the payload is seen (gloas) A gloas beacon_attestation with index == 1 claims a past block's payload is already present. If we haven't seen that block's payload envelope yet, we shouldn't reject it the envelope may just be in flight. So instead we IGNORE it (new AttnError::UnknownPayloadEnvelope), ask sync to fetch the envelope, and park the attestation in the reprocess queue. When the envelope is imported, the parked attestations are released and re-verified. The envelope lookup itself is stubbed here and wired up in #9155 or a follow up PR Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>
282 lines
9.9 KiB
Rust
282 lines
9.9 KiB
Rust
#![cfg(not(debug_assertions))]
|
|
|
|
use beacon_chain::custody_context::NodeCustodyType;
|
|
use beacon_chain::test_utils::{
|
|
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType,
|
|
generate_data_column_sidecars_from_block, test_spec,
|
|
};
|
|
use beacon_chain::{
|
|
AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer,
|
|
block_verification_types::{AsBlock, LookupBlock},
|
|
};
|
|
use bls::{Keypair, Signature};
|
|
use logging::create_test_tracing_subscriber;
|
|
use std::sync::{Arc, LazyLock};
|
|
use types::*;
|
|
|
|
type E = MainnetEthSpec;
|
|
|
|
// >= 32 validators required for Gloas genesis with MainnetEthSpec (32 slots/epoch).
|
|
const VALIDATOR_COUNT: usize = 32;
|
|
|
|
/// A cached set of keys.
|
|
static KEYPAIRS: LazyLock<Vec<Keypair>> =
|
|
LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT));
|
|
|
|
fn get_harness(
|
|
validator_count: usize,
|
|
spec: Arc<ChainSpec>,
|
|
node_custody_type: NodeCustodyType,
|
|
) -> BeaconChainHarness<EphemeralHarnessType<E>> {
|
|
create_test_tracing_subscriber();
|
|
let harness = BeaconChainHarness::builder(MainnetEthSpec)
|
|
.spec(spec)
|
|
.chain_config(ChainConfig {
|
|
archive: true,
|
|
..ChainConfig::default()
|
|
})
|
|
.keypairs(KEYPAIRS[0..validator_count].to_vec())
|
|
.node_custody_type(node_custody_type)
|
|
.fresh_ephemeral_store()
|
|
.mock_execution_layer()
|
|
.build();
|
|
|
|
harness.advance_slot();
|
|
|
|
harness
|
|
}
|
|
|
|
// Regression test for https://github.com/sigp/lighthouse/issues/7650
|
|
#[tokio::test]
|
|
async fn rpc_columns_with_invalid_header_signature() {
|
|
let spec = Arc::new(test_spec::<E>());
|
|
|
|
// Only run this test if columns are enabled.
|
|
// TODO(gloas): Gloas blocks don't have blob_kzg_commitments — blobs are in the envelope.
|
|
if !spec.is_fulu_scheduled() || spec.is_gloas_scheduled() {
|
|
return;
|
|
}
|
|
|
|
let harness = get_harness(VALIDATOR_COUNT, spec, NodeCustodyType::Supernode);
|
|
|
|
let num_blocks = E::slots_per_epoch() as usize;
|
|
|
|
// Add some chain depth.
|
|
harness
|
|
.extend_chain(
|
|
num_blocks,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::AllValidators,
|
|
)
|
|
.await;
|
|
|
|
// Produce a block with blobs.
|
|
harness.execution_block_generator().set_min_blob_count(1);
|
|
let head_state = harness.get_current_state();
|
|
let slot = head_state.slot() + 1;
|
|
let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await;
|
|
let (_, blobs) = opt_blobs.unwrap();
|
|
assert!(!blobs.is_empty());
|
|
let block_root = signed_block.canonical_root();
|
|
|
|
// Process the block without blobs so that it doesn't become available.
|
|
harness.advance_slot();
|
|
let availability = harness
|
|
.chain
|
|
.process_block(
|
|
block_root,
|
|
LookupBlock::new(signed_block.clone()),
|
|
NotifyExecutionLayer::Yes,
|
|
BlockImportSource::Lookup,
|
|
|| Ok(()),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
availability,
|
|
AvailabilityProcessingStatus::MissingComponents(slot, block_root)
|
|
);
|
|
|
|
// Build blob sidecars with invalid signatures in the block header.
|
|
let mut corrupt_block = (*signed_block).clone();
|
|
*corrupt_block.signature_mut() = Signature::infinity().unwrap();
|
|
|
|
let data_column_sidecars =
|
|
generate_data_column_sidecars_from_block(&corrupt_block, &harness.chain.spec);
|
|
|
|
let err = harness
|
|
.chain
|
|
.process_rpc_custody_columns(data_column_sidecars)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(
|
|
err,
|
|
BlockError::InvalidSignature(InvalidSignature::ProposerSignature)
|
|
));
|
|
}
|
|
|
|
/// Test that Gloas block production caches blobs alongside the envelope, and that
|
|
/// data columns can be built from those cached blobs.
|
|
#[tokio::test]
|
|
async fn gloas_envelope_blobs_produce_valid_columns() {
|
|
let spec = Arc::new(test_spec::<E>());
|
|
if !spec.is_gloas_scheduled() {
|
|
return;
|
|
}
|
|
|
|
let harness = get_harness(VALIDATOR_COUNT, spec.clone(), NodeCustodyType::Supernode);
|
|
harness.execution_block_generator().set_min_blob_count(1);
|
|
|
|
// Build some chain depth.
|
|
let num_blocks = E::slots_per_epoch() as usize;
|
|
harness
|
|
.extend_chain(
|
|
num_blocks,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::AllValidators,
|
|
)
|
|
.await;
|
|
|
|
harness.advance_slot();
|
|
let slot = harness.get_current_slot();
|
|
|
|
// Produce a Gloas block via the harness. This caches envelope + blobs.
|
|
let state = harness.get_current_state();
|
|
let (block_contents, opt_envelope, _post_state) =
|
|
harness.make_block_with_envelope(state, slot).await;
|
|
let signed_block = &block_contents.0;
|
|
|
|
assert!(
|
|
opt_envelope.is_some(),
|
|
"Gloas block production should produce an envelope"
|
|
);
|
|
|
|
// Verify the block has blob commitments in the bid.
|
|
let bid = signed_block
|
|
.message()
|
|
.body()
|
|
.signed_execution_payload_bid()
|
|
.expect("Gloas block should have a payload bid");
|
|
assert!(
|
|
!bid.message.blob_kzg_commitments.is_empty(),
|
|
"Block should have blob KZG commitments"
|
|
);
|
|
|
|
// Generate data columns from the block (using test fixtures, same as the harness does).
|
|
let data_column_sidecars =
|
|
generate_data_column_sidecars_from_block(signed_block, &harness.chain.spec);
|
|
assert_eq!(
|
|
data_column_sidecars.len(),
|
|
E::number_of_columns(),
|
|
"Should produce the correct number of data columns"
|
|
);
|
|
|
|
// Verify all columns are Gloas-format.
|
|
for col in &data_column_sidecars {
|
|
assert!(
|
|
col.as_gloas().is_ok(),
|
|
"Data column sidecar should be Gloas variant"
|
|
);
|
|
let gloas_col = col.as_gloas().expect("should be Gloas sidecar");
|
|
assert_eq!(gloas_col.beacon_block_root, signed_block.canonical_root());
|
|
assert_eq!(gloas_col.slot, slot);
|
|
}
|
|
|
|
// End-to-end DA flow (process_block → process_envelope → process_rpc_custody_columns)
|
|
// is not exercised here: Gloas blocks are not gated on columns at block-import time
|
|
// and the envelope/column gating belongs in a dedicated test once the DA path matures.
|
|
}
|
|
|
|
// Regression test for verify_header_signature bug: it uses head_fork() which is wrong for fork blocks
|
|
#[tokio::test]
|
|
async fn verify_header_signature_fork_block_bug() {
|
|
// Create a spec with all forks enabled at genesis except Fulu which is at epoch 1
|
|
// This allows us to easily create the scenario where the head is at Electra
|
|
// but we're trying to verify a block from Fulu epoch
|
|
let mut spec = test_spec::<E>();
|
|
|
|
// Only run this test for FORK_NAME=fulu.
|
|
if !spec.is_fulu_scheduled() || spec.is_gloas_scheduled() {
|
|
return;
|
|
}
|
|
|
|
spec.altair_fork_epoch = Some(Epoch::new(0));
|
|
spec.bellatrix_fork_epoch = Some(Epoch::new(0));
|
|
spec.capella_fork_epoch = Some(Epoch::new(0));
|
|
spec.deneb_fork_epoch = Some(Epoch::new(0));
|
|
spec.electra_fork_epoch = Some(Epoch::new(0));
|
|
let fulu_fork_epoch = Epoch::new(1);
|
|
spec.fulu_fork_epoch = Some(fulu_fork_epoch);
|
|
|
|
let spec = Arc::new(spec);
|
|
let harness = get_harness(VALIDATOR_COUNT, spec.clone(), NodeCustodyType::Supernode);
|
|
harness.execution_block_generator().set_min_blob_count(1);
|
|
|
|
// Add some blocks in epoch 0 (Electra)
|
|
harness
|
|
.extend_chain(
|
|
E::slots_per_epoch() as usize - 1,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::AllValidators,
|
|
)
|
|
.await;
|
|
|
|
// Verify we're still in epoch 0 (Electra)
|
|
let pre_fork_state = harness.get_current_state();
|
|
assert_eq!(pre_fork_state.current_epoch(), Epoch::new(0));
|
|
assert!(matches!(pre_fork_state, BeaconState::Electra(_)));
|
|
|
|
// Now produce a block at the first slot of epoch 1 (Fulu fork).
|
|
// make_block will advance the state which will trigger the Electra->Fulu upgrade.
|
|
let fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch());
|
|
let ((signed_block, opt_blobs), _state_root) =
|
|
harness.make_block(pre_fork_state.clone(), fork_slot).await;
|
|
let (_, blobs) = opt_blobs.expect("Blobs should be present");
|
|
assert!(!blobs.is_empty(), "Block should have blobs");
|
|
let block_root = signed_block.canonical_root();
|
|
|
|
// Process the block WITHOUT blobs to make it unavailable.
|
|
// The block will be accepted but won't become the head because it's not fully available.
|
|
// This keeps the head at the pre-fork state (Electra).
|
|
harness.advance_slot();
|
|
let availability = harness
|
|
.chain
|
|
.process_block(
|
|
block_root,
|
|
LookupBlock::new(signed_block.clone()),
|
|
NotifyExecutionLayer::Yes,
|
|
BlockImportSource::Lookup,
|
|
|| Ok(()),
|
|
)
|
|
.await
|
|
.expect("Block should be processed");
|
|
assert_eq!(
|
|
availability,
|
|
AvailabilityProcessingStatus::MissingComponents(fork_slot, block_root),
|
|
"Block should be pending availability"
|
|
);
|
|
|
|
// The head should still be in epoch 0 (Electra) because the fork block isn't available
|
|
let current_head_state = harness.get_current_state();
|
|
assert_eq!(current_head_state.current_epoch(), Epoch::new(0));
|
|
assert!(matches!(current_head_state, BeaconState::Electra(_)));
|
|
|
|
// Now try to process columns for the fork block.
|
|
// The bug: verify_header_signature previously used head_fork() which fetched the fork from
|
|
// the head state (still Electra fork), but the block was signed with the Fulu fork version.
|
|
// This caused an incorrect signature verification failure.
|
|
let data_column_sidecars =
|
|
generate_data_column_sidecars_from_block(&signed_block, &harness.chain.spec);
|
|
|
|
// Now that the bug is fixed, the block should import.
|
|
let status = harness
|
|
.chain
|
|
.process_rpc_custody_columns(data_column_sidecars)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
status,
|
|
AvailabilityProcessingStatus::Imported(signed_block.slot(), block_root)
|
|
);
|
|
}
|