diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 303f12c8ec..1be7fa0037 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -566,16 +566,21 @@ mod tests { NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_da_checker, test_spec, }; + use bls::Signature; use lighthouse_network::{ PeerId, service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, - DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, RangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, + PayloadEnvelopesByRangeRequestId, RangeRequestId, }, }; use std::{collections::HashMap, sync::Arc}; use tracing::Span; - use types::{Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; + use types::{ + ChainSpec, DataColumnSidecarList, Epoch, ExecutionPayloadEnvelope, ForkName, + MinimalEthSpec as E, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + }; fn components_id() -> ComponentsByRangeRequestId { ComponentsByRangeRequestId { @@ -601,6 +606,15 @@ mod tests { } } + fn payloads_id( + parent_request_id: ComponentsByRangeRequestId, + ) -> PayloadEnvelopesByRangeRequestId { + PayloadEnvelopesByRangeRequestId { + id: 1, + parent_request_id, + } + } + fn columns_id( id: Id, parent_request_id: DataColumnsByRangeRequester, @@ -618,6 +632,88 @@ mod tests { info.responses(da_checker, spec).is_some() } + fn gloas_spec() -> ChainSpec { + let mut spec = test_spec::(); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec + } + + fn matching_envelope(block: &SignedBeaconBlock) -> Arc> { + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .expect("Gloas block should have payload bid") + .message; + let mut envelope = SignedExecutionPayloadEnvelope { + message: ExecutionPayloadEnvelope::empty(), + signature: Signature::empty(), + }; + envelope.message.beacon_block_root = block.canonical_root(); + envelope.message.parent_beacon_block_root = block.parent_root(); + envelope.message.builder_index = bid.builder_index; + envelope.message.payload.slot_number = block.slot(); + envelope.message.payload.parent_hash = bid.parent_block_hash; + envelope.message.payload.block_hash = bid.block_hash; + Arc::new(envelope) + } + + #[allow(clippy::type_complexity)] + fn make_gloas_blocks_and_columns( + count: usize, + spec: &ChainSpec, + ) -> Vec<( + Arc>, + DataColumnSidecarList, + Arc>, + )> { + let mut u = types::test_utils::test_unstructured(); + (0..count) + .map(|_| { + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut u, + spec, + ) + .unwrap(); + let envelope = matching_envelope(&block); + (Arc::new(block), data_columns, envelope) + }) + .collect() + } + + #[allow(clippy::type_complexity)] + fn add_all_columns( + info: &mut RangeBlockComponentsRequest, + blocks: &[( + Arc>, + DataColumnSidecarList, + Arc>, + )], + columns_req_id: &[(DataColumnsByRangeRequestId, Vec)], + expected_custody_columns: &[u64], + ) { + for (i, &column_index) in expected_custody_columns.iter().enumerate() { + let (req, _columns) = columns_req_id.get(i).unwrap(); + info.add_custody_columns( + *req, + blocks + .iter() + .flat_map(|(_, columns, _)| { + columns + .iter() + .filter(|column| *column.index() == column_index) + .cloned() + }) + .collect(), + ) + .unwrap(); + } + } + #[test] fn no_blobs_into_responses() { let spec = Arc::new(test_spec::()); @@ -864,6 +960,267 @@ mod tests { info.responses(da_checker, spec).unwrap().unwrap(); } + #[test] + fn gloas_payload_envelopes_must_complete_before_responses() { + let spec = Arc::new(gloas_spec()); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let expected_custody_columns = da_checker + .custody_context() + .sampling_columns_for_epoch(Epoch::new(0), &spec) + .to_vec(); + let blocks = make_gloas_blocks_and_columns(2, &spec); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let payloads_req_id = payloads_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Some(payloads_req_id), + Span::none(), + ); + + info.add_blocks( + blocks_req_id, + blocks.iter().map(|(block, _, _)| block.clone()).collect(), + ) + .unwrap(); + add_all_columns( + &mut info, + &blocks, + &columns_req_id, + &expected_custody_columns, + ); + + assert!(info.responses(da_checker, spec).is_none()); + } + + #[test] + fn gloas_payload_envelopes_are_coupled_by_block_root() { + let spec = Arc::new(gloas_spec()); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let expected_custody_columns = da_checker + .custody_context() + .sampling_columns_for_epoch(Epoch::new(0), &spec) + .to_vec(); + let blocks = make_gloas_blocks_and_columns(2, &spec); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let payloads_req_id = payloads_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Some(payloads_req_id), + Span::none(), + ); + + info.add_blocks( + blocks_req_id, + blocks.iter().map(|(block, _, _)| block.clone()).collect(), + ) + .unwrap(); + add_all_columns( + &mut info, + &blocks, + &columns_req_id, + &expected_custody_columns, + ); + info.add_payload_envelopes( + payloads_req_id, + blocks + .iter() + .rev() + .map(|(_, _, envelope)| envelope.clone()) + .collect(), + ) + .unwrap(); + + let responses = info.responses(da_checker, spec).unwrap().unwrap(); + assert_eq!(responses.len(), blocks.len()); + for response in responses { + match response { + beacon_chain::block_verification_types::RangeSyncBlock::Gloas { + block, + envelope: Some(envelope), + } => { + assert_eq!( + envelope.envelope().beacon_block_root(), + block.canonical_root() + ); + assert_eq!(envelope.columns.len(), expected_custody_columns.len()); + } + other => panic!("expected Gloas block with envelope, got {other:?}"), + } + } + } + + #[test] + fn gloas_payload_envelopes_allow_missing_envelopes() { + let spec = Arc::new(gloas_spec()); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let expected_custody_columns = da_checker + .custody_context() + .sampling_columns_for_epoch(Epoch::new(0), &spec) + .to_vec(); + let blocks = make_gloas_blocks_and_columns(2, &spec); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let payloads_req_id = payloads_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Some(payloads_req_id), + Span::none(), + ); + + info.add_blocks( + blocks_req_id, + blocks.iter().map(|(block, _, _)| block.clone()).collect(), + ) + .unwrap(); + add_all_columns( + &mut info, + &blocks, + &columns_req_id, + &expected_custody_columns, + ); + info.add_payload_envelopes(payloads_req_id, vec![blocks[0].2.clone()]) + .unwrap(); + + let responses = info.responses(da_checker, spec).unwrap().unwrap(); + assert_eq!( + responses + .iter() + .filter(|response| matches!( + response, + beacon_chain::block_verification_types::RangeSyncBlock::Gloas { + envelope: Some(_), + .. + } + )) + .count(), + 1 + ); + assert_eq!( + responses + .iter() + .filter(|response| matches!( + response, + beacon_chain::block_verification_types::RangeSyncBlock::Gloas { + envelope: None, + .. + } + )) + .count(), + 1 + ); + } + + #[test] + fn gloas_payload_envelope_mismatch_fails_coupling() { + let spec = Arc::new(gloas_spec()); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let expected_custody_columns = da_checker + .custody_context() + .sampling_columns_for_epoch(Epoch::new(0), &spec) + .to_vec(); + let blocks = make_gloas_blocks_and_columns(1, &spec); + let mut bad_envelope = (*blocks[0].2).clone(); + bad_envelope.message.payload.slot_number += 1; + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let payloads_req_id = payloads_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Some(payloads_req_id), + Span::none(), + ); + + info.add_blocks( + blocks_req_id, + blocks.iter().map(|(block, _, _)| block.clone()).collect(), + ) + .unwrap(); + add_all_columns( + &mut info, + &blocks, + &columns_req_id, + &expected_custody_columns, + ); + info.add_payload_envelopes(payloads_req_id, vec![Arc::new(bad_envelope)]) + .unwrap(); + + let result = info.responses(da_checker, spec).unwrap(); + assert!( + matches!( + result, + Err(super::CouplingError::EnvelopePeerFailure(ref error)) + if error.contains("envelope slot mismatch") + ), + "expected envelope slot mismatch, got {result:?}" + ); + } + #[test] fn missing_custody_columns_from_faulty_peers() { // GIVEN: A request expecting sampling columns from multiple peers