mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-07-22 07:40:18 +00:00
fix(sequencer): fix silent reconstruction when only genesis was committed
This commit is contained in:
parent
8fdc3f203d
commit
e3442c695f
@ -65,6 +65,7 @@ impl BlockIngestError {
|
||||
}
|
||||
|
||||
/// The last successfully applied block a candidate must extend.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Tip {
|
||||
pub block_id: BlockId,
|
||||
pub hash: HashType,
|
||||
@ -73,7 +74,7 @@ pub struct Tip {
|
||||
/// Checks that `block` is the valid continuation of `tip`: hash integrity,
|
||||
/// then block-id continuity, then `prev_block_hash` linkage. A `None` tip
|
||||
/// (cold store) expects the genesis block.
|
||||
pub fn validate_against_tip(tip: Option<&Tip>, block: &Block) -> Result<(), BlockIngestError> {
|
||||
pub fn validate_against_tip(tip: Option<Tip>, block: &Block) -> Result<(), BlockIngestError> {
|
||||
let computed = block.recompute_hash();
|
||||
if computed != block.header.hash {
|
||||
return Err(BlockIngestError::HashMismatch {
|
||||
|
||||
@ -253,7 +253,7 @@ impl IndexerStore {
|
||||
return Ok(AcceptOutcome::AlreadyApplied);
|
||||
}
|
||||
|
||||
if let Err(err) = chain_consistency::validate_against_tip(tip.as_ref(), block) {
|
||||
if let Err(err) = chain_consistency::validate_against_tip(tip, block) {
|
||||
self.record_stall(Some(&block.header), l1_slot, err.clone())?;
|
||||
return Ok(AcceptOutcome::Parked(err));
|
||||
}
|
||||
|
||||
@ -68,21 +68,24 @@ impl SequencerStore {
|
||||
signing_key: lee::PrivateKey,
|
||||
) -> DbResult<Self> {
|
||||
let genesis_id = dbio.get_meta_first_block_in_db()?;
|
||||
let last_id = dbio.latest_block_meta()?.id;
|
||||
let last_id = dbio.latest_block_meta()?.map(|meta| meta.id);
|
||||
|
||||
info!("Preparing block cache");
|
||||
let mut tx_hash_to_block_map = HashMap::new();
|
||||
for i in genesis_id..=last_id {
|
||||
let block = dbio
|
||||
.get_block(i)?
|
||||
.expect("Block should be present in the database");
|
||||
|
||||
tx_hash_to_block_map.extend(block_to_transactions_map(&block));
|
||||
if let Some(last_id) = last_id {
|
||||
info!("Preparing block cache");
|
||||
for i in genesis_id..=last_id {
|
||||
let block = dbio
|
||||
.get_block(i)?
|
||||
.expect("Block should be present in the database");
|
||||
|
||||
tx_hash_to_block_map.extend(block_to_transactions_map(&block));
|
||||
}
|
||||
info!(
|
||||
"Block cache prepared. Total blocks in cache: {}",
|
||||
tx_hash_to_block_map.len()
|
||||
);
|
||||
}
|
||||
info!(
|
||||
"Block cache prepared. Total blocks in cache: {}",
|
||||
tx_hash_to_block_map.len()
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
dbio,
|
||||
@ -131,7 +134,7 @@ impl SequencerStore {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn latest_block_meta(&self) -> DbResult<BlockMeta> {
|
||||
pub fn latest_block_meta(&self) -> DbResult<Option<BlockMeta>> {
|
||||
self.dbio.latest_block_meta()
|
||||
}
|
||||
|
||||
@ -299,7 +302,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Verify that initially the latest block hash equals genesis hash
|
||||
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||
let latest_meta = node_store.latest_block_meta().unwrap().unwrap();
|
||||
assert_eq!(latest_meta.hash, genesis_hash);
|
||||
}
|
||||
|
||||
@ -337,7 +340,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Verify that the latest block meta now equals the new block's hash
|
||||
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||
let latest_meta = node_store.latest_block_meta().unwrap().unwrap();
|
||||
assert_eq!(latest_meta.hash, block_hash);
|
||||
}
|
||||
|
||||
|
||||
@ -201,7 +201,8 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
|
||||
let latest_block_meta = store
|
||||
.latest_block_meta()
|
||||
.expect("Failed to read latest block meta from store");
|
||||
.expect("Failed to read latest block meta from store")
|
||||
.expect("Sequencer store should have at least the genesis block after reconstruction");
|
||||
|
||||
let sequencer_core = Self {
|
||||
state,
|
||||
@ -256,14 +257,16 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
let local_tip = store
|
||||
.latest_block_meta()
|
||||
.context("Failed to read latest block meta")?
|
||||
.id;
|
||||
.map(|meta| meta.id);
|
||||
let had_checkpoint_before_start = !is_fresh_start;
|
||||
let has_committed_to_channel = local_tip > GENESIS_BLOCK_ID && had_checkpoint_before_start;
|
||||
if has_committed_to_channel && channel_tip_slot.is_none() {
|
||||
if let Some(local_tip) = local_tip
|
||||
&& had_checkpoint_before_start
|
||||
&& channel_tip_slot.is_none()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Sequencer holds committed blocks (tip {local_tip}) but the Bedrock channel \
|
||||
no longer exists on the connected chain — the channel was wiped or the node \
|
||||
points at a different chain. Refusing to resume onto a foreign channel."
|
||||
no longer exists on the connected chain — the channel was wiped or the node \
|
||||
points at a different chain. Refusing to resume onto a foreign channel."
|
||||
));
|
||||
}
|
||||
|
||||
@ -348,7 +351,9 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
|
||||
// A block at/below the tip must match what we already stored, otherwise
|
||||
// the channel is a different chain.
|
||||
if block_id <= tip.id {
|
||||
if let Some(tip) = &tip
|
||||
&& block_id <= tip.id
|
||||
{
|
||||
match store
|
||||
.get_block_at_id(block_id)
|
||||
.context("Failed to read stored block")?
|
||||
@ -375,14 +380,14 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
}
|
||||
|
||||
// New continuation: validate it chains onto the tip, then apply.
|
||||
let cc_tip = Tip {
|
||||
let cc_tip = tip.as_ref().map(|tip| Tip {
|
||||
block_id: tip.id,
|
||||
hash: tip.hash,
|
||||
};
|
||||
chain_consistency::validate_against_tip(Some(&cc_tip), block).map_err(|err| {
|
||||
});
|
||||
chain_consistency::validate_against_tip(cc_tip, block).map_err(|err| {
|
||||
anyhow!(
|
||||
"Channel block {block_id} does not extend local tip {}: {err}",
|
||||
tip.id
|
||||
"Channel block {block_id} does not extend local tip {:?}: {err}",
|
||||
tip.map(|tip| tip.id)
|
||||
)
|
||||
})?;
|
||||
|
||||
@ -650,7 +655,8 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
let latest_block_meta = self
|
||||
.store
|
||||
.latest_block_meta()
|
||||
.context("Failed to get latest block meta from store")?;
|
||||
.context("Failed to get latest block meta from store")?
|
||||
.context("Sequencer store should have at least the genesis block")?;
|
||||
|
||||
let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis())
|
||||
.expect("Timestamp must be positive");
|
||||
|
||||
@ -627,7 +627,7 @@ async fn produce_block_with_correct_prev_meta_after_restart() {
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
|
||||
// Get the metadata of the last block produced
|
||||
sequencer.store.latest_block_meta().unwrap()
|
||||
sequencer.store.latest_block_meta().unwrap().unwrap()
|
||||
};
|
||||
|
||||
// Step 2: Restart sequencer from the same storage
|
||||
|
||||
@ -27,7 +27,7 @@ fn block_to_channel_message(block: &Block, slot: u64) -> (ZoneMessage, Slot) {
|
||||
/// one block per slot at `slot_step` spacing.
|
||||
fn channel_from_store(store: &SequencerStore, slot_step: u64) -> Vec<(ZoneMessage, Slot)> {
|
||||
let genesis_id = store.genesis_id();
|
||||
let tip_id = store.latest_block_meta().expect("tip").id;
|
||||
let tip_id = store.latest_block_meta().expect("tip").expect("present").id;
|
||||
(genesis_id..=tip_id)
|
||||
.enumerate()
|
||||
.map(|(index, id)| {
|
||||
@ -45,7 +45,7 @@ async fn reconstructs_missing_channel_blocks_into_fresh_store() {
|
||||
SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
let tip_a = seq_a.block_store().latest_block_meta().unwrap();
|
||||
let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
|
||||
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
@ -67,7 +67,7 @@ async fn reconstructs_missing_channel_blocks_into_fresh_store() {
|
||||
.expect("reconstruct");
|
||||
assert!(!channel_was_empty);
|
||||
|
||||
let tip_b = store_b.latest_block_meta().unwrap();
|
||||
let tip_b = store_b.latest_block_meta().unwrap().unwrap();
|
||||
assert_eq!(tip_b.id, tip_a.id);
|
||||
assert_eq!(tip_b.hash, tip_a.hash);
|
||||
|
||||
@ -93,7 +93,7 @@ async fn reconstructs_missing_channel_blocks_into_fresh_store() {
|
||||
.await
|
||||
.expect("reconstruct idempotent");
|
||||
assert!(!channel_was_empty);
|
||||
assert_eq!(store_b.latest_block_meta().unwrap().id, tip_a.id);
|
||||
assert_eq!(store_b.latest_block_meta().unwrap().unwrap().id, tip_a.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -243,7 +243,7 @@ async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() {
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
|
||||
let tip_a = seq_a.block_store().latest_block_meta().unwrap();
|
||||
let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
let channel_id = config_a.bedrock_config.channel_id;
|
||||
@ -274,9 +274,9 @@ async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() {
|
||||
)
|
||||
.await
|
||||
.expect("reconstruct");
|
||||
seq_b.chain_height = seq_b.store.latest_block_meta().unwrap().id;
|
||||
seq_b.chain_height = seq_b.store.latest_block_meta().unwrap().unwrap().id;
|
||||
|
||||
let tip_b = seq_b.block_store().latest_block_meta().unwrap();
|
||||
let tip_b = seq_b.block_store().latest_block_meta().unwrap().unwrap();
|
||||
assert_eq!(tip_b.id, tip_a.id);
|
||||
assert_eq!(tip_b.hash, tip_a.hash);
|
||||
|
||||
@ -412,7 +412,7 @@ async fn reconstruction_reconciles_already_finished_deposit() {
|
||||
.await
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
let deposit_block_id = seq_a.block_store().latest_block_meta().unwrap().id;
|
||||
let deposit_block_id = seq_a.block_store().latest_block_meta().unwrap().unwrap().id;
|
||||
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
@ -476,14 +476,14 @@ async fn committed_local_against_missing_channel_fails_without_anchor() {
|
||||
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||
seq.produce_new_block().await.unwrap();
|
||||
seq.produce_new_block().await.unwrap();
|
||||
assert!(seq.block_store().latest_block_meta().unwrap().id > 1);
|
||||
assert!(seq.block_store().latest_block_meta().unwrap().unwrap().id > 1);
|
||||
} // drop releases the store so we can reopen it
|
||||
|
||||
// Reopen: blocks beyond genesis, no anchor. `is_fresh_start = false` stands in
|
||||
// for a checkpoint persisted by a prior sync (the mock never emits one).
|
||||
let (mut store, mut state) = SequencerCore::<MockBlockPublisher>::open_or_create_store(&config);
|
||||
assert!(store.get_zone_anchor().unwrap().is_none());
|
||||
assert!(store.latest_block_meta().unwrap().id > 1);
|
||||
assert!(store.latest_block_meta().unwrap().unwrap().id > 1);
|
||||
|
||||
// The channel is gone: no tip, no messages.
|
||||
let mock =
|
||||
|
||||
@ -344,8 +344,9 @@ impl RocksDBIO {
|
||||
self.put_batch(&LatestBlockMetaCellRef(block_meta), (), batch)
|
||||
}
|
||||
|
||||
pub fn latest_block_meta(&self) -> DbResult<BlockMeta> {
|
||||
self.get::<LatestBlockMetaCellOwned>(()).map(|val| val.0)
|
||||
pub fn latest_block_meta(&self) -> DbResult<Option<BlockMeta>> {
|
||||
self.get_opt::<LatestBlockMetaCellOwned>(())
|
||||
.map(|val| val.map(|cell| cell.0))
|
||||
}
|
||||
|
||||
pub fn get_zone_sdk_checkpoint_bytes(&self) -> DbResult<Option<Vec<u8>>> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user