Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blockstore: send duplicate proofs for chained merkle root conflicts #35316

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions core/src/window_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ fn run_check_duplicate(
shred_slot,
&root_bank,
);
let chained_merkle_conflict_duplicate_proofs = cluster_nodes::check_feature_activation(
&feature_set::chained_merkle_conflict_duplicate_proofs::id(),
shred_slot,
&root_bank,
);
let (shred1, shred2) = match shred {
PossibleDuplicateShred::LastIndexConflict(shred, conflict)
| PossibleDuplicateShred::ErasureConflict(shred, conflict) => {
Expand Down Expand Up @@ -196,6 +201,24 @@ fn run_check_duplicate(
return Ok(());
}
}
PossibleDuplicateShred::ChainedMerkleRootConflict(shred, conflict) => {
if chained_merkle_conflict_duplicate_proofs {
// Although this proof can be immediately stored on detection, we wait until
// here in order to check the feature flag, as storage in blockstore can
// preclude the detection of other duplicate proofs in this slot
if blockstore.has_duplicate_shreds_in_slot(shred_slot) {
return Ok(());
}
blockstore.store_duplicate_slot(
shred_slot,
conflict.clone(),
shred.clone().into_payload(),
)?;
(shred, conflict)
} else {
return Ok(());
}
}
PossibleDuplicateShred::Exists(shred) => {
// Unlike the other cases we have to wait until here to decide to handle the duplicate and store
// in blockstore. This is because the duplicate could have been part of the same insert batch,
Expand Down
133 changes: 133 additions & 0 deletions ledger/src/blockstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub enum PossibleDuplicateShred {
LastIndexConflict(/* original */ Shred, /* conflict */ Vec<u8>), // The index of this shred conflicts with `slot_meta.last_index`
ErasureConflict(/* original */ Shred, /* conflict */ Vec<u8>), // The coding shred has a conflict in the erasure_meta
MerkleRootConflict(/* original */ Shred, /* conflict */ Vec<u8>), // Merkle root conflict in the same fec set
ChainedMerkleRootConflict(/* original */ Shred, /* conflict */ Vec<u8>), // Merkle root chaining conflict with previous fec set
}

impl PossibleDuplicateShred {
Expand All @@ -155,6 +156,7 @@ impl PossibleDuplicateShred {
Self::LastIndexConflict(shred, _) => shred.slot(),
Self::ErasureConflict(shred, _) => shred.slot(),
Self::MerkleRootConflict(shred, _) => shred.slot(),
Self::ChainedMerkleRootConflict(shred, _) => shred.slot(),
}
}
}
Expand Down Expand Up @@ -1283,6 +1285,18 @@ impl Blockstore {
return false;
}
}

// Check that the chaining between our current shred, the previous fec_set
// and the next fec_set
if !self.check_chained_merkle_root_consistency(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check (and similar other instances below), should only be invoked if a new erasure meta is inserted into blockstore. Otherwise we are redundantly repeating this check for every shred in the erasure batch.

Maybe we should also add the chained_merkle_root to MerkleRootMeta to check if the shreds within the same batch have the same merkle_root AND the same chained_merkle_root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good point, i'll rework this to only check on new erasure metas.

Maybe we should also add the chained_merkle_root to MerkleRootMeta to check if the shreds within the same batch have the same merkle_root AND the same chained_merkle_root.

The chained_merkle_root is also part of the merkle_root right? so if the batch has the same merkle_root they also have the same chained_merkle_root?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Just comparing merkle_roots is sufficient.

just_received_shreds,
&erasure_set,
merkle_root_metas,
&shred,
duplicate_shreds,
) {
return false;
}
}

let erasure_meta_entry = erasure_metas.entry(erasure_set).or_insert_with(|| {
Expand Down Expand Up @@ -1517,6 +1531,18 @@ impl Blockstore {
return Err(InsertDataShredError::InvalidShred);
}
}

// Check that the chaining between our current shred, the previous fec_set
// and the next fec_set
if !self.check_chained_merkle_root_consistency(
just_inserted_shreds,
&erasure_set,
merkle_root_metas,
&shred,
duplicate_shreds,
) {
return Err(InsertDataShredError::InvalidShred);
}
}

