Skip to content

Commit 9f57d7c

Browse files
authored
fast leader handover: error on bad UpdateParent during shred ingest (#605)
#### Problem and Summary of Changes When disseminating an `UpdateParent` component, we need to ensure that the parent change passes validity checks with respect to the `BlockHeader`. We explicitly cover the four cases: - `UpdateParent` observed before `BlockHeader` and... - ... `UpdateParent` and `BlockHeader` are in the same shred insertion batch - ... `UpdateParent` and `BlockHeader` are in different shred insertion batches - `BlockHeader` observed before `UpdateParent` and... - ... `UpdateParent` and `BlockHeader` are in the same shred insertion batch - ... `UpdateParent` and `BlockHeader` are in different shred insertion batches These checks are efficient, since we can detect the presence of a `BlockHeader` in a shred's payload without having to touch blockstore.
1 parent 33a70b2 commit 9f57d7c

3 files changed

Lines changed: 134 additions & 64 deletions

File tree

ledger/src/blockstore.rs

Lines changed: 118 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,47 +1954,44 @@ impl Blockstore {
19541954
None
19551955
}
19561956

1957-
fn maybe_insert_parent_meta(
1958-
&self,
1959-
current_shred: &Shred,
1960-
entry: &mut ParentMetaWorkingSetEntry,
1961-
) {
1957+
fn maybe_parse_block_header(&self, current_shred: &Shred) -> Option<ParentMeta> {
1958+
// Block headers only occur at index 0
1959+
if current_shred.index() != 0 {
1960+
return None;
1961+
}
1962+
19621963
// The contents of BlockHeaderV1 entirely fit into a single shred. So, let's just parse the
19631964
// first shred.
19641965
let shred_bytes = current_shred.payload();
19651966
let Ok(payload) = shred::wire::get_data(shred_bytes) else {
1966-
return;
1967+
return None;
19671968
};
19681969

19691970
// Check whether this is a BlockMarker
19701971
if !BlockComponent::infer_is_block_marker(payload).unwrap_or(false) {
1971-
return;
1972+
return None;
19721973
}
19731974

19741975
// Try to parse BlockHeader from the payload
1975-
let Some((parent_slot, parent_block_id)) =
1976-
BlockComponent::parse_block_header_from_data_payload(payload)
1977-
else {
1978-
return;
1979-
};
1976+
let (parent_slot, parent_block_id) =
1977+
BlockComponent::parse_block_header_from_data_payload(payload)?;
19801978

19811979
let parent_meta = ParentMeta {
19821980
parent_slot,
19831981
parent_block_id,
19841982
replay_fec_set_index: 0,
19851983
};
19861984

1987-
entry.parent_meta = Some(WorkingEntry::Dirty(parent_meta));
1985+
Some(parent_meta)
19881986
}
19891987

1990-
fn maybe_insert_update_parent(
1988+
fn maybe_parse_update_parent(
19911989
&self,
19921990
current_shred: &Shred,
19931991
slot: Slot,
19941992
location: BlockLocation,
19951993
just_inserted_shreds: &HashMap<(BlockLocation, ShredId), Cow<'_, Shred>>,
1996-
entry: &mut ParentMetaWorkingSetEntry,
1997-
) -> bool {
1994+
) -> Option<ParentMeta> {
19981995
let current_index = current_shred.index();
19991996
let fec_set_index = current_shred.fec_set_index();
20001997
let data_complete = current_shred.data_complete();
@@ -2032,41 +2029,30 @@ impl Blockstore {
20322029

20332030
(shred_bytes, fec_set_index)
20342031
} else {
2035-
return false;
2032+
return None;
20362033
};
20372034

20382035
// Process the shred bytes if we have them
2039-
let Some(shred_bytes) = shred_bytes else {
2040-
return false;
2041-
};
2042-
2043-
let Ok(payload) = shred::wire::get_data(&shred_bytes) else {
2044-
return false;
2045-
};
2036+
let shred_bytes = shred_bytes?;
2037+
let payload = shred::wire::get_data(&shred_bytes).ok()?;
20462038

20472039
// Check whether this is a BlockMarker
20482040
if !BlockComponent::infer_is_block_marker(payload).unwrap_or(false) {
2049-
return false;
2041+
return None;
20502042
}
20512043

20522044
// Try to parse UpdateParent from the payload
2053-
let Some((new_parent_slot, new_parent_block_id)) =
2054-
BlockComponent::parse_update_parent_from_data_payload(payload)
2055-
else {
2056-
return false;
2057-
};
2045+
let (new_parent_slot, new_parent_block_id) =
2046+
BlockComponent::parse_update_parent_from_data_payload(payload)?;
20582047

20592048
// First time seeing this UpdateParent
2060-
let parent_meta = ParentMeta {
2049+
let update_parent_meta = ParentMeta {
20612050
parent_slot: new_parent_slot,
20622051
parent_block_id: new_parent_block_id,
20632052
replay_fec_set_index: target_fec_set_index,
20642053
};
20652054

2066-
// Store the UpdateParent metadata
2067-
entry.parent_meta = Some(WorkingEntry::Dirty(parent_meta));
2068-
2069-
true
2055+
Some(update_parent_meta)
20702056
}
20712057

20722058
/// Create an entry to the specified `write_batch` that performs shred
@@ -2250,6 +2236,75 @@ impl Blockstore {
22502236
Ok(())
22512237
}
22522238

2239+
/// Validates compatibility between two ParentMetas.
2240+
///
2241+
/// This function determines which ParentMeta is which (UpdateParent vs BlockHeader) and
2242+
/// validates that they are compatible. Works irrespective of parameter order.
2243+
///
2244+
/// Returns:
2245+
/// - `Ok(true)` if validation passed and the new ParentMeta should be written
2246+
/// - `Ok(false)` if the new ParentMeta should NOT be written (same type and match, or UpdateParent takes precedence)
2247+
/// - `Err` if validation fails
2248+
fn should_write_parent_meta(slot: Slot, new: &ParentMeta, prev: &ParentMeta) -> Result<bool> {
2249+
// Determine which is the UpdateParent and which is the BlockHeader
2250+
let (update_parent_meta, block_header_meta, should_write) = match (
2251+
new.populated_from_block_header(),
2252+
prev.populated_from_block_header(),
2253+
) {
2254+
// New is BlockHeader, prev is UpdateParent
2255+
// UpdateParent takes precedence, so don't overwrite
2256+
(true, false) => (prev, new, false),
2257+
// New is UpdateParent, prev is BlockHeader
2258+
// Validate and allow UpdateParent to replace BlockHeader
2259+
(false, true) => (new, prev, true),
2260+
// Both are the same type - ensure they match
2261+
_ => match new == prev {
2262+
true => return Ok(false),
2263+
false => return Err(BlockstoreError::BlockComponentMismatch(slot)),
2264+
},
2265+
};
2266+
2267+
// Validate that the UpdateParent is compatible with the BlockHeader
2268+
if update_parent_meta.block() == block_header_meta.block() {
2269+
return Err(BlockstoreError::UpdateParentMatchesBlockHeader(slot));
2270+
}
2271+
2272+
if update_parent_meta.parent_slot > block_header_meta.parent_slot {
2273+
return Err(BlockstoreError::UpdateParentSlotGreaterThanBlockHeader(
2274+
slot,
2275+
));
2276+
}
2277+
2278+
Ok(should_write)
2279+
}
2280+
2281+
/// Fetches or retrieves the existing ParentMeta for a given slot and location.
2282+
///
2283+
/// This method first checks the working set, and if not found there, fetches from blockstore.
2284+
/// Returns the existing ParentMeta if it exists, or None otherwise.
2285+
fn get_or_fetch_parent_meta(
2286+
&self,
2287+
slot: Slot,
2288+
location: BlockLocation,
2289+
entry: &mut ParentMetaWorkingSetEntry,
2290+
) -> Result<Option<ParentMeta>> {
2291+
// First check working set
2292+
if let Some(parent_meta) = entry.parent_meta.as_ref() {
2293+
return Ok(Some(*parent_meta.as_ref()));
2294+
}
2295+
2296+
// Then fetch from blockstore if needed
2297+
if !entry.fetched_from_blockstore {
2298+
entry.fetched_from_blockstore = true;
2299+
if let Some(parent_meta) = self.parent_meta_cf.get((slot, location))? {
2300+
entry.parent_meta = Some(WorkingEntry::Clean(parent_meta));
2301+
return Ok(Some(parent_meta));
2302+
}
2303+
}
2304+
2305+
Ok(None)
2306+
}
2307+
22532308
fn maybe_update_parent_meta(
22542309
&self,
22552310
shred: &Shred,
@@ -2261,42 +2316,41 @@ impl Blockstore {
22612316
let key = (location, slot);
22622317
let entry = parent_meta_working_set.entry(key).or_default();
22632318

2264-
// If the working set ParentMeta exists and has an UpdateParent, then return early
2265-
if let Some(parent_meta) = entry.parent_meta.as_ref() {
2266-
let populated_from_update_parent = match parent_meta {
2267-
WorkingEntry::Dirty(parent_meta) | WorkingEntry::Clean(parent_meta) => {
2268-
parent_meta.populated_from_update_parent()
2269-
}
2270-
};
2271-
2272-
if populated_from_update_parent {
2273-
return Ok(());
2319+
// Fetch or retrieve existing ParentMeta (from working set or blockstore)
2320+
let previous_parent_meta = self.get_or_fetch_parent_meta(slot, location, entry)?;
2321+
2322+
// Try to parse new ParentMeta from the current shred
2323+
// - If shred.index() == 0, try to parse as a block header
2324+
// - Otherwise, try to parse as an UpdateParent (only if we don't already have one,
2325+
// since UpdateParent takes precedence and parsing is expensive)
2326+
let new_parent_meta = match (shred.index(), &previous_parent_meta) {
2327+
// Always try to parse block header at index 0
2328+
(0, _) => self.maybe_parse_block_header(shred),
2329+
// Try to parse UpdateParent only if we don't have one already
2330+
(_, Some(pm)) if pm.populated_from_block_header() => {
2331+
self.maybe_parse_update_parent(shred, slot, location, just_inserted_shreds)
22742332
}
2275-
}
2276-
// If we haven't hit blockstore for this key yet...
2277-
else if !entry.fetched_from_blockstore {
2278-
entry.fetched_from_blockstore = true;
2279-
2280-
if let Some(parent_meta) = self.parent_meta_cf.get((slot, location))? {
2281-
entry.parent_meta = Some(WorkingEntry::Clean(parent_meta));
2282-
2283-
if parent_meta.populated_from_update_parent() {
2284-
return Ok(());
2285-
}
2333+
(_, None) => {
2334+
self.maybe_parse_update_parent(shred, slot, location, just_inserted_shreds)
22862335
}
2287-
}
2336+
// previous_parent_meta is UpdateParent; don't parse another one
2337+
_ => None,
2338+
};
22882339

2289-
// Run through insertion logic for update_parent. If we succeed at inserting an
2290-
// UpdateParent, then return.
2291-
if self.maybe_insert_update_parent(shred, slot, location, just_inserted_shreds, entry) {
2340+
let Some(new_parent_meta) = new_parent_meta else {
22922341
return Ok(());
2293-
}
2342+
};
22942343

2295-
// If there isn't any parent_meta, then attempt to detect a block header.
2296-
if entry.parent_meta.is_none() && shred.index() == 0 {
2297-
self.maybe_insert_parent_meta(shred, entry);
2344+
// Validate new ParentMeta against previous (if both exist)
2345+
if let Some(prev) = &previous_parent_meta {
2346+
if !Self::should_write_parent_meta(slot, &new_parent_meta, prev)? {
2347+
return Ok(());
2348+
}
22982349
}
22992350

2351+
// Update entry with new ParentMeta
2352+
entry.parent_meta = Some(WorkingEntry::Dirty(new_parent_meta));
2353+
23002354
Ok(())
23012355
}
23022356

ledger/src/blockstore/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,13 @@ pub enum BlockstoreError {
5858
MissingMerkleRoot(Slot, u64),
5959
#[error("BlockComponent misalignment slot {0}, index {1}")]
6060
BlockComponentMisalignment(Slot, u64),
61+
#[error("Update parent matches block header slot {0}")]
62+
UpdateParentMatchesBlockHeader(Slot),
63+
#[error("Update parent slot greater than block header slot {0}")]
64+
UpdateParentSlotGreaterThanBlockHeader(Slot),
65+
#[error("Unexpected block component")]
66+
UnexpectedBlockComponent,
67+
#[error("Block component mismatch slot {0}")]
68+
BlockComponentMismatch(Slot),
6169
}
6270
pub type Result<T> = std::result::Result<T, BlockstoreError>;

ledger/src/blockstore_meta.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,14 @@ impl ParentMeta {
990990
pub fn populated_from_update_parent(&self) -> bool {
991991
self.replay_fec_set_index > 0
992992
}
993+
994+
pub fn populated_from_block_header(&self) -> bool {
995+
self.replay_fec_set_index == 0
996+
}
997+
998+
pub fn block(&self) -> (Slot, Hash) {
999+
(self.parent_slot, self.parent_block_id)
1000+
}
9931001
}
9941002

9951003
#[cfg(test)]

0 commit comments

Comments
 (0)