From e4e6908f6f13d2731ae49870720374d10269bdf3 Mon Sep 17 00:00:00 2001 From: Woodpile37 Date: Sat, 6 Jan 2024 02:15:30 -0600 Subject: [PATCH] approve --- .../tests/.block_verification.rs.swp | Bin 0 -> 16384 bytes .../.block_verification_BASE_1058.rs.swp | Bin 0 -> 16384 bytes .../.block_verification_LOCAL_1058.rs.swp | Bin 0 -> 16384 bytes .../.block_verification_REMOTE_1058.rs.swp | Bin 0 -> 16384 bytes .../tests/block_verification_BACKUP_1058.rs | 1407 ++++++++++++++++ .../tests/block_verification_BASE_1058.rs | 1441 ++++++++++++++++ .../tests/block_verification_LOCAL_1058.rs | 1398 ++++++++++++++++ .../tests/block_verification_REMOTE_1058.rs | 1446 +++++++++++++++++ 8 files changed, 5692 insertions(+) create mode 100644 beacon_node/beacon_chain/tests/.block_verification.rs.swp create mode 100644 beacon_node/beacon_chain/tests/.block_verification_BASE_1058.rs.swp create mode 100644 beacon_node/beacon_chain/tests/.block_verification_LOCAL_1058.rs.swp create mode 100644 beacon_node/beacon_chain/tests/.block_verification_REMOTE_1058.rs.swp create mode 100644 beacon_node/beacon_chain/tests/block_verification_BACKUP_1058.rs create mode 100644 beacon_node/beacon_chain/tests/block_verification_BASE_1058.rs create mode 100644 beacon_node/beacon_chain/tests/block_verification_LOCAL_1058.rs create mode 100644 beacon_node/beacon_chain/tests/block_verification_REMOTE_1058.rs diff --git a/beacon_node/beacon_chain/tests/.block_verification.rs.swp b/beacon_node/beacon_chain/tests/.block_verification.rs.swp new file mode 100644 index 0000000000000000000000000000000000000000..9b8044cc933d14e209fada86d9e533659f5511a4 GIT binary patch literal 16384 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 literal 0 HcmV?d00001 diff --git a/beacon_node/beacon_chain/tests/.block_verification_LOCAL_1058.rs.swp b/beacon_node/beacon_chain/tests/.block_verification_LOCAL_1058.rs.swp new file mode 100644 index 0000000000000000000000000000000000000000..1e48572457df2cb037dfd1ff55d0e29bc990216c GIT binary patch literal 16384 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 R`3ZG+h=gOXeWtu>{0BYhSJ(gm literal 0 HcmV?d00001 diff --git a/beacon_node/beacon_chain/tests/.block_verification_REMOTE_1058.rs.swp b/beacon_node/beacon_chain/tests/.block_verification_REMOTE_1058.rs.swp new file mode 100644 index 0000000000000000000000000000000000000000..71fc1126e299ec64353c9f483b77730864103987 GIT binary patch literal 16384 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 FzW~xSH532< literal 0 HcmV?d00001 diff --git a/beacon_node/beacon_chain/tests/block_verification_BACKUP_1058.rs b/beacon_node/beacon_chain/tests/block_verification_BACKUP_1058.rs new file mode 100644 index 00000000000..58f43a3d6a6 --- /dev/null +++ b/beacon_node/beacon_chain/tests/block_verification_BACKUP_1058.rs @@ -0,0 +1,1407 @@ +#![cfg(not(debug_assertions))] + +use beacon_chain::{ +<<<<<<< HEAD + blob_verification::{AsBlock, BlockWrapper}, + test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, +======= + BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock, + NotifyExecutionLayer, +>>>>>>> 20067b946 (Remove checkpoint alignment requirements and enable historic state pruning (#4610)) +}; +use beacon_chain::{BeaconSnapshot, BlockError, ChainSegmentResult, NotifyExecutionLayer}; +use fork_choice::CountUnrealized; +use lazy_static::lazy_static; +use logging::test_logger; +use slasher::{Config as SlasherConfig, Slasher}; +use state_processing::{ + common::get_indexed_attestation, + per_block_processing::{per_block_processing, BlockSignatureStrategy}, + per_slot_processing, BlockProcessingError, ConsensusContext, StateProcessingStrategy, + VerifyBlockRoot, +}; +use std::marker::PhantomData; +use std::sync::Arc; +use tempfile::tempdir; +use types::{test_utils::generate_deterministic_keypair, *}; + +type E = MainnetEthSpec; + +// Should ideally be divisible by 3. +const VALIDATOR_COUNT: usize = 24; +const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; +const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; + +lazy_static! { + /// A cached set of keys. + static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); +} + +async fn get_chain_segment() -> Vec> { + let harness = get_harness(VALIDATOR_COUNT); + + harness + .extend_chain( + CHAIN_SEGMENT_LENGTH, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut segment = Vec::with_capacity(CHAIN_SEGMENT_LENGTH); + for snapshot in harness + .chain + .chain_dump() + .expect("should dump chain") + .into_iter() + .skip(1) + { + let full_block = harness + .chain + .get_block(&snapshot.beacon_block_root) + .await + .unwrap() + .unwrap(); + segment.push(BeaconSnapshot { + beacon_block_root: snapshot.beacon_block_root, + beacon_block: Arc::new(full_block), + beacon_state: snapshot.beacon_state, + }); + } + segment +} + +fn get_harness(validator_count: usize) -> BeaconChainHarness> { + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +fn chain_segment_blocks(chain_segment: &[BeaconSnapshot]) -> Vec>> { + chain_segment + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect() +} + +fn junk_signature() -> Signature { + let kp = generate_deterministic_keypair(VALIDATOR_COUNT); + let message = Hash256::from_slice(&[42; 32]); + kp.sk.sign(message) +} + +fn junk_aggregate_signature() -> AggregateSignature { + let mut agg_sig = AggregateSignature::empty(); + agg_sig.add_assign(&junk_signature()); + agg_sig +} + +fn update_proposal_signatures( + snapshots: &mut [BeaconSnapshot], + harness: &BeaconChainHarness>, +) { + for snapshot in snapshots { + let spec = &harness.chain.spec; + let slot = snapshot.beacon_block.slot(); + let state = &snapshot.beacon_state; + let proposer_index = state + .get_beacon_proposer_index(slot, spec) + .expect("should find proposer index"); + let keypair = harness + .validator_keypairs + .get(proposer_index) + .expect("proposer keypair should be available"); + + let (block, _) = snapshot.beacon_block.as_ref().clone().deconstruct(); + snapshot.beacon_block = Arc::new(block.sign( + &keypair.sk, + &state.fork(), + state.genesis_validators_root(), + spec, + )); + } +} + +fn update_parent_roots(snapshots: &mut [BeaconSnapshot]) { + for i in 0..snapshots.len() { + let root = snapshots[i].beacon_block.canonical_root(); + if let Some(child) = snapshots.get_mut(i + 1) { + let (mut block, signature) = child.beacon_block.as_ref().clone().deconstruct(); + *block.parent_root_mut() = root; + child.beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)) + } + } +} + +#[tokio::test] +async fn chain_segment_full_segment() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + // Sneak in a little check to ensure we can process empty chain segments. + harness + .chain + .process_chain_segment(vec![], CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import empty chain segment"); + + harness + .chain + .process_chain_segment( + blocks.clone(), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .into_block_error() + .expect("should import chain segment"); + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); +} + +#[tokio::test] +async fn chain_segment_varying_chunk_size() { + for chunk_size in &[1, 2, 3, 5, 31, 32, 33, 42] { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + for chunk in blocks.chunks(*chunk_size) { + harness + .chain + .process_chain_segment( + chunk.to_vec(), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .into_block_error() + .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); + } + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); + } +} + +#[tokio::test] +async fn chain_segment_non_linear_parent_roots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test with a block removed. + */ + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + blocks.remove(2); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with missing parent" + ); + + /* + * Test with a modified parent root. + */ + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + + let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); + *block.parent_root_mut() = Hash256::zero(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)).into(); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with a broken parent root link" + ); +} + +#[tokio::test] +async fn chain_segment_non_linear_slots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test where a child is lower than the parent. + */ + + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); + *block.slot_mut() = Slot::new(0); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)).into(); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has a lower slot than its child" + ); + + /* + * Test where a child is equal to the parent. + */ + + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); + *block.slot_mut() = blocks[2].slot(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)).into(); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has an equal slot to its child" + ); +} + +async fn assert_invalid_signature( + chain_segment: &[BeaconSnapshot], + harness: &BeaconChainHarness>, + block_index: usize, + snapshots: &[BeaconSnapshot], + item: &str, +) { + let blocks: Vec> = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect(); + + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid {} signature", + item + ); + + // Call fork choice to update cached head (including finalization). + harness.chain.recompute_head_at_current_slot().await; + + // Ensure the block will be rejected if imported on its own (without gossip checking). + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.clone().into()) + .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. + let _ = harness + .chain + .process_chain_segment( + ancestor_blocks, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await; + harness.chain.recompute_head_at_current_slot().await; + + let process_res = harness + .chain + .process_block( + snapshots[block_index].beacon_block.canonical_root(), + snapshots[block_index].beacon_block.clone(), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await; + assert!( + matches!(process_res, Err(BlockError::InvalidSignature)), + "should not import individual block with an invalid {} signature, got: {:?}", + item, + process_res + ); + + // NOTE: we choose not to check gossip verification here. It only checks one signature + // (proposal) and that is already tested elsewhere in this file. + // + // It's not trivial to just check gossip verification since it will start refusing + // blocks as soon as it has seen one valid proposal signature for a given (validator, + // slot) tuple. +} + +async fn get_invalid_sigs_harness( + chain_segment: &[BeaconSnapshot], +) -> BeaconChainHarness> { + let harness = get_harness(VALIDATOR_COUNT); + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + harness +} +#[tokio::test] +async fn invalid_signature_gossip_block() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Ensure the block will be rejected if imported on its own (without gossip checking). + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + // Import all the ancestors before the `block_index` block. + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect(); + harness + .chain + .process_chain_segment( + ancestor_blocks, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .into_block_error() + .expect("should import all blocks prior to the one being tested"); + let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); + assert!( + matches!( + harness + .chain + .process_block( + signed_block.canonical_root(), + Arc::new(signed_block), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await, + Err(BlockError::InvalidSignature) + ), + "should not import individual block with an invalid gossip signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_block_proposal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + let blocks: Vec> = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect::>(); + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid block signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_randao_reveal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + *block.body_mut().randao_reveal_mut() = junk_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature(&chain_segment, &harness, block_index, &snapshots, "randao").await; + } +} + +#[tokio::test] +async fn invalid_signature_proposer_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let proposer_slashing = ProposerSlashing { + signed_header_1: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + signed_header_2: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + }; + block + .body_mut() + .proposer_slashings_mut() + .push(proposer_slashing) + .expect("should update proposer slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "proposer slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attester_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let indexed_attestation = IndexedAttestation { + attesting_indices: vec![0].into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + target: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + }, + signature: junk_aggregate_signature(), + }; + let attester_slashing = AttesterSlashing { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .attester_slashings_mut() + .push(attester_slashing) + .expect("should update attester slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attester slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attestation() { + let chain_segment = get_chain_segment().await; + let mut checked_attestation = false; + + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + if let Some(attestation) = block.body_mut().attestations_mut().get_mut(0) { + attestation.signature = junk_aggregate_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attestation", + ) + .await; + checked_attestation = true; + } + } + + assert!( + checked_attestation, + "the test should check an attestation signature" + ) +} + +#[tokio::test] +async fn invalid_signature_deposit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Note: an invalid deposit signature is permitted! + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let deposit = Deposit { + proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1].into(), + data: DepositData { + pubkey: Keypair::random().pk.into(), + withdrawal_credentials: Hash256::zero(), + amount: 0, + signature: junk_signature().into(), + }, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .deposits_mut() + .push(deposit) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + let blocks: Vec> = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect(); + assert!( + !matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not throw an invalid signature error for a bad deposit signature" + ); + } +} + +#[tokio::test] +async fn invalid_signature_exit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let epoch = snapshots[block_index].beacon_state.current_epoch(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .voluntary_exits_mut() + .push(SignedVoluntaryExit { + message: VoluntaryExit { + epoch, + validator_index: 0, + }, + signature: junk_signature(), + }) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "voluntary exit", + ) + .await; + } +} + +fn unwrap_err(result: Result) -> E { + match result { + Ok(_) => panic!("called unwrap_err on Ok"), + Err(e) => e, + } +} + +#[tokio::test] +async fn block_gossip_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + let block_index = CHAIN_SEGMENT_LENGTH - 2; + + harness + .chain + .slot_clock + .set_slot(chain_segment[block_index].beacon_block.slot().as_u64()); + + // Import the ancestors prior to the block we're testing. + for snapshot in &chain_segment[0..block_index] { + let gossip_verified = harness + .chain + .verify_block_for_gossip(snapshot.beacon_block.clone().into()) + .await + .expect("should obtain gossip verified block"); + + harness + .chain + .process_block( + gossip_verified.block_root, + gossip_verified, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .expect("should import valid gossip verified block"); + } + + // Recompute the head to ensure we cache the latest view of fork choice. + harness.chain.recompute_head_at_current_slot().await; + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- + * i.e. validate that signed_beacon_block.message.slot <= current_slot (a client MAY queue + * future blocks for processing at the appropriate slot). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_block_slot = block.slot() + 1; + *block.slot_mut() = expected_block_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::FutureSlot { + present_slot, + block_slot, + } + if present_slot == expected_block_slot - 1 && block_slot == expected_block_slot + ), + "should not import a block with a future slot" + ); + + /* + * This test ensure that: + * + * Spec v0.12.1 + * + * The block is from a slot greater than the latest finalized slot -- i.e. validate that + * signed_beacon_block.message.slot > + * compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) (a client MAY choose to + * validate and store such blocks for additional purposes -- e.g. slashing detection, archive + * nodes, etc). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_finalized_slot = harness + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + *block.slot_mut() = expected_finalized_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::WouldRevertFinalizedSlot { + block_slot, + finalized_slot, + } + if block_slot == expected_finalized_slot && finalized_slot == expected_finalized_slot + ), + "should not import a block with a finalized slot" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The proposer signature, signed_beacon_block.signature, is valid with respect to the + * proposer_index pubkey. + */ + + let block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + assert!( + matches!( + unwrap_err( + harness + .chain + .verify_block_for_gossip( + Arc::new(SignedBeaconBlock::from_block(block, junk_signature())).into() + ) + .await + ), + BlockError::ProposalSignatureInvalid + ), + "should not import a block with an invalid proposal signature" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The block's parent (defined by block.parent_root) passes validation. + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = Hash256::from_low_u64_be(42); + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::ParentUnknown(block) + if block.parent_root() == parent_root + ), + "should not import a block for an unknown parent" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The current finalized_checkpoint is an ancestor of block -- i.e. get_ancestor(store, + * block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == + * store.finalized_checkpoint.root + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = chain_segment[0].beacon_block_root; + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::NotFinalizedDescendant { block_parent_root } + if block_parent_root == parent_root + ), + "should not import a block that conflicts with finality" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is proposed by the expected proposer_index for the block's slot in the context of + * the current shuffling (defined by parent_root/slot). If the proposer_index cannot + * immediately be verified against the expected shuffling, the block MAY be queued for later + * processing while proposers for the block's branch are calculated. + */ + + let mut block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + let expected_proposer = block.proposer_index(); + let other_proposer = (0..VALIDATOR_COUNT as u64) + .into_iter() + .find(|i| *i != block.proposer_index()) + .expect("there must be more than one validator in this test"); + *block.proposer_index_mut() = other_proposer; + let block = block.sign( + &generate_deterministic_keypair(other_proposer as usize).sk, + &harness.chain.canonical_head.cached_head().head_fork(), + harness.chain.genesis_validators_root, + &harness.chain.spec, + ); + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()).into()).await), + BlockError::IncorrectBlockProposer { + block, + local_shuffling, + } + if block == other_proposer && local_shuffling == expected_proposer + ), + "should not import a block with the wrong proposer index" + ); + // Check to ensure that we registered this is a valid block from this proposer. + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()).into()).await), + BlockError::RepeatProposal { + proposer, + slot, + } + if proposer == other_proposer && slot == block.message().slot() + ), + "should register any valid signature against the proposer, even if the block failed later verification" + ); + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + harness + .chain + .verify_block_for_gossip(block.into()) + .await + .is_ok(), + "the valid block should be processed" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is the first block with valid signature received for the proposer for the slot, + * signed_beacon_block.message.slot. + */ + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + matches!( + harness + .chain + .verify_block_for_gossip(block.clone().into()) + .await + .err() + .expect("should error when processing known block"), + BlockError::RepeatProposal { + proposer, + slot, + } + if proposer == block.message().proposer_index() && slot == block.message().slot() + ), + "the second proposal by this validator should be rejected" + ); +} + +#[tokio::test] +async fn verify_block_for_gossip_slashing_detection() { + let slasher_dir = tempdir().unwrap(); + let slasher = Arc::new( + Slasher::open(SlasherConfig::new(slasher_dir.path().into()), test_logger()).unwrap(), + ); + + let inner_slasher = slasher.clone(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS.to_vec()) + .fresh_ephemeral_store() + .initial_mutator(Box::new(move |builder| builder.slasher(inner_slasher))) + .mock_execution_layer() + .build(); + harness.advance_slot(); + + let state = harness.get_current_state(); + let (block1, _) = harness.make_block(state.clone(), Slot::new(1)).await; + let (block2, _) = harness.make_block(state, Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block1).into()) + .await + .unwrap(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .unwrap(); + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(block2).into()) + .await, + ); + + // Slasher should have been handed the two conflicting blocks and crafted a slashing. + slasher.process_queued(Epoch::new(0)).unwrap(); + let proposer_slashings = slasher.get_proposer_slashings(); + assert_eq!(proposer_slashings.len(), 1); + // windows won't delete the temporary directory if you don't do this.. + drop(harness); + drop(slasher); + slasher_dir.close().unwrap(); +} + +#[tokio::test] +async fn verify_block_for_gossip_doppelganger_detection() { + let harness = get_harness(VALIDATOR_COUNT); + + let state = harness.get_current_state(); + let (block, _) = harness.make_block(state.clone(), Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block).into()) + .await + .unwrap(); + let attestations = verified_block.block.message().body().attestations().clone(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .unwrap(); + + for att in attestations.iter() { + let epoch = att.data.target.epoch; + let committee = state + .get_beacon_committee(att.data.slot, att.data.index) + .unwrap(); + let indexed_attestation = get_indexed_attestation(committee.committee, att).unwrap(); + + for &index in &indexed_attestation.attesting_indices { + let index = index as usize; + + assert!(harness.chain.validator_seen_at_epoch(index, epoch)); + + // Check the correct beacon cache is populated + assert!(harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed")); + assert!(!harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed")); + assert!(!harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed")); + } + } +} + +#[tokio::test] +async fn add_base_block_to_altair_chain() { + let mut spec = MainnetEthSpec::default_spec(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + + // The Altair fork happens at epoch 1. + spec.altair_fork_epoch = Some(Epoch::new(1)); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build out all the blocks in epoch 0. + harness + .extend_chain( + slots_per_epoch as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an Altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (altair_signed_block, _) = harness.make_block(state.clone(), slot).await; + let altair_block = &altair_signed_block + .as_altair() + .expect("test expects an altair block") + .message; + let altair_body = &altair_block.body; + + // Create a Base-equivalent of `altair_block`. + let base_block = SignedBeaconBlock::Base(SignedBeaconBlockBase { + message: BeaconBlockBase { + slot: altair_block.slot, + proposer_index: altair_block.proposer_index, + parent_root: altair_block.parent_root, + state_root: altair_block.state_root, + body: BeaconBlockBodyBase { + randao_reveal: altair_body.randao_reveal.clone(), + eth1_data: altair_body.eth1_data.clone(), + graffiti: altair_body.graffiti, + proposer_slashings: altair_body.proposer_slashings.clone(), + attester_slashings: altair_body.attester_slashings.clone(), + attestations: altair_body.attestations.clone(), + deposits: altair_body.deposits.clone(), + voluntary_exits: altair_body.voluntary_exits.clone(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(base_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &base_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(base_block.clone()).into()) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + base_block.canonical_root(), + Arc::new(base_block.clone()), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment( + vec![Arc::new(base_block).into()], + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + } + )); +} + +#[tokio::test] +async fn add_altair_block_to_base_chain() { + let mut spec = MainnetEthSpec::default_spec(); + + // Altair never happens. + spec.altair_fork_epoch = None; + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build one block. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (base_signed_block, _) = harness.make_block(state.clone(), slot).await; + let base_block = &base_signed_block + .as_base() + .expect("test expects a base block") + .message; + let base_body = &base_block.body; + + // Create an Altair-equivalent of `altair_block`. + let altair_block = SignedBeaconBlock::Altair(SignedBeaconBlockAltair { + message: BeaconBlockAltair { + slot: base_block.slot, + proposer_index: base_block.proposer_index, + parent_root: base_block.parent_root, + state_root: base_block.state_root, + body: BeaconBlockBodyAltair { + randao_reveal: base_body.randao_reveal.clone(), + eth1_data: base_body.eth1_data.clone(), + graffiti: base_body.graffiti, + proposer_slashings: base_body.proposer_slashings.clone(), + attester_slashings: base_body.attester_slashings.clone(), + attestations: base_body.attestations.clone(), + deposits: base_body.deposits.clone(), + voluntary_exits: base_body.voluntary_exits.clone(), + sync_aggregate: SyncAggregate::empty(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(altair_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &altair_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(altair_block.clone()).into()) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + altair_block.canonical_root(), + Arc::new(altair_block.clone()), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment( + vec![Arc::new(altair_block).into()], + CountUnrealized::True, + NotifyExecutionLayer::Yes + ) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + } + )); +} diff --git a/beacon_node/beacon_chain/tests/block_verification_BASE_1058.rs b/beacon_node/beacon_chain/tests/block_verification_BASE_1058.rs new file mode 100644 index 00000000000..75b00b2b442 --- /dev/null +++ b/beacon_node/beacon_chain/tests/block_verification_BASE_1058.rs @@ -0,0 +1,1441 @@ +#![cfg(not(debug_assertions))] + +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, +}; +use beacon_chain::{ + BeaconSnapshot, BlockError, ChainSegmentResult, IntoExecutionPendingBlock, NotifyExecutionLayer, +}; +use lazy_static::lazy_static; +use logging::test_logger; +use slasher::{Config as SlasherConfig, Slasher}; +use state_processing::{ + common::get_indexed_attestation, + per_block_processing::{per_block_processing, BlockSignatureStrategy}, + per_slot_processing, BlockProcessingError, ConsensusContext, StateProcessingStrategy, + VerifyBlockRoot, +}; +use std::marker::PhantomData; +use std::sync::Arc; +use tempfile::tempdir; +use types::{test_utils::generate_deterministic_keypair, *}; + +type E = MainnetEthSpec; + +// Should ideally be divisible by 3. +const VALIDATOR_COUNT: usize = 24; +const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; +const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; + +lazy_static! { + /// A cached set of keys. + static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); +} + +async fn get_chain_segment() -> Vec> { + let harness = get_harness(VALIDATOR_COUNT); + + harness + .extend_chain( + CHAIN_SEGMENT_LENGTH, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut segment = Vec::with_capacity(CHAIN_SEGMENT_LENGTH); + for snapshot in harness + .chain + .chain_dump() + .expect("should dump chain") + .into_iter() + .skip(1) + { + let full_block = harness + .chain + .get_block(&snapshot.beacon_block_root) + .await + .unwrap() + .unwrap(); + segment.push(BeaconSnapshot { + beacon_block_root: snapshot.beacon_block_root, + beacon_block: Arc::new(full_block), + beacon_state: snapshot.beacon_state, + }); + } + segment +} + +fn get_harness(validator_count: usize) -> BeaconChainHarness> { + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +fn chain_segment_blocks(chain_segment: &[BeaconSnapshot]) -> Vec>> { + chain_segment + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect() +} + +fn junk_signature() -> Signature { + let kp = generate_deterministic_keypair(VALIDATOR_COUNT); + let message = Hash256::from_slice(&[42; 32]); + kp.sk.sign(message) +} + +fn junk_aggregate_signature() -> AggregateSignature { + let mut agg_sig = AggregateSignature::empty(); + agg_sig.add_assign(&junk_signature()); + agg_sig +} + +fn update_proposal_signatures( + snapshots: &mut [BeaconSnapshot], + harness: &BeaconChainHarness>, +) { + for snapshot in snapshots { + let spec = &harness.chain.spec; + let slot = snapshot.beacon_block.slot(); + let state = &snapshot.beacon_state; + let proposer_index = state + .get_beacon_proposer_index(slot, spec) + .expect("should find proposer index"); + let keypair = harness + .validator_keypairs + .get(proposer_index) + .expect("proposer keypair should be available"); + + let (block, _) = snapshot.beacon_block.as_ref().clone().deconstruct(); + snapshot.beacon_block = Arc::new(block.sign( + &keypair.sk, + &state.fork(), + state.genesis_validators_root(), + spec, + )); + } +} + +fn update_parent_roots(snapshots: &mut [BeaconSnapshot]) { + for i in 0..snapshots.len() { + let root = snapshots[i].beacon_block.canonical_root(); + if let Some(child) = snapshots.get_mut(i + 1) { + let (mut block, signature) = child.beacon_block.as_ref().clone().deconstruct(); + *block.parent_root_mut() = root; + child.beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)) + } + } +} + +#[tokio::test] +async fn chain_segment_full_segment() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks = chain_segment_blocks(&chain_segment); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + // Sneak in a little check to ensure we can process empty chain segments. + harness + .chain + .process_chain_segment(vec![], NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import empty chain segment"); + + harness + .chain + .process_chain_segment(blocks.clone(), NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import chain segment"); + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); +} + +#[tokio::test] +async fn chain_segment_varying_chunk_size() { + for chunk_size in &[1, 2, 3, 5, 31, 32, 33, 42] { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks = chain_segment_blocks(&chain_segment); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + for chunk in blocks.chunks(*chunk_size) { + harness + .chain + .process_chain_segment(chunk.to_vec(), NotifyExecutionLayer::Yes) + .await + .into_block_error() + .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); + } + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); + } +} + +#[tokio::test] +async fn chain_segment_non_linear_parent_roots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test with a block removed. + */ + let mut blocks = chain_segment_blocks(&chain_segment); + blocks.remove(2); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with missing parent" + ); + + /* + * Test with a modified parent root. + */ + let mut blocks = chain_segment_blocks(&chain_segment); + let (mut block, signature) = blocks[3].as_ref().clone().deconstruct(); + *block.parent_root_mut() = Hash256::zero(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with a broken parent root link" + ); +} + +#[tokio::test] +async fn chain_segment_non_linear_slots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test where a child is lower than the parent. + */ + + let mut blocks = chain_segment_blocks(&chain_segment); + let (mut block, signature) = blocks[3].as_ref().clone().deconstruct(); + *block.slot_mut() = Slot::new(0); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has a lower slot than its child" + ); + + /* + * Test where a child is equal to the parent. + */ + + let mut blocks = chain_segment_blocks(&chain_segment); + let (mut block, signature) = blocks[3].as_ref().clone().deconstruct(); + *block.slot_mut() = blocks[2].slot(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has an equal slot to its child" + ); +} + +async fn assert_invalid_signature( + chain_segment: &[BeaconSnapshot], + harness: &BeaconChainHarness>, + block_index: usize, + snapshots: &[BeaconSnapshot], + item: &str, +) { + let blocks = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect(); + + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid {} signature", + item + ); + + // Call fork choice to update cached head (including finalization). + harness.chain.recompute_head_at_current_slot().await; + + // Ensure the block will be rejected if imported on its own (without gossip checking). + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.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. + let _ = harness + .chain + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) + .await; + harness.chain.recompute_head_at_current_slot().await; + + let process_res = harness + .chain + .process_block( + snapshots[block_index].beacon_block.canonical_root(), + snapshots[block_index].beacon_block.clone(), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await; + assert!( + matches!(process_res, Err(BlockError::InvalidSignature)), + "should not import individual block with an invalid {} signature, got: {:?}", + item, + process_res + ); + + // NOTE: we choose not to check gossip verification here. It only checks one signature + // (proposal) and that is already tested elsewhere in this file. + // + // It's not trivial to just check gossip verification since it will start refusing + // blocks as soon as it has seen one valid proposal signature for a given (validator, + // slot) tuple. +} + +async fn get_invalid_sigs_harness( + chain_segment: &[BeaconSnapshot], +) -> BeaconChainHarness> { + let harness = get_harness(VALIDATOR_COUNT); + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + harness +} +#[tokio::test] +async fn invalid_signature_gossip_block() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Ensure the block will be rejected if imported on its own (without gossip checking). + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + // Import all the ancestors before the `block_index` block. + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.clone()) + .collect(); + harness + .chain + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import all blocks prior to the one being tested"); + let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); + assert!( + matches!( + harness + .chain + .process_block( + signed_block.canonical_root(), + Arc::new(signed_block), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await, + Err(BlockError::InvalidSignature) + ), + "should not import individual block with an invalid gossip signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_block_proposal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + let blocks = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect::>(); + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid block signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_randao_reveal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + *block.body_mut().randao_reveal_mut() = junk_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature(&chain_segment, &harness, block_index, &snapshots, "randao").await; + } +} + +#[tokio::test] +async fn invalid_signature_proposer_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let proposer_slashing = ProposerSlashing { + signed_header_1: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + signed_header_2: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + }; + block + .body_mut() + .proposer_slashings_mut() + .push(proposer_slashing) + .expect("should update proposer slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "proposer slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attester_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let indexed_attestation = IndexedAttestation { + attesting_indices: vec![0].into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + target: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + }, + signature: junk_aggregate_signature(), + }; + let attester_slashing = AttesterSlashing { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .attester_slashings_mut() + .push(attester_slashing) + .expect("should update attester slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attester slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attestation() { + let chain_segment = get_chain_segment().await; + let mut checked_attestation = false; + + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + if let Some(attestation) = block.body_mut().attestations_mut().get_mut(0) { + attestation.signature = junk_aggregate_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attestation", + ) + .await; + checked_attestation = true; + } + } + + assert!( + checked_attestation, + "the test should check an attestation signature" + ) +} + +#[tokio::test] +async fn invalid_signature_deposit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Note: an invalid deposit signature is permitted! + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let deposit = Deposit { + proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1].into(), + data: DepositData { + pubkey: Keypair::random().pk.into(), + withdrawal_credentials: Hash256::zero(), + amount: 0, + signature: junk_signature().into(), + }, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .deposits_mut() + .push(deposit) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + let blocks = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect(); + assert!( + !matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not throw an invalid signature error for a bad deposit signature" + ); + } +} + +#[tokio::test] +async fn invalid_signature_exit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let epoch = snapshots[block_index].beacon_state.current_epoch(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .voluntary_exits_mut() + .push(SignedVoluntaryExit { + message: VoluntaryExit { + epoch, + validator_index: 0, + }, + signature: junk_signature(), + }) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "voluntary exit", + ) + .await; + } +} + +fn unwrap_err(result: Result) -> E { + match result { + Ok(_) => panic!("called unwrap_err on Ok"), + Err(e) => e, + } +} + +#[tokio::test] +async fn block_gossip_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + let block_index = CHAIN_SEGMENT_LENGTH - 2; + + harness + .chain + .slot_clock + .set_slot(chain_segment[block_index].beacon_block.slot().as_u64()); + + // Import the ancestors prior to the block we're testing. + for snapshot in &chain_segment[0..block_index] { + let gossip_verified = harness + .chain + .verify_block_for_gossip(snapshot.beacon_block.clone()) + .await + .expect("should obtain gossip verified block"); + + harness + .chain + .process_block( + gossip_verified.block_root, + gossip_verified, + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .expect("should import valid gossip verified block"); + } + + // Recompute the head to ensure we cache the latest view of fork choice. + harness.chain.recompute_head_at_current_slot().await; + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- + * i.e. validate that signed_beacon_block.message.slot <= current_slot (a client MAY queue + * future blocks for processing at the appropriate slot). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_block_slot = block.slot() + 1; + *block.slot_mut() = expected_block_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::FutureSlot { + present_slot, + block_slot, + } + if present_slot == expected_block_slot - 1 && block_slot == expected_block_slot + ), + "should not import a block with a future slot" + ); + + /* + * This test ensure that: + * + * Spec v0.12.1 + * + * The block is from a slot greater than the latest finalized slot -- i.e. validate that + * signed_beacon_block.message.slot > + * compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) (a client MAY choose to + * validate and store such blocks for additional purposes -- e.g. slashing detection, archive + * nodes, etc). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_finalized_slot = harness + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + *block.slot_mut() = expected_finalized_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::WouldRevertFinalizedSlot { + block_slot, + finalized_slot, + } + if block_slot == expected_finalized_slot && finalized_slot == expected_finalized_slot + ), + "should not import a block with a finalized slot" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The proposer signature, signed_beacon_block.signature, is valid with respect to the + * proposer_index pubkey. + */ + + let block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + assert!( + matches!( + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block( + block, + junk_signature() + ))) + .await + ), + BlockError::ProposalSignatureInvalid + ), + "should not import a block with an invalid proposal signature" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The block's parent (defined by block.parent_root) passes validation. + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = Hash256::from_low_u64_be(42); + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::ParentUnknown(block) + if block.parent_root() == parent_root + ), + "should not import a block for an unknown parent" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The current finalized_checkpoint is an ancestor of block -- i.e. get_ancestor(store, + * block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == + * store.finalized_checkpoint.root + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = chain_segment[0].beacon_block_root; + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::NotFinalizedDescendant { block_parent_root } + if block_parent_root == parent_root + ), + "should not import a block that conflicts with finality" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is proposed by the expected proposer_index for the block's slot in the context of + * the current shuffling (defined by parent_root/slot). If the proposer_index cannot + * immediately be verified against the expected shuffling, the block MAY be queued for later + * processing while proposers for the block's branch are calculated. + */ + + let mut block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + let expected_proposer = block.proposer_index(); + let other_proposer = (0..VALIDATOR_COUNT as u64) + .into_iter() + .find(|i| *i != block.proposer_index()) + .expect("there must be more than one validator in this test"); + *block.proposer_index_mut() = other_proposer; + let block = block.sign( + &generate_deterministic_keypair(other_proposer as usize).sk, + &harness.chain.canonical_head.cached_head().head_fork(), + harness.chain.genesis_validators_root, + &harness.chain.spec, + ); + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), + BlockError::IncorrectBlockProposer { + block, + local_shuffling, + } + if block == other_proposer && local_shuffling == expected_proposer + ), + "should not import a block with the wrong proposer index" + ); + // Check to ensure that we registered this is a valid block from this proposer. + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), + BlockError::BlockIsAlreadyKnown, + ), + "should register any valid signature against the proposer, even if the block failed later verification" + ); + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + harness.chain.verify_block_for_gossip(block).await.is_ok(), + "the valid block should be processed" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is the first block with valid signature received for the proposer for the slot, + * signed_beacon_block.message.slot. + */ + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + matches!( + harness + .chain + .verify_block_for_gossip(block.clone()) + .await + .err() + .expect("should error when processing known block"), + BlockError::BlockIsAlreadyKnown + ), + "the second proposal by this validator should be rejected" + ); +} + +#[tokio::test] +async fn verify_block_for_gossip_slashing_detection() { + let slasher_dir = tempdir().unwrap(); + let slasher = Arc::new( + Slasher::open(SlasherConfig::new(slasher_dir.path().into()), test_logger()).unwrap(), + ); + + let inner_slasher = slasher.clone(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS.to_vec()) + .fresh_ephemeral_store() + .initial_mutator(Box::new(move |builder| builder.slasher(inner_slasher))) + .mock_execution_layer() + .build(); + harness.advance_slot(); + + let state = harness.get_current_state(); + let (block1, _) = harness.make_block(state.clone(), Slot::new(1)).await; + let (block2, _) = harness.make_block(state, Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block1)) + .await + .unwrap(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .unwrap(); + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(block2)) + .await, + ); + + // Slasher should have been handed the two conflicting blocks and crafted a slashing. + slasher.process_queued(Epoch::new(0)).unwrap(); + let proposer_slashings = slasher.get_proposer_slashings(); + assert_eq!(proposer_slashings.len(), 1); + // windows won't delete the temporary directory if you don't do this.. + drop(harness); + drop(slasher); + slasher_dir.close().unwrap(); +} + +#[tokio::test] +async fn verify_block_for_gossip_doppelganger_detection() { + let harness = get_harness(VALIDATOR_COUNT); + + let state = harness.get_current_state(); + let (block, _) = harness.make_block(state.clone(), Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block)) + .await + .unwrap(); + let attestations = verified_block.block.message().body().attestations().clone(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .unwrap(); + + for att in attestations.iter() { + let epoch = att.data.target.epoch; + let committee = state + .get_beacon_committee(att.data.slot, att.data.index) + .unwrap(); + let indexed_attestation = get_indexed_attestation(committee.committee, att).unwrap(); + + for &index in &indexed_attestation.attesting_indices { + let index = index as usize; + + assert!(harness.chain.validator_seen_at_epoch(index, epoch)); + + // Check the correct beacon cache is populated + assert!(harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed")); + assert!(!harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed")); + assert!(!harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed")); + } + } +} + +#[tokio::test] +async fn add_base_block_to_altair_chain() { + let mut spec = MainnetEthSpec::default_spec(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + + // The Altair fork happens at epoch 1. + spec.altair_fork_epoch = Some(Epoch::new(1)); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build out all the blocks in epoch 0. + harness + .extend_chain( + slots_per_epoch as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an Altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (altair_signed_block, _) = harness.make_block(state.clone(), slot).await; + let altair_block = &altair_signed_block + .as_altair() + .expect("test expects an altair block") + .message; + let altair_body = &altair_block.body; + + // Create a Base-equivalent of `altair_block`. + let base_block = SignedBeaconBlock::Base(SignedBeaconBlockBase { + message: BeaconBlockBase { + slot: altair_block.slot, + proposer_index: altair_block.proposer_index, + parent_root: altair_block.parent_root, + state_root: altair_block.state_root, + body: BeaconBlockBodyBase { + randao_reveal: altair_body.randao_reveal.clone(), + eth1_data: altair_body.eth1_data.clone(), + graffiti: altair_body.graffiti, + proposer_slashings: altair_body.proposer_slashings.clone(), + attester_slashings: altair_body.attester_slashings.clone(), + attestations: altair_body.attestations.clone(), + deposits: altair_body.deposits.clone(), + voluntary_exits: altair_body.voluntary_exits.clone(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(base_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &base_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(base_block.clone())) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + base_block.canonical_root(), + Arc::new(base_block.clone()), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment(vec![Arc::new(base_block)], NotifyExecutionLayer::Yes,) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + } + )); +} + +#[tokio::test] +async fn add_altair_block_to_base_chain() { + let mut spec = MainnetEthSpec::default_spec(); + + // Altair never happens. + spec.altair_fork_epoch = None; + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build one block. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (base_signed_block, _) = harness.make_block(state.clone(), slot).await; + let base_block = &base_signed_block + .as_base() + .expect("test expects a base block") + .message; + let base_body = &base_block.body; + + // Create an Altair-equivalent of `altair_block`. + let altair_block = SignedBeaconBlock::Altair(SignedBeaconBlockAltair { + message: BeaconBlockAltair { + slot: base_block.slot, + proposer_index: base_block.proposer_index, + parent_root: base_block.parent_root, + state_root: base_block.state_root, + body: BeaconBlockBodyAltair { + randao_reveal: base_body.randao_reveal.clone(), + eth1_data: base_body.eth1_data.clone(), + graffiti: base_body.graffiti, + proposer_slashings: base_body.proposer_slashings.clone(), + attester_slashings: base_body.attester_slashings.clone(), + attestations: base_body.attestations.clone(), + deposits: base_body.deposits.clone(), + voluntary_exits: base_body.voluntary_exits.clone(), + sync_aggregate: SyncAggregate::empty(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(altair_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &altair_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(altair_block.clone())) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + altair_block.canonical_root(), + Arc::new(altair_block.clone()), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment(vec![Arc::new(altair_block)], NotifyExecutionLayer::Yes) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + } + )); +} + +#[tokio::test] +async fn import_duplicate_block_unrealized_justification() { + let spec = MainnetEthSpec::default_spec(); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let chain = &harness.chain; + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build the chain out to the first justification opportunity 2/3rds of the way through epoch 2. + let num_slots = E::slots_per_epoch() as usize * 8 / 3; + harness + .extend_chain( + num_slots, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // The store's justified checkpoint must still be at epoch 0, while unrealized justification + // must be at epoch 1. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.unrealized_justified_checkpoint().epoch, 1); + drop(fc); + + // Produce a block to justify epoch 2. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (block, _) = harness.make_block(state.clone(), slot).await; + let block = Arc::new(block); + let block_root = block.canonical_root(); + + // Create two verified variants of the block, representing the same block being processed in + // parallel. + let notify_execution_layer = NotifyExecutionLayer::Yes; + let verified_block1 = block + .clone() + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + let verified_block2 = block + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + + // Import the first block, simulating a block processed via a finalized chain segment. + chain + .clone() + .import_execution_pending_block(verified_block1) + .await + .unwrap(); + + // Unrealized justification should NOT have updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + let unrealized_justification = fc.unrealized_justified_checkpoint(); + assert_eq!(unrealized_justification.epoch, 2); + + // The fork choice node for the block should have unrealized justification. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); + drop(fc); + + // Import the second verified block, simulating a block processed via RPC. + chain + .clone() + .import_execution_pending_block(verified_block2) + .await + .unwrap(); + + // Unrealized justification should still be updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!( + fc.unrealized_justified_checkpoint(), + unrealized_justification + ); + + // The fork choice node for the block should still have the unrealized justified checkpoint. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); +} diff --git a/beacon_node/beacon_chain/tests/block_verification_LOCAL_1058.rs b/beacon_node/beacon_chain/tests/block_verification_LOCAL_1058.rs new file mode 100644 index 00000000000..3da3beff504 --- /dev/null +++ b/beacon_node/beacon_chain/tests/block_verification_LOCAL_1058.rs @@ -0,0 +1,1398 @@ +#![cfg(not(debug_assertions))] + +use beacon_chain::{ + blob_verification::{AsBlock, BlockWrapper}, + test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, +}; +use beacon_chain::{BeaconSnapshot, BlockError, ChainSegmentResult, NotifyExecutionLayer}; +use fork_choice::CountUnrealized; +use lazy_static::lazy_static; +use logging::test_logger; +use slasher::{Config as SlasherConfig, Slasher}; +use state_processing::{ + common::get_indexed_attestation, + per_block_processing::{per_block_processing, BlockSignatureStrategy}, + per_slot_processing, BlockProcessingError, ConsensusContext, StateProcessingStrategy, + VerifyBlockRoot, +}; +use std::marker::PhantomData; +use std::sync::Arc; +use tempfile::tempdir; +use types::{test_utils::generate_deterministic_keypair, *}; + +type E = MainnetEthSpec; + +// Should ideally be divisible by 3. +const VALIDATOR_COUNT: usize = 24; +const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; +const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; + +lazy_static! { + /// A cached set of keys. + static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); +} + +async fn get_chain_segment() -> Vec> { + let harness = get_harness(VALIDATOR_COUNT); + + harness + .extend_chain( + CHAIN_SEGMENT_LENGTH, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut segment = Vec::with_capacity(CHAIN_SEGMENT_LENGTH); + for snapshot in harness + .chain + .chain_dump() + .expect("should dump chain") + .into_iter() + .skip(1) + { + let full_block = harness + .chain + .get_block(&snapshot.beacon_block_root) + .await + .unwrap() + .unwrap(); + segment.push(BeaconSnapshot { + beacon_block_root: snapshot.beacon_block_root, + beacon_block: Arc::new(full_block), + beacon_state: snapshot.beacon_state, + }); + } + segment +} + +fn get_harness(validator_count: usize) -> BeaconChainHarness> { + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +fn chain_segment_blocks(chain_segment: &[BeaconSnapshot]) -> Vec>> { + chain_segment + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect() +} + +fn junk_signature() -> Signature { + let kp = generate_deterministic_keypair(VALIDATOR_COUNT); + let message = Hash256::from_slice(&[42; 32]); + kp.sk.sign(message) +} + +fn junk_aggregate_signature() -> AggregateSignature { + let mut agg_sig = AggregateSignature::empty(); + agg_sig.add_assign(&junk_signature()); + agg_sig +} + +fn update_proposal_signatures( + snapshots: &mut [BeaconSnapshot], + harness: &BeaconChainHarness>, +) { + for snapshot in snapshots { + let spec = &harness.chain.spec; + let slot = snapshot.beacon_block.slot(); + let state = &snapshot.beacon_state; + let proposer_index = state + .get_beacon_proposer_index(slot, spec) + .expect("should find proposer index"); + let keypair = harness + .validator_keypairs + .get(proposer_index) + .expect("proposer keypair should be available"); + + let (block, _) = snapshot.beacon_block.as_ref().clone().deconstruct(); + snapshot.beacon_block = Arc::new(block.sign( + &keypair.sk, + &state.fork(), + state.genesis_validators_root(), + spec, + )); + } +} + +fn update_parent_roots(snapshots: &mut [BeaconSnapshot]) { + for i in 0..snapshots.len() { + let root = snapshots[i].beacon_block.canonical_root(); + if let Some(child) = snapshots.get_mut(i + 1) { + let (mut block, signature) = child.beacon_block.as_ref().clone().deconstruct(); + *block.parent_root_mut() = root; + child.beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)) + } + } +} + +#[tokio::test] +async fn chain_segment_full_segment() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + // Sneak in a little check to ensure we can process empty chain segments. + harness + .chain + .process_chain_segment(vec![], CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import empty chain segment"); + + harness + .chain + .process_chain_segment( + blocks.clone(), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .into_block_error() + .expect("should import chain segment"); + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); +} + +#[tokio::test] +async fn chain_segment_varying_chunk_size() { + for chunk_size in &[1, 2, 3, 5, 31, 32, 33, 42] { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + for chunk in blocks.chunks(*chunk_size) { + harness + .chain + .process_chain_segment( + chunk.to_vec(), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .into_block_error() + .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); + } + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); + } +} + +#[tokio::test] +async fn chain_segment_non_linear_parent_roots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test with a block removed. + */ + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + blocks.remove(2); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with missing parent" + ); + + /* + * Test with a modified parent root. + */ + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + + let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); + *block.parent_root_mut() = Hash256::zero(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)).into(); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with a broken parent root link" + ); +} + +#[tokio::test] +async fn chain_segment_non_linear_slots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test where a child is lower than the parent. + */ + + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); + *block.slot_mut() = Slot::new(0); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)).into(); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has a lower slot than its child" + ); + + /* + * Test where a child is equal to the parent. + */ + + let mut blocks: Vec> = chain_segment_blocks(&chain_segment) + .into_iter() + .map(|block| block.into()) + .collect(); + let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); + *block.slot_mut() = blocks[2].slot(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)).into(); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has an equal slot to its child" + ); +} + +async fn assert_invalid_signature( + chain_segment: &[BeaconSnapshot], + harness: &BeaconChainHarness>, + block_index: usize, + snapshots: &[BeaconSnapshot], + item: &str, +) { + let blocks: Vec> = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect(); + + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid {} signature", + item + ); + + // Call fork choice to update cached head (including finalization). + harness.chain.recompute_head_at_current_slot().await; + + // Ensure the block will be rejected if imported on its own (without gossip checking). + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.clone().into()) + .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. + let _ = harness + .chain + .process_chain_segment( + ancestor_blocks, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await; + harness.chain.recompute_head_at_current_slot().await; + + let process_res = harness + .chain + .process_block( + snapshots[block_index].beacon_block.canonical_root(), + snapshots[block_index].beacon_block.clone(), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await; + assert!( + matches!(process_res, Err(BlockError::InvalidSignature)), + "should not import individual block with an invalid {} signature, got: {:?}", + item, + process_res + ); + + // NOTE: we choose not to check gossip verification here. It only checks one signature + // (proposal) and that is already tested elsewhere in this file. + // + // It's not trivial to just check gossip verification since it will start refusing + // blocks as soon as it has seen one valid proposal signature for a given (validator, + // slot) tuple. +} + +async fn get_invalid_sigs_harness( + chain_segment: &[BeaconSnapshot], +) -> BeaconChainHarness> { + let harness = get_harness(VALIDATOR_COUNT); + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + harness +} +#[tokio::test] +async fn invalid_signature_gossip_block() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Ensure the block will be rejected if imported on its own (without gossip checking). + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + // Import all the ancestors before the `block_index` block. + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect(); + harness + .chain + .process_chain_segment( + ancestor_blocks, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .into_block_error() + .expect("should import all blocks prior to the one being tested"); + let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); + assert!( + matches!( + harness + .chain + .process_block( + signed_block.canonical_root(), + Arc::new(signed_block), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await, + Err(BlockError::InvalidSignature) + ), + "should not import individual block with an invalid gossip signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_block_proposal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + let blocks: Vec> = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect::>(); + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid block signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_randao_reveal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + *block.body_mut().randao_reveal_mut() = junk_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature(&chain_segment, &harness, block_index, &snapshots, "randao").await; + } +} + +#[tokio::test] +async fn invalid_signature_proposer_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let proposer_slashing = ProposerSlashing { + signed_header_1: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + signed_header_2: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + }; + block + .body_mut() + .proposer_slashings_mut() + .push(proposer_slashing) + .expect("should update proposer slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "proposer slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attester_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let indexed_attestation = IndexedAttestation { + attesting_indices: vec![0].into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + target: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + }, + signature: junk_aggregate_signature(), + }; + let attester_slashing = AttesterSlashing { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .attester_slashings_mut() + .push(attester_slashing) + .expect("should update attester slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attester slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attestation() { + let chain_segment = get_chain_segment().await; + let mut checked_attestation = false; + + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + if let Some(attestation) = block.body_mut().attestations_mut().get_mut(0) { + attestation.signature = junk_aggregate_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attestation", + ) + .await; + checked_attestation = true; + } + } + + assert!( + checked_attestation, + "the test should check an attestation signature" + ) +} + +#[tokio::test] +async fn invalid_signature_deposit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Note: an invalid deposit signature is permitted! + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let deposit = Deposit { + proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1].into(), + data: DepositData { + pubkey: Keypair::random().pk.into(), + withdrawal_credentials: Hash256::zero(), + amount: 0, + signature: junk_signature().into(), + }, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .deposits_mut() + .push(deposit) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + let blocks: Vec> = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone().into()) + .collect(); + assert!( + !matches!( + harness + .chain + .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not throw an invalid signature error for a bad deposit signature" + ); + } +} + +#[tokio::test] +async fn invalid_signature_exit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let epoch = snapshots[block_index].beacon_state.current_epoch(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .voluntary_exits_mut() + .push(SignedVoluntaryExit { + message: VoluntaryExit { + epoch, + validator_index: 0, + }, + signature: junk_signature(), + }) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "voluntary exit", + ) + .await; + } +} + +fn unwrap_err(result: Result) -> E { + match result { + Ok(_) => panic!("called unwrap_err on Ok"), + Err(e) => e, + } +} + +#[tokio::test] +async fn block_gossip_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + let block_index = CHAIN_SEGMENT_LENGTH - 2; + + harness + .chain + .slot_clock + .set_slot(chain_segment[block_index].beacon_block.slot().as_u64()); + + // Import the ancestors prior to the block we're testing. + for snapshot in &chain_segment[0..block_index] { + let gossip_verified = harness + .chain + .verify_block_for_gossip(snapshot.beacon_block.clone().into()) + .await + .expect("should obtain gossip verified block"); + + harness + .chain + .process_block( + gossip_verified.block_root, + gossip_verified, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .expect("should import valid gossip verified block"); + } + + // Recompute the head to ensure we cache the latest view of fork choice. + harness.chain.recompute_head_at_current_slot().await; + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- + * i.e. validate that signed_beacon_block.message.slot <= current_slot (a client MAY queue + * future blocks for processing at the appropriate slot). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_block_slot = block.slot() + 1; + *block.slot_mut() = expected_block_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::FutureSlot { + present_slot, + block_slot, + } + if present_slot == expected_block_slot - 1 && block_slot == expected_block_slot + ), + "should not import a block with a future slot" + ); + + /* + * This test ensure that: + * + * Spec v0.12.1 + * + * The block is from a slot greater than the latest finalized slot -- i.e. validate that + * signed_beacon_block.message.slot > + * compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) (a client MAY choose to + * validate and store such blocks for additional purposes -- e.g. slashing detection, archive + * nodes, etc). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_finalized_slot = harness + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + *block.slot_mut() = expected_finalized_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::WouldRevertFinalizedSlot { + block_slot, + finalized_slot, + } + if block_slot == expected_finalized_slot && finalized_slot == expected_finalized_slot + ), + "should not import a block with a finalized slot" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The proposer signature, signed_beacon_block.signature, is valid with respect to the + * proposer_index pubkey. + */ + + let block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + assert!( + matches!( + unwrap_err( + harness + .chain + .verify_block_for_gossip( + Arc::new(SignedBeaconBlock::from_block(block, junk_signature())).into() + ) + .await + ), + BlockError::ProposalSignatureInvalid + ), + "should not import a block with an invalid proposal signature" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The block's parent (defined by block.parent_root) passes validation. + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = Hash256::from_low_u64_be(42); + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::ParentUnknown(block) + if block.parent_root() == parent_root + ), + "should not import a block for an unknown parent" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The current finalized_checkpoint is an ancestor of block -- i.e. get_ancestor(store, + * block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == + * store.finalized_checkpoint.root + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = chain_segment[0].beacon_block_root; + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)).into()).await), + BlockError::NotFinalizedDescendant { block_parent_root } + if block_parent_root == parent_root + ), + "should not import a block that conflicts with finality" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is proposed by the expected proposer_index for the block's slot in the context of + * the current shuffling (defined by parent_root/slot). If the proposer_index cannot + * immediately be verified against the expected shuffling, the block MAY be queued for later + * processing while proposers for the block's branch are calculated. + */ + + let mut block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + let expected_proposer = block.proposer_index(); + let other_proposer = (0..VALIDATOR_COUNT as u64) + .into_iter() + .find(|i| *i != block.proposer_index()) + .expect("there must be more than one validator in this test"); + *block.proposer_index_mut() = other_proposer; + let block = block.sign( + &generate_deterministic_keypair(other_proposer as usize).sk, + &harness.chain.canonical_head.cached_head().head_fork(), + harness.chain.genesis_validators_root, + &harness.chain.spec, + ); + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()).into()).await), + BlockError::IncorrectBlockProposer { + block, + local_shuffling, + } + if block == other_proposer && local_shuffling == expected_proposer + ), + "should not import a block with the wrong proposer index" + ); + // Check to ensure that we registered this is a valid block from this proposer. + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()).into()).await), + BlockError::RepeatProposal { + proposer, + slot, + } + if proposer == other_proposer && slot == block.message().slot() + ), + "should register any valid signature against the proposer, even if the block failed later verification" + ); + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + harness + .chain + .verify_block_for_gossip(block.into()) + .await + .is_ok(), + "the valid block should be processed" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is the first block with valid signature received for the proposer for the slot, + * signed_beacon_block.message.slot. + */ + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + matches!( + harness + .chain + .verify_block_for_gossip(block.clone().into()) + .await + .err() + .expect("should error when processing known block"), + BlockError::RepeatProposal { + proposer, + slot, + } + if proposer == block.message().proposer_index() && slot == block.message().slot() + ), + "the second proposal by this validator should be rejected" + ); +} + +#[tokio::test] +async fn verify_block_for_gossip_slashing_detection() { + let slasher_dir = tempdir().unwrap(); + let slasher = Arc::new( + Slasher::open(SlasherConfig::new(slasher_dir.path().into()), test_logger()).unwrap(), + ); + + let inner_slasher = slasher.clone(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS.to_vec()) + .fresh_ephemeral_store() + .initial_mutator(Box::new(move |builder| builder.slasher(inner_slasher))) + .mock_execution_layer() + .build(); + harness.advance_slot(); + + let state = harness.get_current_state(); + let (block1, _) = harness.make_block(state.clone(), Slot::new(1)).await; + let (block2, _) = harness.make_block(state, Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block1).into()) + .await + .unwrap(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .unwrap(); + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(block2).into()) + .await, + ); + + // Slasher should have been handed the two conflicting blocks and crafted a slashing. + slasher.process_queued(Epoch::new(0)).unwrap(); + let proposer_slashings = slasher.get_proposer_slashings(); + assert_eq!(proposer_slashings.len(), 1); + // windows won't delete the temporary directory if you don't do this.. + drop(harness); + drop(slasher); + slasher_dir.close().unwrap(); +} + +#[tokio::test] +async fn verify_block_for_gossip_doppelganger_detection() { + let harness = get_harness(VALIDATOR_COUNT); + + let state = harness.get_current_state(); + let (block, _) = harness.make_block(state.clone(), Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block).into()) + .await + .unwrap(); + let attestations = verified_block.block.message().body().attestations().clone(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .unwrap(); + + for att in attestations.iter() { + let epoch = att.data.target.epoch; + let committee = state + .get_beacon_committee(att.data.slot, att.data.index) + .unwrap(); + let indexed_attestation = get_indexed_attestation(committee.committee, att).unwrap(); + + for &index in &indexed_attestation.attesting_indices { + let index = index as usize; + + assert!(harness.chain.validator_seen_at_epoch(index, epoch)); + + // Check the correct beacon cache is populated + assert!(harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed")); + assert!(!harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed")); + assert!(!harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed")); + } + } +} + +#[tokio::test] +async fn add_base_block_to_altair_chain() { + let mut spec = MainnetEthSpec::default_spec(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + + // The Altair fork happens at epoch 1. + spec.altair_fork_epoch = Some(Epoch::new(1)); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build out all the blocks in epoch 0. + harness + .extend_chain( + slots_per_epoch as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an Altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (altair_signed_block, _) = harness.make_block(state.clone(), slot).await; + let altair_block = &altair_signed_block + .as_altair() + .expect("test expects an altair block") + .message; + let altair_body = &altair_block.body; + + // Create a Base-equivalent of `altair_block`. + let base_block = SignedBeaconBlock::Base(SignedBeaconBlockBase { + message: BeaconBlockBase { + slot: altair_block.slot, + proposer_index: altair_block.proposer_index, + parent_root: altair_block.parent_root, + state_root: altair_block.state_root, + body: BeaconBlockBodyBase { + randao_reveal: altair_body.randao_reveal.clone(), + eth1_data: altair_body.eth1_data.clone(), + graffiti: altair_body.graffiti, + proposer_slashings: altair_body.proposer_slashings.clone(), + attester_slashings: altair_body.attester_slashings.clone(), + attestations: altair_body.attestations.clone(), + deposits: altair_body.deposits.clone(), + voluntary_exits: altair_body.voluntary_exits.clone(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(base_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &base_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(base_block.clone()).into()) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + base_block.canonical_root(), + Arc::new(base_block.clone()), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment( + vec![Arc::new(base_block).into()], + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + } + )); +} + +#[tokio::test] +async fn add_altair_block_to_base_chain() { + let mut spec = MainnetEthSpec::default_spec(); + + // Altair never happens. + spec.altair_fork_epoch = None; + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build one block. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (base_signed_block, _) = harness.make_block(state.clone(), slot).await; + let base_block = &base_signed_block + .as_base() + .expect("test expects a base block") + .message; + let base_body = &base_block.body; + + // Create an Altair-equivalent of `altair_block`. + let altair_block = SignedBeaconBlock::Altair(SignedBeaconBlockAltair { + message: BeaconBlockAltair { + slot: base_block.slot, + proposer_index: base_block.proposer_index, + parent_root: base_block.parent_root, + state_root: base_block.state_root, + body: BeaconBlockBodyAltair { + randao_reveal: base_body.randao_reveal.clone(), + eth1_data: base_body.eth1_data.clone(), + graffiti: base_body.graffiti, + proposer_slashings: base_body.proposer_slashings.clone(), + attester_slashings: base_body.attester_slashings.clone(), + attestations: base_body.attestations.clone(), + deposits: base_body.deposits.clone(), + voluntary_exits: base_body.voluntary_exits.clone(), + sync_aggregate: SyncAggregate::empty(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(altair_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &altair_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(altair_block.clone()).into()) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + altair_block.canonical_root(), + Arc::new(altair_block.clone()), + CountUnrealized::True, + NotifyExecutionLayer::Yes, + ) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment( + vec![Arc::new(altair_block).into()], + CountUnrealized::True, + NotifyExecutionLayer::Yes + ) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + } + )); +} diff --git a/beacon_node/beacon_chain/tests/block_verification_REMOTE_1058.rs b/beacon_node/beacon_chain/tests/block_verification_REMOTE_1058.rs new file mode 100644 index 00000000000..0b87ad1487a --- /dev/null +++ b/beacon_node/beacon_chain/tests/block_verification_REMOTE_1058.rs @@ -0,0 +1,1446 @@ +#![cfg(not(debug_assertions))] + +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, +}; +use beacon_chain::{ + BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock, + NotifyExecutionLayer, +}; +use lazy_static::lazy_static; +use logging::test_logger; +use slasher::{Config as SlasherConfig, Slasher}; +use state_processing::{ + common::get_indexed_attestation, + per_block_processing::{per_block_processing, BlockSignatureStrategy}, + per_slot_processing, BlockProcessingError, ConsensusContext, StateProcessingStrategy, + VerifyBlockRoot, +}; +use std::marker::PhantomData; +use std::sync::Arc; +use tempfile::tempdir; +use types::{test_utils::generate_deterministic_keypair, *}; + +type E = MainnetEthSpec; + +// Should ideally be divisible by 3. +const VALIDATOR_COUNT: usize = 24; +const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; +const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; + +lazy_static! { + /// A cached set of keys. + static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); +} + +async fn get_chain_segment() -> Vec> { + let harness = get_harness(VALIDATOR_COUNT); + + harness + .extend_chain( + CHAIN_SEGMENT_LENGTH, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut segment = Vec::with_capacity(CHAIN_SEGMENT_LENGTH); + for snapshot in harness + .chain + .chain_dump() + .expect("should dump chain") + .into_iter() + .skip(1) + { + let full_block = harness + .chain + .get_block(&snapshot.beacon_block_root) + .await + .unwrap() + .unwrap(); + segment.push(BeaconSnapshot { + beacon_block_root: snapshot.beacon_block_root, + beacon_block: Arc::new(full_block), + beacon_state: snapshot.beacon_state, + }); + } + segment +} + +fn get_harness(validator_count: usize) -> BeaconChainHarness> { + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +fn chain_segment_blocks(chain_segment: &[BeaconSnapshot]) -> Vec>> { + chain_segment + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect() +} + +fn junk_signature() -> Signature { + let kp = generate_deterministic_keypair(VALIDATOR_COUNT); + let message = Hash256::from_slice(&[42; 32]); + kp.sk.sign(message) +} + +fn junk_aggregate_signature() -> AggregateSignature { + let mut agg_sig = AggregateSignature::empty(); + agg_sig.add_assign(&junk_signature()); + agg_sig +} + +fn update_proposal_signatures( + snapshots: &mut [BeaconSnapshot], + harness: &BeaconChainHarness>, +) { + for snapshot in snapshots { + let spec = &harness.chain.spec; + let slot = snapshot.beacon_block.slot(); + let state = &snapshot.beacon_state; + let proposer_index = state + .get_beacon_proposer_index(slot, spec) + .expect("should find proposer index"); + let keypair = harness + .validator_keypairs + .get(proposer_index) + .expect("proposer keypair should be available"); + + let (block, _) = snapshot.beacon_block.as_ref().clone().deconstruct(); + snapshot.beacon_block = Arc::new(block.sign( + &keypair.sk, + &state.fork(), + state.genesis_validators_root(), + spec, + )); + } +} + +fn update_parent_roots(snapshots: &mut [BeaconSnapshot]) { + for i in 0..snapshots.len() { + let root = snapshots[i].beacon_block.canonical_root(); + if let Some(child) = snapshots.get_mut(i + 1) { + let (mut block, signature) = child.beacon_block.as_ref().clone().deconstruct(); + *block.parent_root_mut() = root; + child.beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)) + } + } +} + +#[tokio::test] +async fn chain_segment_full_segment() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks = chain_segment_blocks(&chain_segment); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + // Sneak in a little check to ensure we can process empty chain segments. + harness + .chain + .process_chain_segment(vec![], NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import empty chain segment"); + + harness + .chain + .process_chain_segment(blocks.clone(), NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import chain segment"); + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); +} + +#[tokio::test] +async fn chain_segment_varying_chunk_size() { + for chunk_size in &[1, 2, 3, 5, 31, 32, 33, 42] { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + let blocks = chain_segment_blocks(&chain_segment); + + harness + .chain + .slot_clock + .set_slot(blocks.last().unwrap().slot().as_u64()); + + for chunk in blocks.chunks(*chunk_size) { + harness + .chain + .process_chain_segment(chunk.to_vec(), NotifyExecutionLayer::Yes) + .await + .into_block_error() + .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); + } + + harness.chain.recompute_head_at_current_slot().await; + + assert_eq!( + harness.head_block_root(), + blocks.last().unwrap().canonical_root(), + "harness should have last block as head" + ); + } +} + +#[tokio::test] +async fn chain_segment_non_linear_parent_roots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test with a block removed. + */ + let mut blocks = chain_segment_blocks(&chain_segment); + blocks.remove(2); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with missing parent" + ); + + /* + * Test with a modified parent root. + */ + let mut blocks = chain_segment_blocks(&chain_segment); + let (mut block, signature) = blocks[3].as_ref().clone().deconstruct(); + *block.parent_root_mut() = Hash256::zero(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearParentRoots) + ), + "should not import chain with a broken parent root link" + ); +} + +#[tokio::test] +async fn chain_segment_non_linear_slots() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + + /* + * Test where a child is lower than the parent. + */ + + let mut blocks = chain_segment_blocks(&chain_segment); + let (mut block, signature) = blocks[3].as_ref().clone().deconstruct(); + *block.slot_mut() = Slot::new(0); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has a lower slot than its child" + ); + + /* + * Test where a child is equal to the parent. + */ + + let mut blocks = chain_segment_blocks(&chain_segment); + let (mut block, signature) = blocks[3].as_ref().clone().deconstruct(); + *block.slot_mut() = blocks[2].slot(); + blocks[3] = Arc::new(SignedBeaconBlock::from_block(block, signature)); + + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::NonLinearSlots) + ), + "should not import chain with a parent that has an equal slot to its child" + ); +} + +async fn assert_invalid_signature( + chain_segment: &[BeaconSnapshot], + harness: &BeaconChainHarness>, + block_index: usize, + snapshots: &[BeaconSnapshot], + item: &str, +) { + let blocks = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect(); + + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid {} signature", + item + ); + + // Call fork choice to update cached head (including finalization). + harness.chain.recompute_head_at_current_slot().await; + + // Ensure the block will be rejected if imported on its own (without gossip checking). + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.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. + let _ = harness + .chain + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) + .await; + harness.chain.recompute_head_at_current_slot().await; + + let process_res = harness + .chain + .process_block( + snapshots[block_index].beacon_block.canonical_root(), + snapshots[block_index].beacon_block.clone(), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await; + assert!( + matches!(process_res, Err(BlockError::InvalidSignature)), + "should not import individual block with an invalid {} signature, got: {:?}", + item, + process_res + ); + + // NOTE: we choose not to check gossip verification here. It only checks one signature + // (proposal) and that is already tested elsewhere in this file. + // + // It's not trivial to just check gossip verification since it will start refusing + // blocks as soon as it has seen one valid proposal signature for a given (validator, + // slot) tuple. +} + +async fn get_invalid_sigs_harness( + chain_segment: &[BeaconSnapshot], +) -> BeaconChainHarness> { + let harness = get_harness(VALIDATOR_COUNT); + harness + .chain + .slot_clock + .set_slot(chain_segment.last().unwrap().beacon_block.slot().as_u64()); + harness +} +#[tokio::test] +async fn invalid_signature_gossip_block() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Ensure the block will be rejected if imported on its own (without gossip checking). + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + // Import all the ancestors before the `block_index` block. + let ancestor_blocks = chain_segment + .iter() + .take(block_index) + .map(|snapshot| snapshot.beacon_block.clone()) + .collect(); + harness + .chain + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error() + .expect("should import all blocks prior to the one being tested"); + let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); + assert!( + matches!( + harness + .chain + .process_block( + signed_block.canonical_root(), + Arc::new(signed_block), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await, + Err(BlockError::InvalidSignature) + ), + "should not import individual block with an invalid gossip signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_block_proposal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (block, _) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block( + block.clone(), + junk_signature(), + )); + let blocks = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect::>(); + // Ensure the block will be rejected if imported in a chain segment. + assert!( + matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not import chain segment with an invalid block signature", + ); + } +} + +#[tokio::test] +async fn invalid_signature_randao_reveal() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + *block.body_mut().randao_reveal_mut() = junk_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature(&chain_segment, &harness, block_index, &snapshots, "randao").await; + } +} + +#[tokio::test] +async fn invalid_signature_proposer_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let proposer_slashing = ProposerSlashing { + signed_header_1: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + signed_header_2: SignedBeaconBlockHeader { + message: block.block_header(), + signature: junk_signature(), + }, + }; + block + .body_mut() + .proposer_slashings_mut() + .push(proposer_slashing) + .expect("should update proposer slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "proposer slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attester_slashing() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let indexed_attestation = IndexedAttestation { + attesting_indices: vec![0].into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + target: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + }, + signature: junk_aggregate_signature(), + }; + let attester_slashing = AttesterSlashing { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .attester_slashings_mut() + .push(attester_slashing) + .expect("should update attester slashing"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attester slashing", + ) + .await; + } +} + +#[tokio::test] +async fn invalid_signature_attestation() { + let chain_segment = get_chain_segment().await; + let mut checked_attestation = false; + + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + if let Some(attestation) = block.body_mut().attestations_mut().get_mut(0) { + attestation.signature = junk_aggregate_signature(); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "attestation", + ) + .await; + checked_attestation = true; + } + } + + assert!( + checked_attestation, + "the test should check an attestation signature" + ) +} + +#[tokio::test] +async fn invalid_signature_deposit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + // Note: an invalid deposit signature is permitted! + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let deposit = Deposit { + proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1].into(), + data: DepositData { + pubkey: Keypair::random().pk.into(), + withdrawal_credentials: Hash256::zero(), + amount: 0, + signature: junk_signature().into(), + }, + }; + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .deposits_mut() + .push(deposit) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + let blocks = snapshots + .iter() + .map(|snapshot| snapshot.beacon_block.clone()) + .collect(); + assert!( + !matches!( + harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(), + Err(BlockError::InvalidSignature) + ), + "should not throw an invalid signature error for a bad deposit signature" + ); + } +} + +#[tokio::test] +async fn invalid_signature_exit() { + let chain_segment = get_chain_segment().await; + for &block_index in BLOCK_INDICES { + let harness = get_invalid_sigs_harness(&chain_segment).await; + let mut snapshots = chain_segment.clone(); + let epoch = snapshots[block_index].beacon_state.current_epoch(); + let (mut block, signature) = snapshots[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + block + .body_mut() + .voluntary_exits_mut() + .push(SignedVoluntaryExit { + message: VoluntaryExit { + epoch, + validator_index: 0, + }, + signature: junk_signature(), + }) + .expect("should update deposit"); + snapshots[block_index].beacon_block = + Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_parent_roots(&mut snapshots); + update_proposal_signatures(&mut snapshots, &harness); + assert_invalid_signature( + &chain_segment, + &harness, + block_index, + &snapshots, + "voluntary exit", + ) + .await; + } +} + +fn unwrap_err(result: Result) -> E { + match result { + Ok(_) => panic!("called unwrap_err on Ok"), + Err(e) => e, + } +} + +#[tokio::test] +async fn block_gossip_verification() { + let harness = get_harness(VALIDATOR_COUNT); + let chain_segment = get_chain_segment().await; + + let block_index = CHAIN_SEGMENT_LENGTH - 2; + + harness + .chain + .slot_clock + .set_slot(chain_segment[block_index].beacon_block.slot().as_u64()); + + // Import the ancestors prior to the block we're testing. + for snapshot in &chain_segment[0..block_index] { + let gossip_verified = harness + .chain + .verify_block_for_gossip(snapshot.beacon_block.clone()) + .await + .expect("should obtain gossip verified block"); + + harness + .chain + .process_block( + gossip_verified.block_root, + gossip_verified, + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .expect("should import valid gossip verified block"); + } + + // Recompute the head to ensure we cache the latest view of fork choice. + harness.chain.recompute_head_at_current_slot().await; + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- + * i.e. validate that signed_beacon_block.message.slot <= current_slot (a client MAY queue + * future blocks for processing at the appropriate slot). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_block_slot = block.slot() + 1; + *block.slot_mut() = expected_block_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::FutureSlot { + present_slot, + block_slot, + } + if present_slot == expected_block_slot - 1 && block_slot == expected_block_slot + ), + "should not import a block with a future slot" + ); + + /* + * This test ensure that: + * + * Spec v0.12.1 + * + * The block is from a slot greater than the latest finalized slot -- i.e. validate that + * signed_beacon_block.message.slot > + * compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) (a client MAY choose to + * validate and store such blocks for additional purposes -- e.g. slashing detection, archive + * nodes, etc). + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let expected_finalized_slot = harness + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + *block.slot_mut() = expected_finalized_slot; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::WouldRevertFinalizedSlot { + block_slot, + finalized_slot, + } + if block_slot == expected_finalized_slot && finalized_slot == expected_finalized_slot + ), + "should not import a block with a finalized slot" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The proposer signature, signed_beacon_block.signature, is valid with respect to the + * proposer_index pubkey. + */ + + let block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + assert!( + matches!( + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block( + block, + junk_signature() + ))) + .await + ), + BlockError::ProposalSignatureInvalid + ), + "should not import a block with an invalid proposal signature" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The block's parent (defined by block.parent_root) passes validation. + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = Hash256::from_low_u64_be(42); + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::ParentUnknown(block) + if block.parent_root() == parent_root + ), + "should not import a block for an unknown parent" + ); + + /* + * This test ensures that: + * + * Spec v0.12.2 + * + * The current finalized_checkpoint is an ancestor of block -- i.e. get_ancestor(store, + * block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == + * store.finalized_checkpoint.root + */ + + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + let parent_root = chain_segment[0].beacon_block_root; + *block.parent_root_mut() = parent_root; + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::NotFinalizedDescendant { block_parent_root } + if block_parent_root == parent_root + ), + "should not import a block that conflicts with finality" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is proposed by the expected proposer_index for the block's slot in the context of + * the current shuffling (defined by parent_root/slot). If the proposer_index cannot + * immediately be verified against the expected shuffling, the block MAY be queued for later + * processing while proposers for the block's branch are calculated. + */ + + let mut block = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct() + .0; + let expected_proposer = block.proposer_index(); + let other_proposer = (0..VALIDATOR_COUNT as u64) + .into_iter() + .find(|i| *i != block.proposer_index()) + .expect("there must be more than one validator in this test"); + *block.proposer_index_mut() = other_proposer; + let block = block.sign( + &generate_deterministic_keypair(other_proposer as usize).sk, + &harness.chain.canonical_head.cached_head().head_fork(), + harness.chain.genesis_validators_root, + &harness.chain.spec, + ); + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), + BlockError::IncorrectBlockProposer { + block, + local_shuffling, + } + if block == other_proposer && local_shuffling == expected_proposer + ), + "should not import a block with the wrong proposer index" + ); + // Check to ensure that we registered this is a valid block from this proposer. + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), + BlockError::BlockIsAlreadyKnown, + ), + "should register any valid signature against the proposer, even if the block failed later verification" + ); + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + harness.chain.verify_block_for_gossip(block).await.is_ok(), + "the valid block should be processed" + ); + + /* + * This test ensures that: + * + * Spec v0.12.1 + * + * The block is the first block with valid signature received for the proposer for the slot, + * signed_beacon_block.message.slot. + */ + + let block = chain_segment[block_index].beacon_block.clone(); + assert!( + matches!( + harness + .chain + .verify_block_for_gossip(block.clone()) + .await + .err() + .expect("should error when processing known block"), + BlockError::BlockIsAlreadyKnown + ), + "the second proposal by this validator should be rejected" + ); +} + +#[tokio::test] +async fn verify_block_for_gossip_slashing_detection() { + let slasher_dir = tempdir().unwrap(); + let slasher = Arc::new( + Slasher::open(SlasherConfig::new(slasher_dir.path().into()), test_logger()).unwrap(), + ); + + let inner_slasher = slasher.clone(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS.to_vec()) + .fresh_ephemeral_store() + .initial_mutator(Box::new(move |builder| builder.slasher(inner_slasher))) + .mock_execution_layer() + .build(); + harness.advance_slot(); + + let state = harness.get_current_state(); + let (block1, _) = harness.make_block(state.clone(), Slot::new(1)).await; + let (block2, _) = harness.make_block(state, Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block1)) + .await + .unwrap(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .unwrap(); + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(block2)) + .await, + ); + + // Slasher should have been handed the two conflicting blocks and crafted a slashing. + slasher.process_queued(Epoch::new(0)).unwrap(); + let proposer_slashings = slasher.get_proposer_slashings(); + assert_eq!(proposer_slashings.len(), 1); + // windows won't delete the temporary directory if you don't do this.. + drop(harness); + drop(slasher); + slasher_dir.close().unwrap(); +} + +#[tokio::test] +async fn verify_block_for_gossip_doppelganger_detection() { + let harness = get_harness(VALIDATOR_COUNT); + + let state = harness.get_current_state(); + let (block, _) = harness.make_block(state.clone(), Slot::new(1)).await; + + let verified_block = harness + .chain + .verify_block_for_gossip(Arc::new(block)) + .await + .unwrap(); + let attestations = verified_block.block.message().body().attestations().clone(); + harness + .chain + .process_block( + verified_block.block_root, + verified_block, + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .unwrap(); + + for att in attestations.iter() { + let epoch = att.data.target.epoch; + let committee = state + .get_beacon_committee(att.data.slot, att.data.index) + .unwrap(); + let indexed_attestation = get_indexed_attestation(committee.committee, att).unwrap(); + + for &index in &indexed_attestation.attesting_indices { + let index = index as usize; + + assert!(harness.chain.validator_seen_at_epoch(index, epoch)); + + // Check the correct beacon cache is populated + assert!(harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed")); + assert!(!harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed")); + assert!(!harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed")); + } + } +} + +#[tokio::test] +async fn add_base_block_to_altair_chain() { + let mut spec = MainnetEthSpec::default_spec(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + + // The Altair fork happens at epoch 1. + spec.altair_fork_epoch = Some(Epoch::new(1)); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build out all the blocks in epoch 0. + harness + .extend_chain( + slots_per_epoch as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an Altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (altair_signed_block, _) = harness.make_block(state.clone(), slot).await; + let altair_block = &altair_signed_block + .as_altair() + .expect("test expects an altair block") + .message; + let altair_body = &altair_block.body; + + // Create a Base-equivalent of `altair_block`. + let base_block = SignedBeaconBlock::Base(SignedBeaconBlockBase { + message: BeaconBlockBase { + slot: altair_block.slot, + proposer_index: altair_block.proposer_index, + parent_root: altair_block.parent_root, + state_root: altair_block.state_root, + body: BeaconBlockBodyBase { + randao_reveal: altair_body.randao_reveal.clone(), + eth1_data: altair_body.eth1_data.clone(), + graffiti: altair_body.graffiti, + proposer_slashings: altair_body.proposer_slashings.clone(), + attester_slashings: altair_body.attester_slashings.clone(), + attestations: altair_body.attestations.clone(), + deposits: altair_body.deposits.clone(), + voluntary_exits: altair_body.voluntary_exits.clone(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(base_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &base_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(base_block.clone())) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + base_block.canonical_root(), + Arc::new(base_block.clone()), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .err() + .expect("should error when processing base block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment(vec![Arc::new(base_block)], NotifyExecutionLayer::Yes,) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Altair, + object_fork: ForkName::Base, + }) + } + )); +} + +#[tokio::test] +async fn add_altair_block_to_base_chain() { + let mut spec = MainnetEthSpec::default_spec(); + + // Altair never happens. + spec.altair_fork_epoch = None; + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build one block. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // Produce an altair block. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (base_signed_block, _) = harness.make_block(state.clone(), slot).await; + let base_block = &base_signed_block + .as_base() + .expect("test expects a base block") + .message; + let base_body = &base_block.body; + + // Create an Altair-equivalent of `altair_block`. + let altair_block = SignedBeaconBlock::Altair(SignedBeaconBlockAltair { + message: BeaconBlockAltair { + slot: base_block.slot, + proposer_index: base_block.proposer_index, + parent_root: base_block.parent_root, + state_root: base_block.state_root, + body: BeaconBlockBodyAltair { + randao_reveal: base_body.randao_reveal.clone(), + eth1_data: base_body.eth1_data.clone(), + graffiti: base_body.graffiti, + proposer_slashings: base_body.proposer_slashings.clone(), + attester_slashings: base_body.attester_slashings.clone(), + attestations: base_body.attestations.clone(), + deposits: base_body.deposits.clone(), + voluntary_exits: base_body.voluntary_exits.clone(), + sync_aggregate: SyncAggregate::empty(), + _phantom: PhantomData, + }, + }, + signature: Signature::empty(), + }); + + // Ensure that it would be impossible to apply this block to `per_block_processing`. + { + let mut state = state; + let mut ctxt = ConsensusContext::new(altair_block.slot()); + per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + assert!(matches!( + per_block_processing( + &mut state, + &altair_block, + BlockSignatureStrategy::NoVerification, + StateProcessingStrategy::Accurate, + VerifyBlockRoot::True, + &mut ctxt, + &harness.chain.spec, + ), + Err(BlockProcessingError::InconsistentBlockFork( + InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + } + )) + )); + } + + // Ensure that it would be impossible to verify this block for gossip. + assert!(matches!( + harness + .chain + .verify_block_for_gossip(Arc::new(altair_block.clone())) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_block`. + assert!(matches!( + harness + .chain + .process_block( + altair_block.canonical_root(), + Arc::new(altair_block.clone()), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .err() + .expect("should error when processing altair block"), + BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + )); + + // Ensure that it would be impossible to import via `BeaconChain::process_chain_segment`. + assert!(matches!( + harness + .chain + .process_chain_segment(vec![Arc::new(altair_block)], NotifyExecutionLayer::Yes) + .await, + ChainSegmentResult::Failed { + imported_blocks: 0, + error: BlockError::InconsistentFork(InconsistentFork { + fork_at_slot: ForkName::Base, + object_fork: ForkName::Altair, + }) + } + )); +} + +#[tokio::test] +async fn import_duplicate_block_unrealized_justification() { + let spec = MainnetEthSpec::default_spec(); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let chain = &harness.chain; + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build the chain out to the first justification opportunity 2/3rds of the way through epoch 2. + let num_slots = E::slots_per_epoch() as usize * 8 / 3; + harness + .extend_chain( + num_slots, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // The store's justified checkpoint must still be at epoch 0, while unrealized justification + // must be at epoch 1. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.unrealized_justified_checkpoint().epoch, 1); + drop(fc); + + // Produce a block to justify epoch 2. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (block, _) = harness.make_block(state.clone(), slot).await; + let block = Arc::new(block); + let block_root = block.canonical_root(); + + // Create two verified variants of the block, representing the same block being processed in + // parallel. + let notify_execution_layer = NotifyExecutionLayer::Yes; + let verified_block1 = block + .clone() + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + let verified_block2 = block + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + + // Import the first block, simulating a block processed via a finalized chain segment. + chain + .clone() + .import_execution_pending_block(verified_block1) + .await + .unwrap(); + + // Unrealized justification should NOT have updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + let unrealized_justification = fc.unrealized_justified_checkpoint(); + assert_eq!(unrealized_justification.epoch, 2); + + // The fork choice node for the block should have unrealized justification. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); + drop(fc); + + // Import the second verified block, simulating a block processed via RPC. + chain + .clone() + .import_execution_pending_block(verified_block2) + .await + .unwrap(); + + // Unrealized justification should still be updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!( + fc.unrealized_justified_checkpoint(), + unrealized_justification + ); + + // The fork choice node for the block should still have the unrealized justified checkpoint. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); +}