From 55faed32c61a55b34ef51ead0ab463a04bcb8bab Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:37:28 -0700 Subject: [PATCH 1/7] Add forged block test --- Cargo.lock | 1 + Cargo.toml | 3 ++ vm/cli/mod.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 01a7d51dba..1224163ee0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2553,6 +2553,7 @@ dependencies = [ name = "snarkvm" version = "0.16.19" dependencies = [ + "aleo-std", "anstyle", "anyhow", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 2140fafde8..4f85ae3626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -276,6 +276,9 @@ version = "3.8" [build-dependencies.walkdir] version = "2" +[dev-dependencies.aleo-std] +version = "0.1.24" + [profile.release] opt-level = 3 lto = "thin" diff --git a/vm/cli/mod.rs b/vm/cli/mod.rs index 765e3d8ea7..c18e5f036f 100644 --- a/vm/cli/mod.rs +++ b/vm/cli/mod.rs @@ -23,3 +23,122 @@ pub use errors::*; pub mod helpers; pub use helpers::*; + +// TODO (raychu86): DO NOT IMPLEMENT THESE TESTS HERE. Add them to Ledger or Synthesizer. +#[cfg(test)] +mod tests { + use crate::{ + console::network::TestnetV0, + ledger::{ + authority::Authority, + narwhal::{Data, Subdag, Transmission, TransmissionID}, + store::helpers::memory::ConsensusMemory, + Block, + Ledger, + }, + }; + + use aleo_std::StorageMode; + use core::ops::Deref; + use indexmap::IndexMap; + + fn extract_transmissions(block: &Block) -> IndexMap, Transmission> { + let mut transmissions = IndexMap::new(); + for tx in block.transactions().iter() { + let checksum = Data::Object(tx.transaction().clone()).to_checksum::().unwrap(); + transmissions.insert(TransmissionID::from((&tx.id(), &checksum)), tx.transaction().clone().into()); + } + if let Some(coinbase_solution) = block.solutions().as_ref() { + for (_, solution) in coinbase_solution.iter() { + let checksum = Data::Object(*solution).to_checksum::().unwrap(); + transmissions.insert(TransmissionID::from((solution.id(), checksum)), (*solution).into()); + } + } + transmissions + } + + #[test] + fn test_forged_block() { + // TODO (raychu86): DO NOT USE REAL BLOCKS. CREATE BLOCKS FROM SCRATCH FOR TESTING. + let genesis: Block = + ureq::get("https://api.explorer.aleo.org/v1/testnet/block/0").call().unwrap().into_json().unwrap(); + let block_1: Block = + ureq::get("https://api.explorer.aleo.org/v1/testnet/block/1").call().unwrap().into_json().unwrap(); + let block_2: Block = + ureq::get("https://api.explorer.aleo.org/v1/testnet/block/2").call().unwrap().into_json().unwrap(); + let block_3: Block = + ureq::get("https://api.explorer.aleo.org/v1/testnet/block/3").call().unwrap().into_json().unwrap(); + + let ledger = + Ledger::>::load(genesis.clone(), StorageMode::Development(111)) + .unwrap(); + ledger.advance_to_next_block(&block_1).unwrap(); + + //////////////////////////////////////////////////////////////////////////// + // Attack 1: Forge block 2' with the subdag of block 3. + //////////////////////////////////////////////////////////////////////////// + { + println!("\n\n\nPERFORMING ATTACK 1: FORGING BLOCK 2' WITH THE SUBDAG OF BLOCK 3"); + + let block_3_subdag = + if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; + + // Fetch the transmissions. + let transmissions = extract_transmissions(&block_3); + + // Forge the block. + let forged_block_2 = ledger + .prepare_advance_to_next_quorum_block(block_3_subdag.clone(), transmissions, &mut rand::thread_rng()) + .unwrap(); + + assert_ne!(forged_block_2, block_2); + + // Attempt to verify the forged block. + assert!(ledger.check_next_block(&forged_block_2, &mut rand::thread_rng()).is_err()); + } + + //////////////////////////////////////////////////////////////////////////// + // Attack 2: Forge block 2' with the combined subdag of block 2 and 3. + //////////////////////////////////////////////////////////////////////////// + { + println!("\n\n\nPERFORMING ATTACK 2: FORGING BLOCK 2' WITH THE COMBINED SUBDAG OF BLOCK 2 AND 3"); + + // Fetch the subdags. + let block_2_subdag = + if let Authority::Quorum(subdag) = block_2.authority() { subdag } else { unreachable!("") }; + let block_3_subdag = + if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; + + // Combined the subdags. + let mut combined_subdag = block_2_subdag.deref().clone(); + for (round, certificates) in block_3_subdag.iter() { + combined_subdag + .entry(*round) + .and_modify(|c| c.extend(certificates.clone())) + .or_insert(certificates.clone()); + } + + // Fetch the transmissions. + let block_2_transmissions = extract_transmissions(&block_2); + let block_3_transmissions = extract_transmissions(&block_3); + + // Combine the transmissions. + let mut combined_transmissions = block_2_transmissions; + combined_transmissions.extend(block_3_transmissions); + + // Forge the block. + let forged_block_2_from_both_subdags = ledger + .prepare_advance_to_next_quorum_block( + Subdag::from(combined_subdag).unwrap(), + combined_transmissions, + &mut rand::thread_rng(), + ) + .unwrap(); + + assert_ne!(forged_block_2_from_both_subdags, block_1); + + // Attempt to verify the forged block. + assert!(ledger.check_next_block(&forged_block_2_from_both_subdags, &mut rand::thread_rng()).is_err()); + } + } +} From aefccaa91f14a005abf31df325acf21c3a1b50a6 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:40:36 -0700 Subject: [PATCH 2/7] Add check for consequtive rounds --- ledger/block/src/verify.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ledger/block/src/verify.rs b/ledger/block/src/verify.rs index 5fdf77ff92..402b66ce4f 100644 --- a/ledger/block/src/verify.rs +++ b/ledger/block/src/verify.rs @@ -173,6 +173,20 @@ impl Block { subdag.anchor_round(), previous_round ); + // Ensure that the rounds in the subdag are sequential. + if previous_round != 0 { + for round in previous_round..=subdag.anchor_round() { + ensure!( + subdag.contains_key(&round), + "Subdag does not contain round {} in block {}", + round, + expected_height + ); + } + } + + // TODO (raychu86): Add is_linked checks + // Output the subdag anchor round. subdag.anchor_round() } From 0ead66bbea1a61828c3357be516cb1046581d468 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:40:41 -0700 Subject: [PATCH 3/7] Add is_linked checks in subdag --- ledger/block/src/verify.rs | 7 +-- ledger/src/check_next_block.rs | 79 ++++++++++++++++++++++++++++++++++ vm/cli/mod.rs | 4 +- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/ledger/block/src/verify.rs b/ledger/block/src/verify.rs index 402b66ce4f..c5fe4c1c22 100644 --- a/ledger/block/src/verify.rs +++ b/ledger/block/src/verify.rs @@ -178,15 +178,10 @@ impl Block { for round in previous_round..=subdag.anchor_round() { ensure!( subdag.contains_key(&round), - "Subdag does not contain round {} in block {}", - round, - expected_height + "Subdag is missing round {round} in block {expected_height}", ); } } - - // TODO (raychu86): Add is_linked checks - // Output the subdag anchor round. subdag.anchor_round() } diff --git a/ledger/src/check_next_block.rs b/ledger/src/check_next_block.rs index 77752b478f..ad757b2ff4 100644 --- a/ledger/src/check_next_block.rs +++ b/ledger/src/check_next_block.rs @@ -117,6 +117,9 @@ impl> Ledger { ratified_finalize_operations, )?; + // Determine if the block subdag is correctly constructed and is not a combination of multiple subdags. + self.check_block_subdag_atomicity(block)?; + // Ensure that each existing solution ID from the block exists in the ledger. for existing_solution_id in expected_existing_solution_ids { if !self.contains_solution_id(&existing_solution_id)? { @@ -133,4 +136,80 @@ impl> Ledger { Ok(()) } + + /// Checks that the block subdag can not be split into multiple valid subdags. + fn check_block_subdag_atomicity(&self, block: &Block) -> Result<()> { + // Returns `true` if there is a path from the previous certificate to the current certificate. + fn is_linked( + subdag: &Subdag, + previous_certificate: &BatchCertificate, + current_certificate: &BatchCertificate, + ) -> Result { + // Initialize the list containing the traversal. + let mut traversal = vec![current_certificate]; + // Iterate over the rounds from the current certificate to the previous certificate. + for round in (previous_certificate.round()..current_certificate.round()).rev() { + // Retrieve all of the certificates for this past round. + let certificates = subdag.get(&round).ok_or(anyhow!("No certificates found for round {round}"))?; + // Filter the certificates to only include those that are in the traversal. + traversal = certificates + .into_iter() + .filter(|p| traversal.iter().any(|c| c.previous_certificate_ids().contains(&p.id()))) + .collect(); + } + Ok(traversal.contains(&previous_certificate)) + } + + // Check if the block has a subdag. + let subdag = match block.authority() { + Authority::Quorum(subdag) => subdag, + _ => return Ok(()), + }; + + // Iterate over the rounds to find possible leader certificates. + for round in (self.latest_round().saturating_add(2)..=subdag.anchor_round().saturating_sub(2)).rev().step_by(2) + { + // Retrieve the previous committee lookback. + let previous_committee_lookback = { + // Determine the round number for the previous committee. + let previous_round = match round % 2 == 0 { + true => round.saturating_sub(1), + false => round.saturating_sub(2), + }; + // Determine the previous committee lookback round. + let committee_lookback_round = previous_round.saturating_sub(Committee::::COMMITTEE_LOOKBACK_RANGE); + // Output the previous committee lookback. + self.get_committee_for_round(committee_lookback_round) + .and_then(|opt| { + opt.ok_or_else(|| anyhow!("No committee found for lookback round {committee_lookback_round}")) + }) + .map_err(|e| anyhow!("Failed to retrieve previous committee for round {round}: {e}"))? + }; + + // Compute the leader for the commit round. + let computed_leader = previous_committee_lookback + .get_leader(round) + .map_err(|e| anyhow!("Failed to compute leader for round {round}: {e}"))?; + + // Retrieve the previous leader certificates. + let previous_certificate = match subdag.get(&round).and_then(|certificates| { + certificates.iter().find(|certificate| certificate.author() == computed_leader) + }) { + Some(cert) => cert, + None => continue, + }; + + // Determine if there is a path between the previous certificate and the subdag's leader certificate. + if is_linked(subdag, previous_certificate, subdag.leader_certificate())? { + bail!( + "The previous certificate should not be linked to the current certificate in block {}", + block.height() + ); + } + + // Instead of doing leader checks. Do a is_linked check for all certificates? + } + + Ok(()) + } } diff --git a/vm/cli/mod.rs b/vm/cli/mod.rs index c18e5f036f..9437f8cdf3 100644 --- a/vm/cli/mod.rs +++ b/vm/cli/mod.rs @@ -78,7 +78,7 @@ mod tests { // Attack 1: Forge block 2' with the subdag of block 3. //////////////////////////////////////////////////////////////////////////// { - println!("\n\n\nPERFORMING ATTACK 1: FORGING BLOCK 2' WITH THE SUBDAG OF BLOCK 3"); + println!("PERFORMING ATTACK 1: FORGING BLOCK 2' WITH THE SUBDAG OF BLOCK 3"); let block_3_subdag = if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; @@ -101,7 +101,7 @@ mod tests { // Attack 2: Forge block 2' with the combined subdag of block 2 and 3. //////////////////////////////////////////////////////////////////////////// { - println!("\n\n\nPERFORMING ATTACK 2: FORGING BLOCK 2' WITH THE COMBINED SUBDAG OF BLOCK 2 AND 3"); + println!("PERFORMING ATTACK 2: FORGING BLOCK 2' WITH THE COMBINED SUBDAG OF BLOCK 2 AND 3"); // Fetch the subdags. let block_2_subdag = From 2929fe155f4f2abf650c6857794b30775ea7e49d Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:28:06 -0700 Subject: [PATCH 4/7] Implement tests for blocks with forged subdags --- Cargo.lock | 1 - Cargo.toml | 3 - ledger/src/tests.rs | 235 +++++++++++++++++++++++++++++++++++++++++++- vm/cli/mod.rs | 119 ---------------------- 4 files changed, 233 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1224163ee0..01a7d51dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2553,7 +2553,6 @@ dependencies = [ name = "snarkvm" version = "0.16.19" dependencies = [ - "aleo-std", "anstyle", "anyhow", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 4f85ae3626..2140fafde8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -276,9 +276,6 @@ version = "3.8" [build-dependencies.walkdir] version = "2" -[dev-dependencies.aleo-std] -version = "0.1.24" - [profile.release] opt-level = 3 lto = "thin" diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index b16971f69a..498bb72ec0 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -25,19 +25,146 @@ use console::{ program::{Entry, Identifier, Literal, Plaintext, ProgramID, Value}, types::U16, }; -use ledger_block::{ConfirmedTransaction, Execution, Ratify, Rejected, Transaction}; +use ledger_authority::Authority; +use ledger_block::{Block, ConfirmedTransaction, Execution, Ratify, Rejected, Transaction}; use ledger_committee::{Committee, MIN_VALIDATOR_STAKE}; +use ledger_narwhal::{BatchCertificate, BatchHeader, Data, Subdag, Transmission, TransmissionID}; use ledger_store::{helpers::memory::ConsensusMemory, ConsensusStore}; use synthesizer::{program::Program, vm::VM, Stack}; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use rand::seq::SliceRandom; +use std::collections::{BTreeMap, HashMap}; +use time::OffsetDateTime; /// Initializes a sample VM. fn sample_vm() -> VM> { VM::from(ConsensusStore::>::open(None).unwrap()).unwrap() } +/// Extract the transmissions from a block. +fn extract_transmissions( + block: &Block, +) -> IndexMap, Transmission> { + let mut transmissions = IndexMap::new(); + for tx in block.transactions().iter() { + let checksum = Data::Object(tx.transaction().clone()).to_checksum::().unwrap(); + transmissions.insert(TransmissionID::from((&tx.id(), &checksum)), tx.transaction().clone().into()); + } + if let Some(coinbase_solution) = block.solutions().as_ref() { + for (_, solution) in coinbase_solution.iter() { + let checksum = Data::Object(*solution).to_checksum::().unwrap(); + transmissions.insert(TransmissionID::from((solution.id(), checksum)), (*solution).into()); + } + } + transmissions +} + +/// Construct `num_blocks` quroum blocks given a set of validator private keys and the genesis block. +fn construct_quorum_blocks( + private_keys: Vec>, + genesis: Block, + num_blocks: u64, + rng: &mut TestRng, +) -> Vec> { + // Initialize the ledger with the genesis block. + let ledger = + Ledger::>::load(genesis.clone(), StorageMode::Development(123)) + .unwrap(); + + // Initialize the round parameters. + assert!(num_blocks > 0); + assert!(num_blocks < 25); + let rounds_per_commit = 2; + let final_round = num_blocks.saturating_mul(2); + + // Sample rounds of batch certificates starting at the genesis round from a static set of 4 authors. + let (round_to_certificates_map, committee) = { + let committee = ledger.latest_committee().unwrap(); + let mut round_to_certificates_map: HashMap>> = HashMap::new(); + let mut previous_certificates: IndexSet> = IndexSet::with_capacity(4); + + for round in 1..=final_round { + let mut current_certificates = IndexSet::new(); + let previous_certificate_ids = + if round <= 1 { IndexSet::new() } else { previous_certificates.iter().map(|c| c.id()).collect() }; + + for (i, private_key_1) in private_keys.iter().enumerate() { + let batch_header = BatchHeader::new( + private_key_1, + round, + OffsetDateTime::now_utc().unix_timestamp(), + committee.id(), + Default::default(), + previous_certificate_ids.clone(), + rng, + ) + .unwrap(); + let signatures = private_keys + .iter() + .enumerate() + .filter(|&(j, _)| i != j) + .map(|(_, private_key_2)| private_key_2.sign(&[batch_header.batch_id()], rng).unwrap()) + .collect(); + current_certificates.insert(BatchCertificate::from(batch_header, signatures).unwrap()); + } + + round_to_certificates_map.insert(round, current_certificates.clone()); + previous_certificates = current_certificates; + } + (round_to_certificates_map, committee) + }; + + // Helper function to create a quroum block. + fn create_next_quroum_block( + ledger: &Ledger>, + round: u64, + leader_certificate: &BatchCertificate, + previous_leader_certificate: Option<&BatchCertificate>, + round_to_certificates_map: &HashMap>>, + rng: &mut TestRng, + ) -> Block { + let mut subdag_map = BTreeMap::new(); + subdag_map.insert(round, [leader_certificate.clone()].into()); + subdag_map.insert(round - 1, round_to_certificates_map.get(&(round - 1)).unwrap().clone()); + if let Some(prev_leader_cert) = previous_leader_certificate { + let mut previous_leader_round_certificates = + round_to_certificates_map.get(&(round - 2)).cloned().unwrap_or_default(); + previous_leader_round_certificates.shift_remove(prev_leader_cert); + subdag_map.insert(round - 2, previous_leader_round_certificates); + } + let subdag = Subdag::from(subdag_map).unwrap(); + let block = ledger.prepare_advance_to_next_quorum_block(subdag, Default::default(), rng).unwrap(); + ledger.check_next_block(&block, rng).unwrap(); + block + } + + // Track the blocks that are created. + let mut blocks = Vec::new(); + let mut previous_leader_certificate: Option<&BatchCertificate> = None; + + // Construct the blocks + for block_height in 1..=num_blocks { + let round = block_height.saturating_mul(rounds_per_commit); + let leader = committee.get_leader(round).unwrap(); + let leader_certificate = + round_to_certificates_map.get(&round).unwrap().iter().find(|c| c.author() == leader).unwrap(); + let block = create_next_quroum_block( + &ledger, + round, + leader_certificate, + previous_leader_certificate, + &round_to_certificates_map, + rng, + ); + ledger.advance_to_next_block(&block).unwrap(); + previous_leader_certificate = Some(leader_certificate); + blocks.push(block); + } + + blocks +} + #[test] fn test_load() { let rng = &mut TestRng::default(); @@ -2812,3 +2939,107 @@ mod valid_solutions { assert_eq!(*block_aborted_solution_id, invalid_solution.id(), "Aborted solutions do not match"); } } + +#[test] +fn test_forged_block_subdags() { + let rng = &mut TestRng::default(); + + // Sample the genesis private key. + let private_key = PrivateKey::::new(rng).unwrap(); + // Initialize the store. + let store = ConsensusStore::<_, ConsensusMemory<_>>::open(None).unwrap(); + // Create a genesis block with a seeded RNG to reproduce the same genesis private keys. + let seed: u64 = rng.gen(); + let genesis_rng = &mut TestRng::from_seed(seed); + let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, genesis_rng).unwrap(); + + // Extract the private keys from the genesis committee by using the same RNG to sample private keys. + let genesis_rng = &mut TestRng::from_seed(seed); + let private_keys = [ + private_key, + PrivateKey::new(genesis_rng).unwrap(), + PrivateKey::new(genesis_rng).unwrap(), + PrivateKey::new(genesis_rng).unwrap(), + ]; + + // Construct 3 quroum blocks. + let mut quroum_blocks = construct_quorum_blocks(private_keys.to_vec(), genesis.clone(), 3, rng); + + // Extract the individual blocks. + let block_1 = quroum_blocks.remove(0); + let block_2 = quroum_blocks.remove(0); + let block_3 = quroum_blocks.remove(0); + + // Construct the ledger. + let ledger = + Ledger::>::load(genesis, StorageMode::Development(111)) + .unwrap(); + ledger.advance_to_next_block(&block_1).unwrap(); + + //////////////////////////////////////////////////////////////////////////// + // Attack 1: Forge block 2' with the subdag of block 3. + //////////////////////////////////////////////////////////////////////////// + { + println!("PERFORMING ATTACK 1: FORGING BLOCK 2' WITH THE SUBDAG OF BLOCK 3"); + + let block_3_subdag = + if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; + + // Fetch the transmissions. + let transmissions = extract_transmissions(&block_3); + + // Forge the block. + let forged_block_2 = ledger + .prepare_advance_to_next_quorum_block(block_3_subdag.clone(), transmissions, &mut rand::thread_rng()) + .unwrap(); + + assert_ne!(forged_block_2, block_2); + + // Attempt to verify the forged block. + assert!(ledger.check_next_block(&forged_block_2, &mut rand::thread_rng()).is_err()); + } + + //////////////////////////////////////////////////////////////////////////// + // Attack 2: Forge block 2' with the combined subdag of block 2 and 3. + //////////////////////////////////////////////////////////////////////////// + { + println!("PERFORMING ATTACK 2: FORGING BLOCK 2' WITH THE COMBINED SUBDAG OF BLOCK 2 AND 3"); + + // Fetch the subdags. + let block_2_subdag = + if let Authority::Quorum(subdag) = block_2.authority() { subdag } else { unreachable!("") }; + let block_3_subdag = + if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; + + // Combined the subdags. + let mut combined_subdag = block_2_subdag.deref().clone(); + for (round, certificates) in block_3_subdag.iter() { + combined_subdag + .entry(*round) + .and_modify(|c| c.extend(certificates.clone())) + .or_insert(certificates.clone()); + } + + // Fetch the transmissions. + let block_2_transmissions = extract_transmissions(&block_2); + let block_3_transmissions = extract_transmissions(&block_3); + + // Combine the transmissions. + let mut combined_transmissions = block_2_transmissions; + combined_transmissions.extend(block_3_transmissions); + + // Forge the block. + let forged_block_2_from_both_subdags = ledger + .prepare_advance_to_next_quorum_block( + Subdag::from(combined_subdag).unwrap(), + combined_transmissions, + &mut rand::thread_rng(), + ) + .unwrap(); + + assert_ne!(forged_block_2_from_both_subdags, block_1); + + // Attempt to verify the forged block. + assert!(ledger.check_next_block(&forged_block_2_from_both_subdags, &mut rand::thread_rng()).is_err()); + } +} diff --git a/vm/cli/mod.rs b/vm/cli/mod.rs index 9437f8cdf3..765e3d8ea7 100644 --- a/vm/cli/mod.rs +++ b/vm/cli/mod.rs @@ -23,122 +23,3 @@ pub use errors::*; pub mod helpers; pub use helpers::*; - -// TODO (raychu86): DO NOT IMPLEMENT THESE TESTS HERE. Add them to Ledger or Synthesizer. -#[cfg(test)] -mod tests { - use crate::{ - console::network::TestnetV0, - ledger::{ - authority::Authority, - narwhal::{Data, Subdag, Transmission, TransmissionID}, - store::helpers::memory::ConsensusMemory, - Block, - Ledger, - }, - }; - - use aleo_std::StorageMode; - use core::ops::Deref; - use indexmap::IndexMap; - - fn extract_transmissions(block: &Block) -> IndexMap, Transmission> { - let mut transmissions = IndexMap::new(); - for tx in block.transactions().iter() { - let checksum = Data::Object(tx.transaction().clone()).to_checksum::().unwrap(); - transmissions.insert(TransmissionID::from((&tx.id(), &checksum)), tx.transaction().clone().into()); - } - if let Some(coinbase_solution) = block.solutions().as_ref() { - for (_, solution) in coinbase_solution.iter() { - let checksum = Data::Object(*solution).to_checksum::().unwrap(); - transmissions.insert(TransmissionID::from((solution.id(), checksum)), (*solution).into()); - } - } - transmissions - } - - #[test] - fn test_forged_block() { - // TODO (raychu86): DO NOT USE REAL BLOCKS. CREATE BLOCKS FROM SCRATCH FOR TESTING. - let genesis: Block = - ureq::get("https://api.explorer.aleo.org/v1/testnet/block/0").call().unwrap().into_json().unwrap(); - let block_1: Block = - ureq::get("https://api.explorer.aleo.org/v1/testnet/block/1").call().unwrap().into_json().unwrap(); - let block_2: Block = - ureq::get("https://api.explorer.aleo.org/v1/testnet/block/2").call().unwrap().into_json().unwrap(); - let block_3: Block = - ureq::get("https://api.explorer.aleo.org/v1/testnet/block/3").call().unwrap().into_json().unwrap(); - - let ledger = - Ledger::>::load(genesis.clone(), StorageMode::Development(111)) - .unwrap(); - ledger.advance_to_next_block(&block_1).unwrap(); - - //////////////////////////////////////////////////////////////////////////// - // Attack 1: Forge block 2' with the subdag of block 3. - //////////////////////////////////////////////////////////////////////////// - { - println!("PERFORMING ATTACK 1: FORGING BLOCK 2' WITH THE SUBDAG OF BLOCK 3"); - - let block_3_subdag = - if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; - - // Fetch the transmissions. - let transmissions = extract_transmissions(&block_3); - - // Forge the block. - let forged_block_2 = ledger - .prepare_advance_to_next_quorum_block(block_3_subdag.clone(), transmissions, &mut rand::thread_rng()) - .unwrap(); - - assert_ne!(forged_block_2, block_2); - - // Attempt to verify the forged block. - assert!(ledger.check_next_block(&forged_block_2, &mut rand::thread_rng()).is_err()); - } - - //////////////////////////////////////////////////////////////////////////// - // Attack 2: Forge block 2' with the combined subdag of block 2 and 3. - //////////////////////////////////////////////////////////////////////////// - { - println!("PERFORMING ATTACK 2: FORGING BLOCK 2' WITH THE COMBINED SUBDAG OF BLOCK 2 AND 3"); - - // Fetch the subdags. - let block_2_subdag = - if let Authority::Quorum(subdag) = block_2.authority() { subdag } else { unreachable!("") }; - let block_3_subdag = - if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; - - // Combined the subdags. - let mut combined_subdag = block_2_subdag.deref().clone(); - for (round, certificates) in block_3_subdag.iter() { - combined_subdag - .entry(*round) - .and_modify(|c| c.extend(certificates.clone())) - .or_insert(certificates.clone()); - } - - // Fetch the transmissions. - let block_2_transmissions = extract_transmissions(&block_2); - let block_3_transmissions = extract_transmissions(&block_3); - - // Combine the transmissions. - let mut combined_transmissions = block_2_transmissions; - combined_transmissions.extend(block_3_transmissions); - - // Forge the block. - let forged_block_2_from_both_subdags = ledger - .prepare_advance_to_next_quorum_block( - Subdag::from(combined_subdag).unwrap(), - combined_transmissions, - &mut rand::thread_rng(), - ) - .unwrap(); - - assert_ne!(forged_block_2_from_both_subdags, block_1); - - // Attempt to verify the forged block. - assert!(ledger.check_next_block(&forged_block_2_from_both_subdags, &mut rand::thread_rng()).is_err()); - } - } -} From 99db4847d57bd2050d9b30dd3faa96f0bf04e033 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:31:39 -0700 Subject: [PATCH 5/7] nits --- ledger/src/check_next_block.rs | 2 -- ledger/src/tests.rs | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/ledger/src/check_next_block.rs b/ledger/src/check_next_block.rs index ad757b2ff4..a627362903 100644 --- a/ledger/src/check_next_block.rs +++ b/ledger/src/check_next_block.rs @@ -206,8 +206,6 @@ impl> Ledger { block.height() ); } - - // Instead of doing leader checks. Do a is_linked check for all certificates? } Ok(()) diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 498bb72ec0..838a6f795b 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -2975,13 +2975,12 @@ fn test_forged_block_subdags() { Ledger::>::load(genesis, StorageMode::Development(111)) .unwrap(); ledger.advance_to_next_block(&block_1).unwrap(); + ledger.check_next_block(&block_2, rng).unwrap(); //////////////////////////////////////////////////////////////////////////// // Attack 1: Forge block 2' with the subdag of block 3. //////////////////////////////////////////////////////////////////////////// { - println!("PERFORMING ATTACK 1: FORGING BLOCK 2' WITH THE SUBDAG OF BLOCK 3"); - let block_3_subdag = if let Authority::Quorum(subdag) = block_3.authority() { subdag } else { unreachable!("") }; @@ -3003,8 +3002,6 @@ fn test_forged_block_subdags() { // Attack 2: Forge block 2' with the combined subdag of block 2 and 3. //////////////////////////////////////////////////////////////////////////// { - println!("PERFORMING ATTACK 2: FORGING BLOCK 2' WITH THE COMBINED SUBDAG OF BLOCK 2 AND 3"); - // Fetch the subdags. let block_2_subdag = if let Authority::Quorum(subdag) = block_2.authority() { subdag } else { unreachable!("") }; From f483953a0be015ab1803ae2931515167fc8a0695 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:37:06 -0700 Subject: [PATCH 6/7] Add documentation --- ledger/src/tests.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 838a6f795b..6eba3637ef 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -69,14 +69,14 @@ fn construct_quorum_blocks( ) -> Vec> { // Initialize the ledger with the genesis block. let ledger = - Ledger::>::load(genesis.clone(), StorageMode::Development(123)) + Ledger::>::load(genesis.clone(), StorageMode::Production) .unwrap(); // Initialize the round parameters. assert!(num_blocks > 0); assert!(num_blocks < 25); let rounds_per_commit = 2; - let final_round = num_blocks.saturating_mul(2); + let final_round = num_blocks.saturating_mul(rounds_per_commit); // Sample rounds of batch certificates starting at the genesis round from a static set of 4 authors. let (round_to_certificates_map, committee) = { @@ -84,6 +84,7 @@ fn construct_quorum_blocks( let mut round_to_certificates_map: HashMap>> = HashMap::new(); let mut previous_certificates: IndexSet> = IndexSet::with_capacity(4); + // Create certificates for each round. for round in 1..=final_round { let mut current_certificates = IndexSet::new(); let previous_certificate_ids = @@ -100,6 +101,7 @@ fn construct_quorum_blocks( rng, ) .unwrap(); + // Add signatures for the batch headers. This creates a fully connected DAG. let signatures = private_keys .iter() .enumerate() @@ -124,15 +126,21 @@ fn construct_quorum_blocks( round_to_certificates_map: &HashMap>>, rng: &mut TestRng, ) -> Block { + // Construct the subdag for the block. let mut subdag_map = BTreeMap::new(); + // Add the leader certificate. subdag_map.insert(round, [leader_certificate.clone()].into()); + // Add the certificates of the previous round. subdag_map.insert(round - 1, round_to_certificates_map.get(&(round - 1)).unwrap().clone()); + // Add the certificates from the previous leader round, excluding the previous leader certificate. + // This assumes the number of rounds per commit is 2. if let Some(prev_leader_cert) = previous_leader_certificate { let mut previous_leader_round_certificates = round_to_certificates_map.get(&(round - 2)).cloned().unwrap_or_default(); previous_leader_round_certificates.shift_remove(prev_leader_cert); subdag_map.insert(round - 2, previous_leader_round_certificates); } + // Construct the block. let subdag = Subdag::from(subdag_map).unwrap(); let block = ledger.prepare_advance_to_next_quorum_block(subdag, Default::default(), rng).unwrap(); ledger.check_next_block(&block, rng).unwrap(); @@ -143,7 +151,7 @@ fn construct_quorum_blocks( let mut blocks = Vec::new(); let mut previous_leader_certificate: Option<&BatchCertificate> = None; - // Construct the blocks + // Construct the blocks. for block_height in 1..=num_blocks { let round = block_height.saturating_mul(rounds_per_commit); let leader = committee.get_leader(round).unwrap(); @@ -2972,8 +2980,7 @@ fn test_forged_block_subdags() { // Construct the ledger. let ledger = - Ledger::>::load(genesis, StorageMode::Development(111)) - .unwrap(); + Ledger::>::load(genesis, StorageMode::Production).unwrap(); ledger.advance_to_next_block(&block_1).unwrap(); ledger.check_next_block(&block_2, rng).unwrap(); From 67be1061e280ba7bcd7c040ea5b31f2d9be28719 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:02:55 -0700 Subject: [PATCH 7/7] nits --- ledger/src/check_next_block.rs | 18 +++--------------- ledger/src/get.rs | 16 ++++++++++++++++ ledger/src/tests.rs | 18 +++++++++--------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/ledger/src/check_next_block.rs b/ledger/src/check_next_block.rs index a627362903..c41d5d135a 100644 --- a/ledger/src/check_next_block.rs +++ b/ledger/src/check_next_block.rs @@ -170,21 +170,9 @@ impl> Ledger { for round in (self.latest_round().saturating_add(2)..=subdag.anchor_round().saturating_sub(2)).rev().step_by(2) { // Retrieve the previous committee lookback. - let previous_committee_lookback = { - // Determine the round number for the previous committee. - let previous_round = match round % 2 == 0 { - true => round.saturating_sub(1), - false => round.saturating_sub(2), - }; - // Determine the previous committee lookback round. - let committee_lookback_round = previous_round.saturating_sub(Committee::::COMMITTEE_LOOKBACK_RANGE); - // Output the previous committee lookback. - self.get_committee_for_round(committee_lookback_round) - .and_then(|opt| { - opt.ok_or_else(|| anyhow!("No committee found for lookback round {committee_lookback_round}")) - }) - .map_err(|e| anyhow!("Failed to retrieve previous committee for round {round}: {e}"))? - }; + let previous_committee_lookback = self + .get_committee_lookback_for_round(round)? + .ok_or_else(|| anyhow!("No committee lookback found for round {round}"))?; // Compute the leader for the commit round. let computed_leader = previous_committee_lookback diff --git a/ledger/src/get.rs b/ledger/src/get.rs index a92d52d142..2c39e618b9 100644 --- a/ledger/src/get.rs +++ b/ledger/src/get.rs @@ -25,6 +25,22 @@ impl> Ledger { self.vm.finalize_store().committee_store().get_committee_for_round(round) } + /// Returns the committee lookback for the given round. + pub fn get_committee_lookback_for_round(&self, round: u64) -> Result>> { + // Get the round number for the previous committee. Note, we subtract 2 from odd rounds, + // because committees are updated in even rounds. + let previous_round = match round % 2 == 0 { + true => round.saturating_sub(1), + false => round.saturating_sub(2), + }; + + // Get the committee lookback round. + let committee_lookback_round = previous_round.saturating_sub(Committee::::COMMITTEE_LOOKBACK_RANGE); + + // Retrieve the committee for the committee lookback round. + self.get_committee_for_round(committee_lookback_round) + } + /// Returns the state root that contains the given `block height`. pub fn get_state_root(&self, block_height: u32) -> Result> { self.vm.block_store().get_state_root(block_height) diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 6eba3637ef..69b89775d7 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -60,7 +60,7 @@ fn extract_transmissions( transmissions } -/// Construct `num_blocks` quroum blocks given a set of validator private keys and the genesis block. +/// Construct `num_blocks` quorum blocks given a set of validator private keys and the genesis block. fn construct_quorum_blocks( private_keys: Vec>, genesis: Block, @@ -117,8 +117,8 @@ fn construct_quorum_blocks( (round_to_certificates_map, committee) }; - // Helper function to create a quroum block. - fn create_next_quroum_block( + // Helper function to create a quorum block. + fn create_next_quorum_block( ledger: &Ledger>, round: u64, leader_certificate: &BatchCertificate, @@ -157,7 +157,7 @@ fn construct_quorum_blocks( let leader = committee.get_leader(round).unwrap(); let leader_certificate = round_to_certificates_map.get(&round).unwrap().iter().find(|c| c.author() == leader).unwrap(); - let block = create_next_quroum_block( + let block = create_next_quorum_block( &ledger, round, leader_certificate, @@ -2970,13 +2970,13 @@ fn test_forged_block_subdags() { PrivateKey::new(genesis_rng).unwrap(), ]; - // Construct 3 quroum blocks. - let mut quroum_blocks = construct_quorum_blocks(private_keys.to_vec(), genesis.clone(), 3, rng); + // Construct 3 quorum blocks. + let mut quorum_blocks = construct_quorum_blocks(private_keys.to_vec(), genesis.clone(), 3, rng); // Extract the individual blocks. - let block_1 = quroum_blocks.remove(0); - let block_2 = quroum_blocks.remove(0); - let block_3 = quroum_blocks.remove(0); + let block_1 = quorum_blocks.remove(0); + let block_2 = quorum_blocks.remove(0); + let block_3 = quorum_blocks.remove(0); // Construct the ledger. let ledger =