mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-09 15:29:34 +00:00
fix(indexer): catch the edge case of re-using the last valid tip on restart
This commit is contained in:
parent
d24e25fea3
commit
15ebccb11e
@ -24,6 +24,8 @@ struct Tip {
|
||||
pub enum AcceptOutcome {
|
||||
/// Chained and applied; tip and L1 read cursor both advance.
|
||||
Applied,
|
||||
/// A duplicate re-delivery of the current tip. Just L2 advances.
|
||||
AlreadyApplied,
|
||||
/// Did not chain or failed to apply; tip stays frozen, stall recorded.
|
||||
Parked(BlockIngestError),
|
||||
}
|
||||
@ -266,6 +268,15 @@ impl IndexerStore {
|
||||
block: &Block,
|
||||
l1_slot: serde_json::Value,
|
||||
) -> Result<AcceptOutcome> {
|
||||
// idempotent edge case: re-delivery of the current tip
|
||||
// (e.g. after a crash that left the L1 cursor behind the applied tip)
|
||||
if let Some(tip) = self.validated_tip()?
|
||||
&& block.header.block_id == tip.block_id
|
||||
&& block.header.hash == tip.hash
|
||||
{
|
||||
return Ok(AcceptOutcome::AlreadyApplied);
|
||||
}
|
||||
|
||||
if let Some(err) = self.acceptance_error(block)? {
|
||||
self.record_stall(Some(&block.header), l1_slot, err.clone())?;
|
||||
return Ok(AcceptOutcome::Parked(err));
|
||||
@ -286,7 +297,12 @@ impl IndexerStore {
|
||||
|
||||
// Commit in-memory state (infallible) only after the DB write succeeded.
|
||||
*self.current_state.write().await = scratch;
|
||||
self.set_stall_reason(&None)?;
|
||||
// Clear a recorded stall now that a block has chained and applied. Only write
|
||||
// when one is actually present, so the steady state does no extra per-block
|
||||
// write (and can't turn a transient clear-write error into a spurious park).
|
||||
if self.get_stall_reason()?.is_some() {
|
||||
self.set_stall_reason(&None)?;
|
||||
}
|
||||
Ok(AcceptOutcome::Applied)
|
||||
}
|
||||
}
|
||||
@ -656,4 +672,60 @@ mod accept_tests {
|
||||
"tip must advance to the recovered block"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redelivered_tip_block_is_idempotent_not_parked() {
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path()).expect("open store");
|
||||
|
||||
let accounts = initial_pub_accounts_private_keys();
|
||||
let from = accounts[0].account_id;
|
||||
let to = accounts[1].account_id;
|
||||
let sign_key = accounts[0].pub_sign_key.clone();
|
||||
|
||||
let genesis = produce_dummy_block(1, None, vec![]);
|
||||
store
|
||||
.accept_block(&genesis, serde_json::Value::Null)
|
||||
.await
|
||||
.expect("accept genesis");
|
||||
|
||||
// Block 2: a single transfer of 10.
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
from, 0, to, 10, &sign_key,
|
||||
);
|
||||
let block = produce_dummy_block(2, Some(genesis.header.hash), vec![tx]);
|
||||
assert!(matches!(
|
||||
store
|
||||
.accept_block(&block, serde_json::Value::Null)
|
||||
.await
|
||||
.unwrap(),
|
||||
AcceptOutcome::Applied
|
||||
));
|
||||
let balance_after = store.account_current_state(&from).await.unwrap().balance;
|
||||
|
||||
// Re-deliver the exact same block: idempotent skip, no state change, no park.
|
||||
assert!(matches!(
|
||||
store
|
||||
.accept_block(&block, serde_json::Value::Null)
|
||||
.await
|
||||
.unwrap(),
|
||||
AcceptOutcome::AlreadyApplied
|
||||
));
|
||||
assert_eq!(
|
||||
store.account_current_state(&from).await.unwrap().balance,
|
||||
balance_after,
|
||||
"re-delivered block must not be applied twice"
|
||||
);
|
||||
assert_eq!(
|
||||
store.get_last_block_id().unwrap(),
|
||||
Some(2),
|
||||
"tip must stay at the already-applied block"
|
||||
);
|
||||
assert!(
|
||||
store.get_stall_reason().unwrap().is_none(),
|
||||
"a benign duplicate must not park the indexer"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,6 +261,16 @@ impl IndexerCore {
|
||||
}
|
||||
yield Ok(block);
|
||||
}
|
||||
Ok(AcceptOutcome::AlreadyApplied) => {
|
||||
info!(
|
||||
"Skipping already-applied block {}",
|
||||
block.header.block_id
|
||||
);
|
||||
cursor = Some(slot);
|
||||
if let Err(err) = self.store.set_zone_cursor(&slot) {
|
||||
warn!("Failed to persist indexer cursor: {err:#}");
|
||||
}
|
||||
}
|
||||
Ok(AcceptOutcome::Parked(ingest_err)) => {
|
||||
error!(
|
||||
"Parked at block {}: {ingest_err}",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user