Skip to content
Merged
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
22 changes: 19 additions & 3 deletions tests/e2e/src/test_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,15 +1515,31 @@ async fn run_setup_recovery_round_trip_test<A: Application>(

// Explicit recovery-correctness assertions, beyond the structural
// `assert_schema_invariants` (which checks `0..`-from-anchor contiguity):
// 1. the rebuilt tree is anchored at the checkpoint's resume nonce N' (I16);
// 1. the rebuilt tree is anchored at N' (I16): the checkpoint's N plus the
// batches accepted in (B, C], counted from L1;
// 2. recovery's resync did NOT spuriously freeze the frontier — the I15
// content-identity false-positive against below-anchor collapsed history
// is otherwise a silent failure, invisible at the e2e level (the same
// silence the (C, H1] bug shipped behind).
// The 5 s timer keeps closing batches until the stop, so whether one lands
// after B varies run to run. Counting consecutive nonces from N mirrors the
// scheduler's acceptance; batches this fresh cannot be stale.
let stop_block = runtime.baseline_safe_block()?;
let mut resume_nonce = checkpoint.resume_nonce;
for nonce in runtime
.l1_batch_nonces(checkpoint.checkpoint_block, stop_block)
.await?
{
if nonce == resume_nonce {
resume_nonce += 1;
}
}
eprintln!("recovery fold: C={stop_block} N'={resume_nonce}");
assert_eq!(
runtime.batch_tree_anchor()?,
checkpoint.resume_nonce,
"the rebuilt tree must be anchored at the checkpoint resume nonce N'"
resume_nonce,
"the rebuilt tree must be anchored at N', the checkpoint nonce plus the \
batches accepted after the checkpoint block"
);
assert_eq!(
runtime.canonical_divergence()?,
Expand Down
62 changes: 62 additions & 0 deletions tests/harness/src/sequencer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,68 @@ impl ManagedSequencer {
Ok(anchor as u64)
}

/// The rebuilt baseline's L1 stop block `C` (`history_state.base_safe_block`),
/// read from the run DB read-only.
pub fn baseline_safe_block(&self) -> HarnessResult<u64> {
let db_path = self.data_dir_path.join("sequencer.db");
let conn = rusqlite::Connection::open_with_flags(
db_path.as_path(),
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)
.map_err(|err| io_other(format!("open DB read-only: {err}")))?;
let block: i64 = conn
.query_row(
"SELECT base_safe_block FROM history_state WHERE singleton_id = 0",
[],
|row| row.get(0),
)
.map_err(|err| io_other(format!("read history_state: {err}")))?;
Ok(block as u64)
}

/// Nonces of this app's batch submissions from the devnet submitter included
/// in L1 blocks `(after_block, through_block]`, in L1 order. Read from the
/// InputBox logs, independent of any sequencer database.
pub async fn l1_batch_nonces(
&self,
after_block: u64,
through_block: u64,
) -> HarnessResult<Vec<u64>> {
use alloy::contract::Event;
use alloy::sol_types::{SolCall, SolEvent};
use cartesi_rollups_contracts::input_box::InputBox::InputAdded;
use cartesi_rollups_contracts::inputs::Inputs::EvmAdvanceCall;

let provider = alloy::providers::ProviderBuilder::new()
.connect(self.l1_endpoint())
.await
.map_err(|err| io_other(format!("failed to connect anvil provider: {err}")))?;
let mut logs: Vec<(InputAdded, alloy::rpc::types::Log)> =
Event::new_sol(&provider, &self.input_box_address())
.from_block(after_block + 1)
.to_block(through_block)
.event(InputAdded::SIGNATURE)
.topic1(self.app_address().into_word())
.query()
.await
.map_err(|err| io_other(format!("query InputAdded logs: {err}")))?;
logs.sort_by_key(|(_, log)| (log.block_number, log.log_index));

let mut nonces = Vec::new();
for (event, _) in logs {
let advance = EvmAdvanceCall::abi_decode(&event.input)
.map_err(|err| io_other(format!("decode EvmAdvance: {err}")))?;
if advance.msgSender != app_core::application::DEVNET_SEQUENCER_ADDRESS {
continue;
}
let batch =
<sequencer_core::batch::Batch as ssz::Decode>::from_ssz_bytes(&advance.payload)
.map_err(|err| io_other(format!("decode batch: {err:?}")))?;
nonces.push(batch.nonce);
}
Ok(nonces)
}

/// The canonical-divergence marker (I9/I15) from the run DB, or `None`
/// when the frontier is healthy. A recovery/resync e2e asserts this is `None`
/// to prove the content-identity check did NOT spuriously freeze the frontier
Expand Down
Loading