mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-29 00:53:19 +00:00
fix(indexer): catch the edge case of re-using the last valid tip on restart
This commit is contained in:
parent
255a94c8ed
commit
b5ed8548d5
@ -24,6 +24,8 @@ struct Tip {
|
|||||||
pub enum AcceptOutcome {
|
pub enum AcceptOutcome {
|
||||||
/// Chained and applied; tip and L1 read cursor both advance.
|
/// Chained and applied; tip and L1 read cursor both advance.
|
||||||
Applied,
|
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.
|
/// Did not chain or failed to apply; tip stays frozen, stall recorded.
|
||||||
Parked(BlockIngestError),
|
Parked(BlockIngestError),
|
||||||
}
|
}
|
||||||
@ -266,6 +268,15 @@ impl IndexerStore {
|
|||||||
block: &Block,
|
block: &Block,
|
||||||
l1_slot: serde_json::Value,
|
l1_slot: serde_json::Value,
|
||||||
) -> Result<AcceptOutcome> {
|
) -> 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)? {
|
if let Some(err) = self.acceptance_error(block)? {
|
||||||
self.record_stall(Some(&block.header), l1_slot, err.clone())?;
|
self.record_stall(Some(&block.header), l1_slot, err.clone())?;
|
||||||
return Ok(AcceptOutcome::Parked(err));
|
return Ok(AcceptOutcome::Parked(err));
|
||||||
@ -286,7 +297,12 @@ impl IndexerStore {
|
|||||||
|
|
||||||
// Commit in-memory state (infallible) only after the DB write succeeded.
|
// Commit in-memory state (infallible) only after the DB write succeeded.
|
||||||
*self.current_state.write().await = scratch;
|
*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)
|
Ok(AcceptOutcome::Applied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -656,4 +672,60 @@ mod accept_tests {
|
|||||||
"tip must advance to the recovered block"
|
"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);
|
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)) => {
|
Ok(AcceptOutcome::Parked(ingest_err)) => {
|
||||||
error!(
|
error!(
|
||||||
"Parked at block {}: {ingest_err}",
|
"Parked at block {}: {ingest_err}",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user