Convert RpcBlock to an enum that indicates availability (#8424)

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Mark Mackey <mark@sigmaprime.io>

Co-Authored-By: Eitan Seri-Levi <eserilev@gmail.com>

Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Eitan Seri-Levi
2026-01-27 21:59:32 -08:00
committed by GitHub
parent c4409cdf28
commit f7b5c7ee3f
23 changed files with 1368 additions and 579 deletions

View File

@@ -1,9 +1,11 @@
#![cfg(not(debug_assertions))]
use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock};
use beacon_chain::data_availability_checker::{AvailabilityCheckError, AvailableBlockData};
use beacon_chain::data_column_verification::CustodyDataColumn;
use beacon_chain::{
AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutionPendingBlock,
WhenSlotSkipped,
custody_context::NodeCustodyType,
test_utils::{
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec,
@@ -11,7 +13,7 @@ use beacon_chain::{
};
use beacon_chain::{
BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock,
InvalidSignature, NotifyExecutionLayer,
InvalidSignature, NotifyExecutionLayer, signature_verify_chain_segment,
};
use bls::{AggregateSignature, Keypair, Signature};
use fixed_bytes::FixedBytesExtended;
@@ -39,6 +41,7 @@ const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGT
static KEYPAIRS: LazyLock<Vec<Keypair>> =
LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT));
// TODO(#8633): Delete this unnecessary enum and refactor this file to use `AvailableBlockData` instead.
enum DataSidecars<E: EthSpec> {
Blobs(BlobSidecarList<E>),
DataColumns(Vec<CustodyDataColumn<E>>),
@@ -130,32 +133,65 @@ fn get_harness(
harness
}
fn chain_segment_blocks(
fn chain_segment_blocks<T>(
chain_segment: &[BeaconSnapshot<E>],
chain_segment_sidecars: &[Option<DataSidecars<E>>],
) -> Vec<RpcBlock<E>> {
chain: Arc<BeaconChain<T>>,
) -> Vec<RpcBlock<E>>
where
T: BeaconChainTypes<EthSpec = E>,
{
chain_segment
.iter()
.zip(chain_segment_sidecars.iter())
.map(|(snapshot, data_sidecars)| {
let block = snapshot.beacon_block.clone();
build_rpc_block(block, data_sidecars)
build_rpc_block(block, data_sidecars, chain.clone())
})
.collect()
}
fn build_rpc_block(
fn build_rpc_block<T>(
block: Arc<SignedBeaconBlock<E>>,
data_sidecars: &Option<DataSidecars<E>>,
) -> RpcBlock<E> {
chain: Arc<BeaconChain<T>>,
) -> RpcBlock<E>
where
T: BeaconChainTypes<EthSpec = E>,
{
match data_sidecars {
Some(DataSidecars::Blobs(blobs)) => {
RpcBlock::new(None, block, Some(blobs.clone())).unwrap()
let block_data = AvailableBlockData::new_with_blobs(blobs.clone());
RpcBlock::new(
block,
Some(block_data),
&chain.data_availability_checker,
chain.spec.clone(),
)
.unwrap()
}
Some(DataSidecars::DataColumns(columns)) => {
RpcBlock::new_with_custody_columns(None, block, columns.clone()).unwrap()
let block_data = AvailableBlockData::new_with_data_columns(
columns
.iter()
.map(|c| c.as_data_column().clone())
.collect::<Vec<_>>(),
);
RpcBlock::new(
block,
Some(block_data),
&chain.data_availability_checker,
chain.spec.clone(),
)
.unwrap()
}
None => RpcBlock::new_without_blobs(None, block),
None => RpcBlock::new(
block,
Some(AvailableBlockData::NoData),
&chain.data_availability_checker,
chain.spec.clone(),
)
.unwrap(),
}
}
@@ -266,9 +302,10 @@ fn update_data_column_signed_header<E: EthSpec>(
async fn chain_segment_full_segment() {
let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode);
let (chain_segment, chain_segment_blobs) = get_chain_segment().await;
let blocks: Vec<RpcBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs)
.into_iter()
.collect();
let blocks: Vec<RpcBlock<E>> =
chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone())
.into_iter()
.collect();
harness
.chain
@@ -302,9 +339,11 @@ async fn chain_segment_full_segment() {
#[tokio::test]
async fn chain_segment_varying_chunk_size() {
let (chain_segment, chain_segment_blobs) = get_chain_segment().await;
let blocks: Vec<RpcBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs)
.into_iter()
.collect();
let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode);
let blocks: Vec<RpcBlock<E>> =
chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone())
.into_iter()
.collect();
for chunk_size in &[1, 2, 31, 32, 33] {
let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode);
@@ -346,9 +385,10 @@ async fn chain_segment_non_linear_parent_roots() {
/*
* Test with a block removed.
*/
let mut blocks: Vec<RpcBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs)
.into_iter()
.collect();
let mut blocks: Vec<RpcBlock<E>> =
chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone())
.into_iter()
.collect();
blocks.remove(2);
assert!(
@@ -366,16 +406,21 @@ async fn chain_segment_non_linear_parent_roots() {
/*
* Test with a modified parent root.
*/
let mut blocks: Vec<RpcBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs)
.into_iter()
.collect();
let mut blocks: Vec<RpcBlock<E>> =
chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone())
.into_iter()
.collect();
let (mut block, signature) = blocks[3].as_block().clone().deconstruct();
*block.parent_root_mut() = Hash256::zero();
blocks[3] = RpcBlock::new_without_blobs(
None,
blocks[3] = RpcBlock::new(
Arc::new(SignedBeaconBlock::from_block(block, signature)),
);
blocks[3].block_data().cloned(),
&harness.chain.data_availability_checker,
harness.spec.clone(),
)
.unwrap();
assert!(
matches!(
@@ -403,15 +448,19 @@ async fn chain_segment_non_linear_slots() {
* Test where a child is lower than the parent.
*/
let mut blocks: Vec<RpcBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs)
.into_iter()
.collect();
let mut blocks: Vec<RpcBlock<E>> =
chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone())
.into_iter()
.collect();
let (mut block, signature) = blocks[3].as_block().clone().deconstruct();
*block.slot_mut() = Slot::new(0);
blocks[3] = RpcBlock::new_without_blobs(
None,
blocks[3] = RpcBlock::new(
Arc::new(SignedBeaconBlock::from_block(block, signature)),
);
blocks[3].block_data().cloned(),
&harness.chain.data_availability_checker,
harness.spec.clone(),
)
.unwrap();
assert!(
matches!(
@@ -429,15 +478,19 @@ async fn chain_segment_non_linear_slots() {
* Test where a child is equal to the parent.
*/
let mut blocks: Vec<RpcBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs)
.into_iter()
.collect();
let mut blocks: Vec<RpcBlock<E>> =
chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone())
.into_iter()
.collect();
let (mut block, signature) = blocks[3].as_block().clone().deconstruct();
*block.slot_mut() = blocks[2].slot();
blocks[3] = RpcBlock::new_without_blobs(
None,
blocks[3] = RpcBlock::new(
Arc::new(SignedBeaconBlock::from_block(block, signature)),
);
blocks[3].block_data().cloned(),
&harness.chain.data_availability_checker,
harness.chain.spec.clone(),
)
.unwrap();
assert!(
matches!(
@@ -463,7 +516,9 @@ async fn assert_invalid_signature(
let blocks: Vec<RpcBlock<E>> = snapshots
.iter()
.zip(chain_segment_blobs.iter())
.map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs))
.map(|(snapshot, blobs)| {
build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone())
})
.collect();
// Ensure the block will be rejected if imported in a chain segment.
@@ -488,7 +543,9 @@ async fn assert_invalid_signature(
.iter()
.take(block_index)
.zip(chain_segment_blobs.iter())
.map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs))
.map(|(snapshot, blobs)| {
build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone())
})
.collect();
// We don't care if this fails, we just call this to ensure that all prior blocks have been
// imported prior to this test.
@@ -505,6 +562,7 @@ async fn assert_invalid_signature(
build_rpc_block(
snapshots[block_index].beacon_block.clone(),
&chain_segment_blobs[block_index],
harness.chain.clone(),
),
NotifyExecutionLayer::Yes,
BlockImportSource::Lookup,
@@ -562,7 +620,9 @@ async fn invalid_signature_gossip_block() {
.iter()
.take(block_index)
.zip(chain_segment_blobs.iter())
.map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs))
.map(|(snapshot, blobs)| {
build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone())
})
.collect();
harness
.chain
@@ -571,7 +631,13 @@ async fn invalid_signature_gossip_block() {
.into_block_error()
.expect("should import all blocks prior to the one being tested");
let signed_block = SignedBeaconBlock::from_block(block, junk_signature());
let rpc_block = RpcBlock::new_without_blobs(None, Arc::new(signed_block));
let rpc_block = RpcBlock::new(
Arc::new(signed_block),
None,
&harness.chain.data_availability_checker,
harness.spec.clone(),
)
.unwrap();
let process_res = harness
.chain
.process_block(
@@ -613,7 +679,9 @@ async fn invalid_signature_block_proposal() {
let blocks: Vec<RpcBlock<E>> = snapshots
.iter()
.zip(chain_segment_blobs.iter())
.map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs))
.map(|(snapshot, blobs)| {
build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone())
})
.collect::<Vec<_>>();
// Ensure the block will be rejected if imported in a chain segment.
let process_res = harness
@@ -930,7 +998,9 @@ async fn invalid_signature_deposit() {
let blocks: Vec<RpcBlock<E>> = snapshots
.iter()
.zip(chain_segment_blobs.iter())
.map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs))
.map(|(snapshot, blobs)| {
build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone())
})
.collect();
assert!(
!matches!(
@@ -1572,7 +1642,13 @@ async fn add_base_block_to_altair_chain() {
));
// Ensure that it would be impossible to import via `BeaconChain::process_block`.
let base_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(base_block.clone()));
let base_rpc_block = RpcBlock::new(
Arc::new(base_block.clone()),
None,
&harness.chain.data_availability_checker,
harness.spec.clone(),
)
.unwrap();
assert!(matches!(
harness
.chain
@@ -1596,7 +1672,15 @@ async fn add_base_block_to_altair_chain() {
harness
.chain
.process_chain_segment(
vec![RpcBlock::new_without_blobs(None, Arc::new(base_block))],
vec![
RpcBlock::new(
Arc::new(base_block),
None,
&harness.chain.data_availability_checker,
harness.spec.clone()
)
.unwrap()
],
NotifyExecutionLayer::Yes,
)
.await,
@@ -1709,7 +1793,13 @@ async fn add_altair_block_to_base_chain() {
));
// Ensure that it would be impossible to import via `BeaconChain::process_block`.
let altair_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(altair_block.clone()));
let altair_rpc_block = RpcBlock::new(
Arc::new(altair_block.clone()),
None,
&harness.chain.data_availability_checker,
harness.spec.clone(),
)
.unwrap();
assert!(matches!(
harness
.chain
@@ -1733,7 +1823,15 @@ async fn add_altair_block_to_base_chain() {
harness
.chain
.process_chain_segment(
vec![RpcBlock::new_without_blobs(None, Arc::new(altair_block))],
vec![
RpcBlock::new(
Arc::new(altair_block),
None,
&harness.chain.data_availability_checker,
harness.spec.clone()
)
.unwrap()
],
NotifyExecutionLayer::Yes
)
.await,
@@ -1796,7 +1894,13 @@ async fn import_duplicate_block_unrealized_justification() {
// Create two verified variants of the block, representing the same block being processed in
// parallel.
let notify_execution_layer = NotifyExecutionLayer::Yes;
let rpc_block = RpcBlock::new_without_blobs(Some(block_root), block.clone());
let rpc_block = RpcBlock::new(
block.clone(),
Some(AvailableBlockData::NoData),
&harness.chain.data_availability_checker,
harness.spec.clone(),
)
.unwrap();
let verified_block1 = rpc_block
.clone()
.into_execution_pending_block(block_root, chain, notify_execution_layer)
@@ -1870,3 +1974,277 @@ async fn import_execution_pending_block<T: BeaconChainTypes>(
}
}
}
// Test that `signature_verify_chain_segment` errors with a chain segment of mixed `FullyAvailable`
// and `BlockOnly` RpcBlocks. This situation should never happen in production.
#[tokio::test]
async fn signature_verify_mixed_rpc_block_variants() {
let (snapshots, data_sidecars) = get_chain_segment().await;
let snapshots: Vec<_> = snapshots.into_iter().take(10).collect();
let data_sidecars: Vec<_> = data_sidecars.into_iter().take(10).collect();
let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode);
let mut chain_segment = Vec::new();
for (i, (snapshot, blobs)) in snapshots.iter().zip(data_sidecars.iter()).enumerate() {
let block = snapshot.beacon_block.clone();
let block_root = snapshot.beacon_block_root;
// Alternate between FullyAvailable and BlockOnly
let rpc_block = if i % 2 == 0 {
// FullyAvailable - with blobs/columns if needed
build_rpc_block(block, blobs, harness.chain.clone())
} else {
// BlockOnly - no data
RpcBlock::new(
block,
None,
&harness.chain.data_availability_checker,
harness.chain.spec.clone(),
)
.unwrap()
};
chain_segment.push((block_root, rpc_block));
}
// This should error because `signature_verify_chain_segment` expects a list
// of `RpcBlock::FullyAvailable`.
assert!(signature_verify_chain_segment(chain_segment.clone(), &harness.chain).is_err());
}
// Test that RpcBlock::new() rejects blocks when blob count doesn't match expected.
#[tokio::test]
async fn rpc_block_construction_fails_with_wrong_blob_count() {
let spec = test_spec::<E>();
if !spec.fork_name_at_slot::<E>(Slot::new(0)).deneb_enabled()
|| spec.fork_name_at_slot::<E>(Slot::new(0)).fulu_enabled()
{
return;
}
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.spec(spec.into())
.keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec())
.node_custody_type(NodeCustodyType::Fullnode)
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
harness.advance_slot();
harness
.extend_chain(
E::slots_per_epoch() as usize * 2,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Get a block with blobs
for slot in 1..=5 {
let root = harness
.chain
.block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None)
.unwrap()
.unwrap();
let block = harness.chain.get_block(&root).await.unwrap().unwrap();
if let Ok(commitments) = block.message().body().blob_kzg_commitments()
&& !commitments.is_empty()
{
let blobs = harness.chain.get_blobs(&root).unwrap().blobs().unwrap();
// Create AvailableBlockData with wrong number of blobs (remove one)
let mut wrong_blobs_vec: Vec<_> = blobs.iter().cloned().collect();
wrong_blobs_vec.pop();
let max_blobs = harness.spec.max_blobs_per_block(block.epoch()) as usize;
let wrong_blobs = ssz_types::RuntimeVariableList::new(wrong_blobs_vec, max_blobs)
.expect("should create BlobSidecarList");
let block_data = AvailableBlockData::new_with_blobs(wrong_blobs);
// Try to create RpcBlock with wrong blob count
let result = RpcBlock::new(
Arc::new(block),
Some(block_data),
&harness.chain.data_availability_checker,
harness.chain.spec.clone(),
);
// Should fail with MissingBlobs
assert!(
matches!(result, Err(AvailabilityCheckError::MissingBlobs)),
"RpcBlock construction should fail with wrong blob count, got: {:?}",
result
);
return;
}
}
panic!("No block with blobs found");
}
// Test that RpcBlock::new() rejects blocks when custody columns are incomplete.
#[tokio::test]
async fn rpc_block_rejects_missing_custody_columns() {
let spec = test_spec::<E>();
if !spec.fork_name_at_slot::<E>(Slot::new(0)).fulu_enabled() {
return;
}
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.spec(spec.into())
.keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec())
.node_custody_type(NodeCustodyType::Fullnode)
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
harness.advance_slot();
// Extend chain to create some blocks with data columns
harness
.extend_chain(
5,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Get a block with data columns
for slot in 1..=5 {
let root = harness
.chain
.block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None)
.unwrap()
.unwrap();
let block = harness.chain.get_block(&root).await.unwrap().unwrap();
if let Ok(commitments) = block.message().body().blob_kzg_commitments()
&& !commitments.is_empty()
{
let fork_name = harness.chain.spec.fork_name_at_slot::<E>(block.slot());
let columns = harness
.chain
.get_data_columns(&root, fork_name)
.unwrap()
.unwrap();
if columns.len() > 1 {
// Create AvailableBlockData with incomplete columns (remove one)
let mut incomplete_columns: Vec<_> = columns.to_vec();
incomplete_columns.pop();
let block_data = AvailableBlockData::new_with_data_columns(incomplete_columns);
// Try to create RpcBlock with incomplete custody columns
let result = RpcBlock::new(
Arc::new(block),
Some(block_data),
&harness.chain.data_availability_checker,
harness.chain.spec.clone(),
);
// Should fail with MissingCustodyColumns
assert!(
matches!(result, Err(AvailabilityCheckError::MissingCustodyColumns)),
"RpcBlock construction should fail with missing custody columns, got: {:?}",
result
);
return;
}
}
}
panic!("No block with data columns found");
}
// Test that RpcBlock::new() allows construction past the data availability boundary.
// When a block is past the DA boundary, we should be able to construct an RpcBlock
// with NoData even if the block has blob commitments, since columns are not expected.
#[tokio::test]
async fn rpc_block_allows_construction_past_da_boundary() {
let spec = test_spec::<E>();
if !spec.fork_name_at_slot::<E>(Slot::new(0)).fulu_enabled() {
return;
}
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.spec(spec.into())
.keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec())
.node_custody_type(NodeCustodyType::Fullnode)
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
harness.advance_slot();
// Extend chain to create some blocks with blob commitments
harness
.extend_chain(
5,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Find a block with blob commitments
for slot in 1..=5 {
let root = harness
.chain
.block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None)
.unwrap()
.unwrap();
let block = harness.chain.get_block(&root).await.unwrap().unwrap();
if let Ok(commitments) = block.message().body().blob_kzg_commitments()
&& !commitments.is_empty()
{
let block_epoch = block.epoch();
// Advance the slot clock far into the future, past the DA boundary
// For a block to be past the DA boundary:
// current_epoch - min_epochs_for_data_column_sidecars_requests > block_epoch
let min_epochs_for_data = harness.spec.min_epochs_for_data_column_sidecars_requests;
let future_epoch = block_epoch + min_epochs_for_data + 10;
let future_slot = future_epoch.start_slot(E::slots_per_epoch());
harness.chain.slot_clock.set_slot(future_slot.as_u64());
// Now verify the block is past the DA boundary
let da_boundary = harness
.chain
.data_availability_checker
.data_availability_boundary()
.expect("DA boundary should be set");
assert!(
block_epoch < da_boundary,
"Block should be past the DA boundary. Block epoch: {}, DA boundary: {}",
block_epoch,
da_boundary
);
// Try to create RpcBlock with NoData for a block past DA boundary
// This should succeed since columns are not expected for blocks past DA boundary
let result = RpcBlock::new(
Arc::new(block),
Some(AvailableBlockData::NoData),
&harness.chain.data_availability_checker,
harness.chain.spec.clone(),
);
assert!(
result.is_ok(),
"RpcBlock construction should succeed for blocks past DA boundary, got: {:?}",
result
);
return;
}
}
panic!("No block with blob commitments found");
}