let newly_completed_data_sets = self.insert_data_shred(
Expand Down Expand Up @@ -1648,6 +1674,113 @@ impl Blockstore {
false
}

/// Returns true if there is no chaining conflict between
/// the `shred` and `merkle_root_meta` of the next or previous
/// FEC set, or if shreds from the next or previous set are
/// yet to be received.
///
/// Otherwise return false and add duplicate proof to
/// `duplicate_shreds`.
fn check_chained_merkle_root_consistency(
&self,
just_inserted_shreds: &HashMap<ShredId, Shred>,
erasure_set: &ErasureSetId,
merkle_root_metas: &HashMap<ErasureSetId, WorkingEntry<MerkleRootMeta>>,
shred: &Shred,
duplicate_shreds: &mut Vec<PossibleDuplicateShred>,
) -> bool {
let (slot, fec_set_index) = erasure_set.store_key();

let next_erasure_set = ErasureSetId::new(slot, fec_set_index + 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fec_set_index + 1

is wrong. FEC set indices are not sequential.

fec_set_index == shred index of the 1st data shred in the erasure batch.

so this should be

fec_set_index + num_data_shreds

if let Some(next_merkle_root_meta) =
merkle_root_metas.get(&next_erasure_set).map(AsRef::as_ref)
{
let next_shred_id = ShredId::new(
slot,
next_merkle_root_meta.first_received_shred_index(),
next_merkle_root_meta.first_received_shred_type(),
);
let next_shred =
Self::get_shred_from_just_inserted_or_db(self, just_inserted_shreds, next_shred_id)
.expect("Shred indicated by merkle root meta must exist")
.into_owned();
let next_shred = Shred::new_from_serialized_shred(next_shred)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably shouldn't deserialize the entire shred to check its merkle_root and chained_merkle_root; but instead add some apis to shred::layout:: to pull this from the binary directly.

.expect("Shred indicated by merkle root meta should deserialize");

if !self.check_chaining(shred, &next_shred, duplicate_shreds) {
return false;
}
}

if fec_set_index == 0 {
// Although the first fec set chains to the last fec set of the parent block,
// if this chain is incorrect we do not which block is the duplicate until votes
// are received. We instead delay this check until the block reaches duplicate
// confirmation.
return true;
}
let prev_erasure_set = ErasureSetId::new(slot, fec_set_index - 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment regarding

fec_set_index - 1

This one is actually more tricky because we need to know num_data_shreds in the previous erasure batch.

Maybe blockstore/rocksdb has some api which gives the immediate next or immediate previous stored key directly; and then we can check if those keys are indeed two consecutive erasure batches.

if let Some(prev_merkle_root_meta) =
merkle_root_metas.get(&prev_erasure_set).map(AsRef::as_ref)
{
let prev_shred_id = ShredId::new(
slot,
prev_merkle_root_meta.first_received_shred_index(),
prev_merkle_root_meta.first_received_shred_type(),
);
let prev_shred =
Self::get_shred_from_just_inserted_or_db(self, just_inserted_shreds, prev_shred_id)
.expect("Shred indicated by merkle root meta must exist")
.into_owned();
let prev_shred = Shred::new_from_serialized_shred(prev_shred)
.expect("Shred indicated by merkle root meta should deserialize");
if !self.check_chaining(&prev_shred, shred, duplicate_shreds) {
return false;
}
}

true
}

/// Checks if the chained merkle root of `next_shred` == `prev_shred`'s merkle root.
///
/// Returns true if no conflict, otherwise updates duplicate_shreds
fn check_chaining(
&self,
prev_shred: &Shred,
next_shred: &Shred,
duplicate_shreds: &mut Vec<PossibleDuplicateShred>,
) -> bool {
let Ok(chained_merkle_root) = next_shred.chained_merkle_root() else {
// Chained merkle roots have not been enabled yet
return true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently in tests we always end up in this case, but I ran local-cluster with StandardBroadcastRun::should_chain_merkle_shreds returning true to verify the logic.

};
let merkle_root = prev_shred.merkle_root().ok();
if merkle_root == Some(chained_merkle_root) {
return true;
}
warn!(
"Received conflicting chained merkle roots for slot: {},
shred {:?} type {:?} has merkle root {:?}, however
next shred {:?} type {:?} chains to merkle root {:?}. Reporting as duplicate",
prev_shred.slot(),
prev_shred.erasure_set(),
prev_shred.shred_type(),
merkle_root,
next_shred.erasure_set(),
next_shred.shred_type(),
chained_merkle_root,
);

if !self.has_duplicate_shreds_in_slot(prev_shred.slot()) {
duplicate_shreds.push(PossibleDuplicateShred::ChainedMerkleRootConflict(
prev_shred.clone(),
next_shred.payload().clone(),
));
}
false
}

fn should_insert_data_shred(
&self,
shred: &Shred,
Expand Down
5 changes: 5 additions & 0 deletions ledger/src/shred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ impl ShredId {
pub(crate) struct ErasureSetId(Slot, /*fec_set_index:*/ u32);

impl ErasureSetId {
pub(crate) fn new(slot: Slot, fec_set_index: u32) -> Self {
Self(slot, fec_set_index)
}

pub(crate) fn slot(&self) -> Slot {
self.0
}
Expand Down Expand Up @@ -342,6 +346,7 @@ impl Shred {
dispatch!(pub(crate) fn erasure_shard_index(&self) -> Result<usize, Error>);

dispatch!(pub fn into_payload(self) -> Vec<u8>);
dispatch!(pub fn chained_merkle_root(&self) -> Result<Hash, Error>);
dispatch!(pub fn merkle_root(&self) -> Result<Hash, Error>);
dispatch!(pub fn payload(&self) -> &Vec<u8>);
dispatch!(pub fn sanitize(&self) -> Result<(), Error>);
Expand Down
10 changes: 9 additions & 1 deletion ledger/src/shred/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ impl ShredData {
Ok(Self::SIZE_OF_HEADERS + Self::capacity(proof_size, /*chained:*/ true)?)
}

pub(super) fn chained_merkle_root(&self) -> Result<Hash, Error> {
let offset = self.chained_merkle_root_offset()?;
self.payload
.get(offset..offset + SIZE_OF_MERKLE_ROOT)
.map(Hash::new)
.ok_or(Error::InvalidPayloadSize(self.payload.len()))
}

fn set_chained_merkle_root(&mut self, chained_merkle_root: &Hash) -> Result<(), Error> {
let offset = self.chained_merkle_root_offset()?;
let Some(buffer) = self.payload.get_mut(offset..offset + SIZE_OF_MERKLE_ROOT) else {
Expand Down Expand Up @@ -328,7 +336,7 @@ impl ShredCode {
Ok(Self::SIZE_OF_HEADERS + Self::capacity(proof_size, /*chained:*/ true)?)
}

fn chained_merkle_root(&self) -> Result<Hash, Error> {
pub(super) fn chained_merkle_root(&self) -> Result<Hash, Error> {
let offset = self.chained_merkle_root_offset()?;
self.payload
.get(offset..offset + SIZE_OF_MERKLE_ROOT)
Expand Down
7 changes: 7 additions & 0 deletions ledger/src/shred/shred_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ impl ShredCode {
}
}

pub(super) fn chained_merkle_root(&self) -> Result<Hash, Error> {
match self {
Self::Legacy(_) => Err(Error::InvalidShredType),
Self::Merkle(shred) => shred.chained_merkle_root(),
}
}

pub(super) fn merkle_root(&self) -> Result<Hash, Error> {
match self {
Self::Legacy(_) => Err(Error::InvalidShredType),
Expand Down
7 changes: 7 additions & 0 deletions ledger/src/shred/shred_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ impl ShredData {
}
}

pub(super) fn chained_merkle_root(&self) -> Result<Hash, Error> {
match self {
Self::Legacy(_) => Err(Error::InvalidShredType),
Self::Merkle(shred) => shred.chained_merkle_root(),
}
}

pub(super) fn merkle_root(&self) -> Result<Hash, Error> {
match self {
Self::Legacy(_) => Err(Error::InvalidShredType),
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,10 @@ pub mod enable_gossip_duplicate_proof_ingestion {
solana_sdk::declare_id!("FNKCMBzYUdjhHyPdsKG2LSmdzH8TCHXn3ytj8RNBS4nG");
}

pub mod chained_merkle_conflict_duplicate_proofs {
solana_sdk::declare_id!("chaie9S2zVfuxJKNRGkyTDokLwWxx6kD2ZLsqQHaDD8");
}

pub mod enable_chained_merkle_shreds {
solana_sdk::declare_id!("7uZBkJXJ1HkuP6R3MJfZs7mLwymBcDbKdqbF51ZWLier");
}
Expand Down Expand Up @@ -975,6 +979,7 @@ lazy_static! {
(enable_gossip_duplicate_proof_ingestion::id(), "enable gossip duplicate proof ingestion #32963"),
(enable_chained_merkle_shreds::id(), "Enable chained Merkle shreds #34916"),
(remove_rounding_in_fee_calculation::id(), "Removing unwanted rounding in fee calculation #34982"),
(chained_merkle_conflict_duplicate_proofs::id(), "generate duplicate proofs for chained merkle root conflicts #35316"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()
Expand Down
Loading