From 18f09223d7e4408693cea9dce749f202e13e9bd4 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 14 May 2024 18:28:28 +0000 Subject: [PATCH] gossip: process duplicate proofs for chained merkle root conflicts --- gossip/src/duplicate_shred.rs | 396 +++++++++++++++++++++++++--------- ledger/src/blockstore_meta.rs | 4 +- ledger/src/shred.rs | 2 +- 3 files changed, 302 insertions(+), 100 deletions(-) diff --git a/gossip/src/duplicate_shred.rs b/gossip/src/duplicate_shred.rs index 4c270f61421aed..52ed80be6a756d 100644 --- a/gossip/src/duplicate_shred.rs +++ b/gossip/src/duplicate_shred.rs @@ -94,7 +94,7 @@ pub enum Error { /// Check that `shred1` and `shred2` indicate a valid duplicate proof /// - Must be for the same slot /// - Must both sigverify for the correct leader -/// - Must have a merkle root conflict, otherwise `shred1` and `shred2` must have the same `shred_type` +/// - Must have a merkle or chained merkle root conflict, otherwise `shred1` and `shred2` must have the same `shred_type` /// - If `shred1` and `shred2` share the same index they must be not equal /// - If `shred1` and `shred2` do not share the same index and are data shreds /// verify that they indicate an index conflict. One of them must be the @@ -127,6 +127,23 @@ where return Ok(()); } + // Chained merkle root conflict check + let first_shred = std::cmp::min_by(shred1, shred2, |s1, s2| { + s1.fec_set_index().cmp(&s2.fec_set_index()) + }); + let second_shred = std::cmp::max_by(shred1, shred2, |s1, s2| { + s1.fec_set_index().cmp(&s2.fec_set_index()) + }); + + if let Some(erasure_meta) = ErasureMeta::from_coding_shred(first_shred) { + if erasure_meta.next_fec_set_index() == Some(second_shred.fec_set_index()) + && first_shred.merkle_root().ok() != second_shred.chained_merkle_root().ok() + { + // This catches a mixture of legacy and merkle shreds as well as improper chaining + return Ok(()); + } + } + if shred1.shred_type() != shred2.shred_type() { return Err(Error::ShredTypeMismatch); } @@ -335,6 +352,7 @@ pub(crate) mod tests { shredder, keypair, true, + None, ); data_shreds.pop().unwrap() } @@ -346,6 +364,7 @@ pub(crate) mod tests { keypair: &Keypair, merkle_variant: bool, is_last_in_slot: bool, + chained_merkle_root: Option, ) -> Shred { let (mut data_shreds, _) = new_rand_shreds( rng, @@ -356,6 +375,7 @@ pub(crate) mod tests { shredder, keypair, is_last_in_slot, + chained_merkle_root, ); data_shreds.pop().unwrap() } @@ -367,6 +387,7 @@ pub(crate) mod tests { shredder: &Shredder, keypair: &Keypair, merkle_variant: bool, + chained_merkle_root: Option, ) -> Vec { let (_, coding_shreds) = new_rand_shreds( rng, @@ -377,6 +398,7 @@ pub(crate) mod tests { shredder, keypair, true, + chained_merkle_root, ); coding_shreds } @@ -390,6 +412,7 @@ pub(crate) mod tests { shredder: &Shredder, keypair: &Keypair, is_last_in_slot: bool, + chained_merkle_root: Option, ) -> (Vec, Vec) { let entries: Vec<_> = std::iter::repeat_with(|| { let tx = system_transaction::transfer( @@ -410,8 +433,7 @@ pub(crate) mod tests { keypair, &entries, is_last_in_slot, - // chained_merkle_root - Some(Hash::new_from_array(rng.gen())), + chained_merkle_root, next_shred_index, next_code_index, // next_code_index merkle_variant, @@ -467,6 +489,7 @@ pub(crate) mod tests { &leader, merkle_variant, true, + None, ); let shred2 = new_rand_data_shred( &mut rng, @@ -475,6 +498,7 @@ pub(crate) mod tests { &leader, merkle_variant, true, + None, ); let leader_schedule = |s| { if s == slot { @@ -521,6 +545,7 @@ pub(crate) mod tests { &leader, merkle_variant, true, + None, ); let coding_shreds = new_rand_coding_shreds( &mut rng, @@ -529,6 +554,7 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + None, ); let test_cases = vec![ // Same data_shred @@ -584,16 +610,19 @@ pub(crate) mod tests { None } }; + let data_shred = new_rand_data_shred( + &mut rng, + next_shred_index, + &shredder, + &leader, + merkle_variant, + true, + None, + ); + let merkle_root = merkle_variant.then(|| data_shred.merkle_root().unwrap()); let test_cases = vec![ ( - new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder, - &leader, - merkle_variant, - true, - ), + data_shred.clone(), new_rand_data_shred( &mut rng, // With Merkle shreds, last erasure batch is padded with @@ -603,6 +632,7 @@ pub(crate) mod tests { &leader, merkle_variant, false, + merkle_root, ), ), ( @@ -612,16 +642,10 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, - true, - ), - new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder, - &leader, - merkle_variant, - true, + false, + merkle_root, ), + data_shred.clone(), ), ]; for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { @@ -657,79 +681,40 @@ pub(crate) mod tests { None } }; + + let data_shred = new_rand_data_shred( + &mut rng, + next_shred_index, + &shredder, + &leader, + merkle_variant, + false, + None, + ); + let merkle_root = merkle_variant.then(|| data_shred.merkle_root().unwrap()); + let next_data_shred = new_rand_data_shred( + &mut rng, + next_shred_index + 100, + &shredder, + &leader, + merkle_variant, + false, + merkle_root, + ); + let last_data_shred = new_rand_data_shred( + &mut rng, + next_shred_index + 1, + &shredder, + &leader, + merkle_variant, + true, + merkle_root, + ); let test_cases = vec![ - ( - new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder, - &leader, - merkle_variant, - false, - ), - new_rand_data_shred( - &mut rng, - next_shred_index + 1, - &shredder, - &leader, - merkle_variant, - true, - ), - ), - ( - new_rand_data_shred( - &mut rng, - next_shred_index + 1, - &shredder, - &leader, - merkle_variant, - true, - ), - new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder, - &leader, - merkle_variant, - false, - ), - ), - ( - new_rand_data_shred( - &mut rng, - next_shred_index + 100, - &shredder, - &leader, - merkle_variant, - false, - ), - new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder, - &leader, - merkle_variant, - false, - ), - ), - ( - new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder, - &leader, - merkle_variant, - false, - ), - new_rand_data_shred( - &mut rng, - next_shred_index + 100, - &shredder, - &leader, - merkle_variant, - false, - ), - ), + (data_shred.clone(), last_data_shred.clone()), + (last_data_shred.clone(), data_shred.clone()), + (next_data_shred.clone(), data_shred.clone()), + (data_shred.clone(), next_data_shred.clone()), ]; for (shred1, shred2) in test_cases.into_iter() { assert_matches!( @@ -786,6 +771,7 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + None, ); let coding_shreds_bigger = new_rand_coding_shreds( &mut rng, @@ -794,6 +780,7 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + None, ); let coding_shreds_smaller = new_rand_coding_shreds( &mut rng, @@ -802,6 +789,7 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + None, ); // Same fec-set, different index, different erasure meta @@ -849,7 +837,9 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + None, ); + let merkle_root = merkle_variant.then(|| coding_shreds[0].merkle_root().unwrap()); let coding_shreds_different_fec = new_rand_coding_shreds( &mut rng, next_shred_index + 1, @@ -857,6 +847,7 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + merkle_root, ); let coding_shreds_different_fec_and_size = new_rand_coding_shreds( &mut rng, @@ -865,6 +856,7 @@ pub(crate) mod tests { &shredder, &leader, merkle_variant, + merkle_root, ); let test_cases = vec![ @@ -946,6 +938,7 @@ pub(crate) mod tests { &shredder, &leader, false, + None, ); let (legacy_data_shreds, legacy_coding_shreds) = new_rand_shreds( @@ -957,6 +950,7 @@ pub(crate) mod tests { &shredder, &leader, true, + None, ); let (diff_data_shreds, diff_coding_shreds) = new_rand_shreds( @@ -968,6 +962,7 @@ pub(crate) mod tests { &shredder, &leader, false, + None, ); let test_cases = vec![ @@ -1023,7 +1018,9 @@ pub(crate) mod tests { &shredder, &leader, true, + None, ); + let merkle_root = data_shreds[0].merkle_root().unwrap(); let (next_data_shreds, next_coding_shreds) = new_rand_shreds( &mut rng, @@ -1034,6 +1031,7 @@ pub(crate) mod tests { &shredder, &leader, true, + Some(merkle_root), ); let (legacy_data_shreds, legacy_coding_shreds) = new_rand_shreds( @@ -1045,6 +1043,7 @@ pub(crate) mod tests { &shredder, &leader, true, + None, ); let test_cases = vec![ @@ -1065,11 +1064,6 @@ pub(crate) mod tests { legacy_data_shreds[0].clone(), legacy_coding_shreds[0].clone(), ), - // Mix of legacy and merkle with different fec index - (legacy_coding_shreds[0].clone(), next_data_shreds[0].clone()), - (next_coding_shreds[0].clone(), legacy_data_shreds[0].clone()), - (legacy_data_shreds[0].clone(), next_coding_shreds[0].clone()), - (next_data_shreds[0].clone(), legacy_coding_shreds[0].clone()), ]; for (shred1, shred2) in test_cases.into_iter() { assert_matches!( @@ -1103,4 +1097,212 @@ pub(crate) mod tests { ); } } + + #[test] + fn test_chained_merkle_root_conflict_round_trip() { + let mut rng = rand::thread_rng(); + let leader = Arc::new(Keypair::new()); + let (slot, parent_slot, reference_tick, version) = (53084024, 53084023, 0, 0); + let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); + let next_shred_index = rng.gen_range(0..31_000); + let leader_schedule = |s| { + if s == slot { + Some(leader.pubkey()) + } else { + None + } + }; + + let (data_shreds, coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + true, /* merkle_variant */ + &shredder, + &leader, + false, + None, + ); + + let next_shred_index = next_shred_index + data_shreds.len() as u32; + let (legacy_data_shreds, legacy_coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + false, /* merkle_variant */ + &shredder, + &leader, + true, + None, + ); + + let (diff_data_shreds, diff_coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + true, /* merkle_variant */ + &shredder, + &leader, + false, + Some(Hash::new_unique()), + ); + + let test_cases = vec![ + // Improper chaining + (coding_shreds[0].clone(), diff_coding_shreds[1].clone()), + (coding_shreds[0].clone(), diff_data_shreds[0].clone()), + // Mix of legacy and merkle in same slot + (coding_shreds[0].clone(), legacy_data_shreds[0].clone()), + (coding_shreds[0].clone(), legacy_coding_shreds[0].clone()), + // Order doesn't matter + (diff_coding_shreds[0].clone(), coding_shreds[1].clone()), + (diff_data_shreds[0].clone(), coding_shreds[0].clone()), + (legacy_data_shreds[0].clone(), coding_shreds[0].clone()), + (legacy_coding_shreds[0].clone(), coding_shreds[0].clone()), + ]; + + for (shred1, shred2) in test_cases.into_iter() { + let chunks: Vec<_> = from_shred( + shred1.clone(), + Pubkey::new_unique(), // self_pubkey + shred2.payload().clone(), + Some(leader_schedule), + rng.gen(), // wallclock + 512, // max_size + ) + .unwrap() + .collect(); + assert!(chunks.len() > 4); + let (shred3, shred4) = into_shreds(&leader.pubkey(), chunks).unwrap(); + assert_eq!(shred1, shred3); + assert_eq!(shred2, shred4); + } + } + + #[test] + fn test_chained_merkle_root_conflict_invalid() { + let mut rng = rand::thread_rng(); + let leader = Arc::new(Keypair::new()); + let (slot, parent_slot, reference_tick, version) = (53084024, 53084023, 0, 0); + let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); + let next_shred_index = rng.gen_range(0..31_000); + let leader_schedule = |s| { + if s == slot { + Some(leader.pubkey()) + } else { + None + } + }; + + let (data_shreds, coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + true, + &shredder, + &leader, + false, + None, + ); + let (legacy_data_shreds, legacy_coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + false, + &shredder, + &leader, + false, + None, + ); + + let next_shred_index = next_shred_index + data_shreds.len() as u32; + let merkle_root = data_shreds[0].merkle_root().unwrap(); + + let (next_data_shreds, next_coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + true, + &shredder, + &leader, + true, + Some(merkle_root), + ); + let (next_legacy_data_shreds, next_legacy_coding_shreds) = new_rand_shreds( + &mut rng, + next_shred_index, + next_shred_index, + 10, + false, + &shredder, + &leader, + false, + None, + ); + + let test_cases = vec![ + // Proper chaining + (coding_shreds[0].clone(), next_coding_shreds[0].clone()), + (coding_shreds[0].clone(), next_data_shreds[0].clone()), + // Legacy shreds + ( + legacy_coding_shreds[0].clone(), + next_legacy_coding_shreds[0].clone(), + ), + ( + legacy_coding_shreds[0].clone(), + next_legacy_data_shreds[0].clone(), + ), + // Improper chaining, but lower fec set is a data shred + (data_shreds[0].clone(), next_legacy_coding_shreds[0].clone()), + (data_shreds[0].clone(), next_legacy_data_shreds[0].clone()), + (legacy_data_shreds[0].clone(), next_coding_shreds[0].clone()), + (legacy_data_shreds[0].clone(), next_data_shreds[0].clone()), + // Order doesn't matter + (next_coding_shreds[0].clone(), coding_shreds[0].clone()), + (next_data_shreds[0].clone(), coding_shreds[0].clone()), + ( + next_legacy_coding_shreds[0].clone(), + legacy_coding_shreds[0].clone(), + ), + ( + next_legacy_data_shreds[0].clone(), + legacy_coding_shreds[0].clone(), + ), + (next_legacy_coding_shreds[0].clone(), data_shreds[0].clone()), + (next_legacy_data_shreds[0].clone(), data_shreds[0].clone()), + (next_coding_shreds[0].clone(), legacy_data_shreds[0].clone()), + (next_data_shreds[0].clone(), legacy_data_shreds[0].clone()), + ]; + for (shred1, shred2) in test_cases.into_iter() { + assert!(from_shred( + shred1.clone(), + Pubkey::new_unique(), // self_pubkey + shred2.payload().clone(), + Some(leader_schedule), + rng.gen(), // wallclock + 512, // max_size + ) + .is_err()); + + let chunks: Vec<_> = from_shred_bypass_checks( + shred1.clone(), + Pubkey::new_unique(), // self_pubkey + shred2.clone(), + rng.gen(), // wallclock + 512, // max_size + ) + .unwrap() + .collect(); + assert!(chunks.len() > 4); + + assert!(into_shreds(&leader.pubkey(), chunks).is_err()); + } + } } diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index 002d4970dd5403..f29fcfa9a85e68 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -338,7 +338,7 @@ impl SlotMeta { } impl ErasureMeta { - pub(crate) fn from_coding_shred(shred: &Shred) -> Option { + pub fn from_coding_shred(shred: &Shred) -> Option { match shred.shred_type() { ShredType::Data => None, ShredType::Code => { @@ -396,7 +396,7 @@ impl ErasureMeta { u32::try_from(self.first_received_coding_index).ok() } - pub(crate) fn next_fec_set_index(&self) -> Option { + pub fn next_fec_set_index(&self) -> Option { let num_data = u64::try_from(self.config.num_data).ok()?; self.fec_set_index .checked_add(num_data) diff --git a/ledger/src/shred.rs b/ledger/src/shred.rs index 79d0c9ff79bab6..608a42abfcbfac 100644 --- a/ledger/src/shred.rs +++ b/ledger/src/shred.rs @@ -348,7 +348,7 @@ impl Shred { dispatch!(fn set_signature(&mut self, signature: Signature)); dispatch!(fn signed_data(&self) -> Result); - dispatch!(pub(crate) fn chained_merkle_root(&self) -> Result); + dispatch!(pub fn chained_merkle_root(&self) -> Result); // Returns the portion of the shred's payload which is erasure coded. dispatch!(pub(crate) fn erasure_shard(self) -> Result, Error>); // Like Shred::erasure_shard but returning a slice.