From 2d5489c04efa0a49fcfb4f9bf12aed95bf622d4c Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 7 Jul 2026 23:03:46 +0300 Subject: [PATCH 1/8] feat(sequencer): bootstrap state from Bedrock --- Cargo.lock | 22 +- Cargo.toml | 2 + integration_tests/tests/cross_zone_bridge.rs | 23 +- .../tests/cross_zone_ingress_guard.rs | 7 +- integration_tests/tests/cross_zone_ping.rs | 18 +- .../tests/cross_zone_verified.rs | 23 +- .../tests/sequencer_bootstrap.rs | 545 ++++++++++++++++++ integration_tests/tests/two_zone.rs | 12 +- lez/chain_consistency/Cargo.toml | 26 + lez/chain_consistency/src/apply.rs | 195 +++++++ .../src/consistency.rs} | 373 +++++------- lez/chain_consistency/src/lib.rs | 9 + lez/indexer/core/Cargo.toml | 2 +- lez/indexer/core/src/block_store.rs | 120 +--- lez/indexer/core/src/ingest_error.rs | 80 --- lez/indexer/core/src/lib.rs | 185 +++++- lez/indexer/core/src/stall_reason.rs | 25 - lez/indexer/core/src/status.rs | 27 +- lez/sequencer/core/Cargo.toml | 1 + lez/sequencer/core/src/block_publisher.rs | 50 +- lez/sequencer/core/src/block_store.rs | 80 ++- lez/sequencer/core/src/lib.rs | 266 ++++++++- lez/sequencer/core/src/mock.rs | 44 +- lez/sequencer/core/src/tests.rs | 2 + .../core/src/tests/reconstruction.rs | 401 +++++++++++++ lez/storage/src/sequencer/mod.rs | 14 +- lez/storage/src/sequencer/sequencer_cells.rs | 36 +- .../src/bin/regenerate_test_fixture.rs | 19 +- test_fixtures/src/config.rs | 19 + test_fixtures/src/lib.rs | 25 +- test_fixtures/src/setup.rs | 186 +++--- tools/cross_zone_chat/src/main.rs | 24 +- 32 files changed, 2176 insertions(+), 685 deletions(-) create mode 100644 integration_tests/tests/sequencer_bootstrap.rs create mode 100644 lez/chain_consistency/Cargo.toml create mode 100644 lez/chain_consistency/src/apply.rs rename lez/{indexer/core/src/chain_consistency.rs => chain_consistency/src/consistency.rs} (53%) create mode 100644 lez/chain_consistency/src/lib.rs delete mode 100644 lez/indexer/core/src/ingest_error.rs delete mode 100644 lez/indexer/core/src/stall_reason.rs create mode 100644 lez/sequencer/core/src/tests/reconstruction.rs diff --git a/Cargo.lock b/Cargo.lock index ca8c2a0f..e7c9979a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1368,6 +1368,25 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chain_consistency" +version = "0.1.0" +dependencies = [ + "anyhow", + "borsh", + "common", + "futures", + "lee", + "lee_core", + "log", + "logos-blockchain-core", + "logos-blockchain-zone-sdk", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "chkstk_stub" version = "0.1.0" @@ -3939,6 +3958,7 @@ dependencies = [ "arc-swap", "async-stream", "borsh", + "chain_consistency", "common", "cross_zone", "cross_zone_inbox_core", @@ -3958,7 +3978,6 @@ dependencies = [ "storage", "tempfile", "testnet_initial_state", - "thiserror 2.0.18", "tokio", "url", ] @@ -9023,6 +9042,7 @@ dependencies = [ "borsh", "bridge_core", "bytesize", + "chain_consistency", "chrono", "common", "cross_zone", diff --git a/Cargo.toml b/Cargo.toml index 164b64cc..bac0ea4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "lez/wallet", "lez/wallet-ffi", "lez/common", + "lez/chain_consistency", "lez/programs", "lez/programs/amm", "lez/programs/associated_token_account", @@ -72,6 +73,7 @@ members = [ lee = { path = "lee/state_machine" } lee_core = { path = "lee/state_machine/core" } common = { path = "lez/common" } +chain_consistency = { path = "lez/chain_consistency" } mempool = { path = "lez/mempool" } storage = { path = "lez/storage" } key_protocol = { path = "lee/key_protocol" } diff --git a/integration_tests/tests/cross_zone_bridge.rs b/integration_tests/tests/cross_zone_bridge.rs index 63b14c95..f835602a 100644 --- a/integration_tests/tests/cross_zone_bridge.rs +++ b/integration_tests/tests/cross_zone_bridge.rs @@ -17,7 +17,7 @@ use cross_zone_outbox_core::outbox_pda; use integration_tests::{ config::{self, SequencerPartialConfig}, indexer_client::IndexerClient, - setup::{setup_bedrock_node, setup_indexer, setup_sequencer}, + setup::{SequencerSetup, setup_bedrock_node, setup_indexer}, }; use lee::{ AccountId, PrivateKey, PublicKey, PublicTransaction, @@ -62,18 +62,19 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> { holder: holder_id, amount: INITIAL_BALANCE, }]; - let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, genesis_a, channel_a, None) + let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_a) + .with_genesis(genesis_a) + .setup() .await .context("Failed to set up zone A sequencer")?; - let (_seq_b, _seq_b_home) = setup_sequencer( - partial, - bedrock_addr, - vec![], - channel_b, - Some(cross_zone.clone()), - ) - .await - .context("Failed to set up zone B sequencer")?; + let (_seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_b) + .with_genesis(vec![]) + .with_cross_zone(cross_zone.clone()) + .setup() + .await + .context("Failed to set up zone B sequencer")?; let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone)) .await .context("Failed to set up zone B indexer")?; diff --git a/integration_tests/tests/cross_zone_ingress_guard.rs b/integration_tests/tests/cross_zone_ingress_guard.rs index 5f401869..fe5fe7fc 100644 --- a/integration_tests/tests/cross_zone_ingress_guard.rs +++ b/integration_tests/tests/cross_zone_ingress_guard.rs @@ -18,7 +18,7 @@ use cross_zone_inbox_core::{ }; use integration_tests::{ config::{self, SequencerPartialConfig}, - setup::{setup_bedrock_node, setup_sequencer}, + setup::{SequencerSetup, setup_bedrock_node}, }; use lee::{ PublicTransaction, @@ -34,7 +34,10 @@ async fn user_origin_inbox_call_rejected() -> Result<()> { .context("Failed to set up Bedrock node")?; let partial = SequencerPartialConfig::default(); let channel = config::bedrock_channel_id(); - let (seq, _seq_home) = setup_sequencer(partial, bedrock_addr, vec![], channel, None) + let (seq, _seq_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel) + .with_genesis(vec![]) + .setup() .await .context("Failed to set up sequencer")?; diff --git a/integration_tests/tests/cross_zone_ping.rs b/integration_tests/tests/cross_zone_ping.rs index 19223b5f..a3ed0c0c 100644 --- a/integration_tests/tests/cross_zone_ping.rs +++ b/integration_tests/tests/cross_zone_ping.rs @@ -18,7 +18,7 @@ use common::transaction::LeeTransaction; use cross_zone_outbox_core::outbox_pda; use integration_tests::{ config::{self, SequencerPartialConfig}, - setup::{setup_bedrock_node, setup_sequencer}, + setup::{SequencerSetup, setup_bedrock_node}, }; use lee::{AccountId, PublicTransaction, public_transaction::Message}; use lee_core::program::ProgramId; @@ -54,13 +54,19 @@ async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> { }], }; - let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None) + let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_a) + .with_genesis(vec![]) + .setup() .await .context("Failed to set up zone A sequencer")?; - let (seq_b, _seq_b_home) = - setup_sequencer(partial, bedrock_addr, vec![], channel_b, Some(cross_zone)) - .await - .context("Failed to set up zone B sequencer")?; + let (seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_b) + .with_genesis(vec![]) + .with_cross_zone(cross_zone) + .setup() + .await + .context("Failed to set up zone B sequencer")?; // Submit the ping on zone A, addressed to ping_receiver on zone B. let ping = build_ping_tx(zone_b, receiver_id); diff --git a/integration_tests/tests/cross_zone_verified.rs b/integration_tests/tests/cross_zone_verified.rs index e2f016c0..aa2ac06a 100644 --- a/integration_tests/tests/cross_zone_verified.rs +++ b/integration_tests/tests/cross_zone_verified.rs @@ -17,7 +17,7 @@ use cross_zone_outbox_core::outbox_pda; use integration_tests::{ config::{self, SequencerPartialConfig}, indexer_client::IndexerClient, - setup::{setup_bedrock_node, setup_indexer, setup_sequencer}, + setup::{SequencerSetup, setup_bedrock_node, setup_indexer}, }; use lee::{AccountId, PublicTransaction, public_transaction::Message}; use lee_core::program::ProgramId; @@ -53,21 +53,22 @@ async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> { // Zone A: source. Zone B: destination, with the watcher on its sequencer and // the verifier on its indexer. - let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None) + let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_a) + .with_genesis(vec![]) + .setup() .await .context("Failed to set up zone A sequencer")?; let (_idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None) .await .context("Failed to set up zone A indexer")?; - let (_seq_b, _seq_b_home) = setup_sequencer( - partial, - bedrock_addr, - vec![], - channel_b, - Some(cross_zone.clone()), - ) - .await - .context("Failed to set up zone B sequencer")?; + let (_seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_b) + .with_genesis(vec![]) + .with_cross_zone(cross_zone.clone()) + .setup() + .await + .context("Failed to set up zone B sequencer")?; let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone)) .await .context("Failed to set up zone B indexer")?; diff --git a/integration_tests/tests/sequencer_bootstrap.rs b/integration_tests/tests/sequencer_bootstrap.rs new file mode 100644 index 00000000..5261c412 --- /dev/null +++ b/integration_tests/tests/sequencer_bootstrap.rs @@ -0,0 +1,545 @@ +//! End-to-end tests for the sequencer's startup bootstrap/reconstruction from +//! Bedrock (verify-and-reconstruct). Each test drives a real Bedrock node and one +//! or more sequencer instances sharing the same channel, exercising how a +//! sequencer reconciles its local store against what the channel serves. + +#![expect( + clippy::tests_outside_test_module, + reason = "Integration tests live at crate root and don't care about these lints" +)] + +use std::{net::SocketAddr, path::Path, time::Duration}; + +use anyhow::{Context as _, Result, bail}; +use indexer_service_rpc::RpcClient as _; +use integration_tests::L2_TO_L1_TIMEOUT; +use lee::{AccountId, PrivateKey, PublicKey}; +use logos_blockchain_core::mantle::ops::channel::ChannelId; +use sequencer_core::config::GenesisAction; +use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; +use test_fixtures::{ + config::{SequencerPartialConfig, UrlProtocol, addr_to_url}, + indexer_client::IndexerClient, + setup::{SequencerSetup, setup_bedrock_node, setup_indexer}, +}; +use tokio::test; + +/// Block cadence for the tests: short so we don't wait long for local production. +fn fast_blocks() -> SequencerPartialConfig { + SequencerPartialConfig { + block_create_timeout: Duration::from_secs(2), + ..SequencerPartialConfig::default() + } +} + +/// Block cadence for a sequencer we don't want producing during the inspection +/// window, so its post-reconstruction tip stays put while we read it. +fn slow_blocks() -> SequencerPartialConfig { + SequencerPartialConfig { + block_create_timeout: Duration::from_secs(30), + ..SequencerPartialConfig::default() + } +} + +fn sequencer_client(addr: SocketAddr) -> Result { + let url = addr_to_url(UrlProtocol::Http, addr).context("Failed to build sequencer URL")?; + SequencerClientBuilder::default() + .build(url) + .context("Failed to build sequencer client") +} + +/// Polls the indexer's last finalized block id until it reaches `target`. The +/// indexer reads finalized channel history, so this is our oracle for "block +/// `target` is finalized on Bedrock" — exactly what a reconstructing sequencer +/// can read back. +async fn wait_for_finalized( + indexer: &IndexerClient, + target: u64, + timeout: Duration, +) -> Result { + let poll = async { + loop { + let finalized = indexer + .get_last_finalized_block_id() + .await + .context("Failed to read indexer last finalized block id")? + .unwrap_or(0); + if finalized >= target { + return Ok::<_, anyhow::Error>(finalized); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + }; + tokio::time::timeout(timeout, poll) + .await + .with_context(|| format!("Timed out waiting for indexer to finalize block {target}"))? +} + +/// Polls the sequencer's last block id until it reaches `target` or `timeout` elapses. +async fn wait_for_block_id( + client: &SequencerClient, + target: u64, + timeout: Duration, +) -> Result { + let poll = async { + loop { + let id = client + .get_last_block_id() + .await + .context("Failed to read sequencer last block id")?; + if id >= target { + return Ok::<_, anyhow::Error>(id); + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + }; + tokio::time::timeout(timeout, poll) + .await + .with_context(|| format!("Timed out waiting for block id {target}"))? +} + +/// Best-effort extraction of a panic payload's message (panics carry a `String` +/// or `&str`), for asserting a startup aborted for the *expected* reason. +fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { + payload + .downcast_ref::() + .cloned() + .or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_owned())) + .unwrap_or_else(|| "".to_owned()) +} + +/// A `SupplyAccount` genesis action for a fresh account, returning the vault +/// account id the funds land in (genesis supply goes into a claimable vault, not +/// the account directly), so tests can assert genesis state is present. +fn supplied_account(balance: u128) -> (AccountId, GenesisAction) { + let account_id = AccountId::from(&PublicKey::new_from_private_key( + &PrivateKey::new_os_random(), + )); + let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), account_id); + ( + vault_id, + GenesisAction::SupplyAccount { + account_id, + balance, + }, + ) +} + +/// Recursively copies the contents of `src` into `dst` (used to snapshot/restore a +/// sequencer's rocksdb directory while it is stopped). +fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> { + std::fs::create_dir_all(dst) + .with_context(|| format!("Failed to create dir {}", dst.display()))?; + for entry in + std::fs::read_dir(src).with_context(|| format!("Failed to read dir {}", src.display()))? + { + let entry = entry.context("Failed to read dir entry")?; + let target = dst.join(entry.file_name()); + if entry + .file_type() + .context("Failed to read file type")? + .is_dir() + { + copy_dir_recursive(&entry.path(), &target)?; + } else { + std::fs::copy(entry.path(), &target) + .with_context(|| format!("Failed to copy into {}", target.display()))?; + } + } + Ok(()) +} + +/// Case 1: local store is empty and the Bedrock channel is empty. +/// +/// The sequencer bootstraps genesis state, finds nothing to reconstruct, publishes +/// its own genesis to open the channel, and starts producing. +#[test] +async fn empty_local_and_empty_bedrock_bootstraps_from_genesis() -> Result<()> { + let (_bedrock, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock")?; + let home = tempfile::tempdir().context("Failed to create sequencer home")?; + + let (vault_id, supply) = supplied_account(12_345); + let genesis = vec![ + supply, + GenesisAction::SupplyBridgeAccount { balance: 1_000_000 }, + ]; + + let handle = SequencerSetup::new(fast_blocks(), bedrock_addr) + .with_genesis(genesis) + .setup_at(home.path()) + .await + .context("Failed to start sequencer")?; + let client = sequencer_client(handle.addr())?; + + // Fresh store + empty channel: startup bootstrapped genesis state directly. + assert_eq!( + client.get_account_balance(vault_id).await?, + 12_345, + "genesis-supplied vault balance must be present after bootstrap" + ); + + // The sequencer is live and producing on the freshly opened channel. + let last = wait_for_block_id(&client, 3, Duration::from_secs(60)).await?; + assert!( + last >= 3, + "sequencer should keep producing blocks, last={last}" + ); + assert!(handle.is_healthy(), "sequencer must stay healthy"); + + Ok(()) +} + +/// Case 2: local store is empty, but the Bedrock channel already has blocks. +/// +/// A first sequencer opens the channel and produces blocks; a second sequencer +/// starts from an empty store on the same channel (reusing the first's bedrock +/// signing key) and reconstructs the finalized history into its state. +#[test] +async fn empty_local_reconstructs_from_populated_bedrock() -> Result<()> { + const PRODUCED_TARGET: u64 = 3; + + let (_bedrock, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock")?; + let (indexer_handle, _indexer_dir) = setup_indexer( + bedrock_addr, + test_fixtures::config::bedrock_channel_id(), + None, + ) + .await + .context("Failed to setup indexer")?; + let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr()) + .context("Failed to build indexer URL")?; + let indexer = IndexerClient::new(&indexer_url) + .await + .context("Failed to build indexer client")?; + + let (vault_id, supply) = supplied_account(7_777); + let genesis = vec![ + supply, + GenesisAction::SupplyBridgeAccount { balance: 1_000_000 }, + ]; + + // Sequencer A opens the channel and produces a few blocks. + let home_a = tempfile::tempdir().context("Failed to create sequencer A home")?; + let handle_a = SequencerSetup::new(fast_blocks(), bedrock_addr) + .with_genesis(genesis.clone()) + .setup_at(home_a.path()) + .await + .context("Failed to start sequencer A")?; + let client_a = sequencer_client(handle_a.addr())?; + wait_for_block_id(&client_a, PRODUCED_TARGET, Duration::from_secs(60)).await?; + + // Wait until those blocks are finalized on Bedrock — reconstruction only + // reads finalized history. A stays alive so its publish task keeps flushing. + let finalized = wait_for_finalized(&indexer, PRODUCED_TARGET, L2_TO_L1_TIMEOUT).await?; + + // Stop A, then wipe just its L2 store (keeping the bedrock signing key) so it + // restarts from an empty store on the same channel/identity — a sequencer that + // lost its local DB. + drop(handle_a); + tokio::time::sleep(Duration::from_secs(2)).await; + std::fs::remove_dir_all(home_a.path().join("rocksdb")) + .context("Failed to wipe sequencer L2 store")?; + + // Sequencer B restarts on the same home from that empty store and reconstructs. + let handle_b = SequencerSetup::new(slow_blocks(), bedrock_addr) + .with_genesis(genesis) + .setup_at(home_a.path()) + .await + .context("Failed to start sequencer B")?; + let client_b = sequencer_client(handle_b.addr())?; + + // Reconstruction ran synchronously during B's startup: even though its local + // store was empty, its tip is past genesis, matching the finalized channel. + let tip_b = client_b.get_last_block_id().await?; + assert!( + tip_b >= finalized, + "B should reconstruct at least the finalized blocks; tip_b={tip_b}, finalized={finalized}" + ); + assert!( + tip_b > 1, + "B should have reconstructed blocks beyond genesis; tip_b={tip_b}" + ); + + // Genesis state was rebuilt as part of the reconstruction. + assert_eq!( + client_b.get_account_balance(vault_id).await?, + 7_777, + "reconstructed genesis vault balance must be present" + ); + assert!(handle_b.is_healthy(), "sequencer B must stay healthy"); + + Ok(()) +} + +/// Case 3: local store is not empty, but the Bedrock channel is empty. +/// +/// A sequencer produces blocks (committing to a channel), is stopped, and is +/// restarted against a fresh/empty channel — i.e. the channel it committed to +/// was wiped or the node points at a different chain. Startup must fail rather +/// than silently resume onto a foreign channel. Crucially this must hold even +/// though the sequencer only ever *produced* (so it never recorded a per-block +/// anchor): the committed-but-missing-channel invariant catches it. +#[test] +async fn nonempty_local_against_empty_channel_fails_startup() -> Result<()> { + const PRODUCED_TARGET: u64 = 3; + + let (_bedrock, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock")?; + + let (_vault_id, supply) = supplied_account(1); + let genesis = vec![ + supply, + GenesisAction::SupplyBridgeAccount { balance: 1_000_000 }, + ]; + + // A opens the channel and produces blocks. They land in its local store + // immediately, so no need to wait for finalization. + let home_a = tempfile::tempdir().context("Failed to create sequencer A home")?; + let handle_a = SequencerSetup::new(fast_blocks(), bedrock_addr) + .with_genesis(genesis.clone()) + .setup_at(home_a.path()) + .await + .context("Failed to start sequencer A")?; + wait_for_block_id( + &sequencer_client(handle_a.addr())?, + PRODUCED_TARGET, + Duration::from_secs(60), + ) + .await?; + drop(handle_a); + tokio::time::sleep(Duration::from_secs(2)).await; + + // Restart on the SAME home (A's committed store: blocks + checkpoint) but + // pointed at a fresh, never-used channel — the channel it committed to is gone. + let empty_channel = ChannelId::from([0x5a_u8; 32]); + + // Startup aborts on the missing-channel invariant (a panic in + // `start_from_config`). Run it on a dedicated OS thread with its own runtime + // so the panic is isolated to `join()` instead of failing the test thread. + // The `timeout` future must be created *inside* `block_on` (it needs a running + // reactor), so build it in an `async` block rather than as an eager argument. + let home_a_path = home_a.path().to_owned(); + let outcome = std::thread::spawn(move || { + let runtime = tokio::runtime::Runtime::new().expect("Failed to build runtime"); + runtime.block_on(async { + tokio::time::timeout( + Duration::from_secs(90), + SequencerSetup::new(slow_blocks(), bedrock_addr) + .with_channel_id(empty_channel) + .with_genesis(genesis) + .setup_at(&home_a_path), + ) + .await + }) + }) + .join(); + + match outcome { + // Expected: `start_from_config` panicked on the missing-channel invariant. + // Assert the *reason*, so an unrelated panic fails the test rather than + // masquerading as success. + Err(panic) => { + let message = panic_message(&*panic); + assert!( + message.contains("Refusing to resume onto a foreign channel"), + "startup panicked for an unexpected reason: {message}" + ); + } + Ok(Err(_elapsed)) => { + bail!("Sequencer startup hung instead of failing against an empty channel") + } + Ok(Ok(Err(err))) => { + bail!("Sequencer expected to panic, but it failed with error: {err:#?}") + } + Ok(Ok(Ok(_handle))) => { + bail!("Sequencer startup unexpectedly succeeded against an empty channel") + } + } + + Ok(()) +} + +/// Case 4: both non-empty, but the local store is *ahead* of the finalized channel. +/// +/// A sequencer produces blocks faster than Bedrock finalizes them (the normal +/// steady state), so on restart its local tip leads the channel's finalized tip. +/// Startup must succeed: reconstruction re-verifies the finalized blocks it already +/// holds and leaves the extra, not-yet-finalized local blocks untouched. +#[test] +async fn local_ahead_of_channel_resumes() -> Result<()> { + const FINALIZED_TARGET: u64 = 2; + + let (_bedrock, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock")?; + let (indexer_handle, _indexer_dir) = setup_indexer( + bedrock_addr, + test_fixtures::config::bedrock_channel_id(), + None, + ) + .await + .context("Failed to setup indexer")?; + let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr()) + .context("Failed to build indexer URL")?; + let indexer = IndexerClient::new(&indexer_url) + .await + .context("Failed to build indexer client")?; + + let (vault_id, supply) = supplied_account(4_242); + let genesis = vec![ + supply, + GenesisAction::SupplyBridgeAccount { balance: 1_000_000 }, + ]; + + // A produces continuously (fast cadence) while Bedrock finalizes slowly, so + // its local tip runs well ahead of the channel's finalized tip. + let home = tempfile::tempdir().context("Failed to create sequencer home")?; + let handle_a = SequencerSetup::new(fast_blocks(), bedrock_addr) + .with_genesis(genesis.clone()) + .setup_at(home.path()) + .await + .context("Failed to start sequencer A")?; + let client_a = sequencer_client(handle_a.addr())?; + let finalized = wait_for_finalized(&indexer, FINALIZED_TARGET, L2_TO_L1_TIMEOUT).await?; + let tip_before = client_a.get_last_block_id().await?; + assert!( + tip_before > finalized, + "local tip {tip_before} should lead the finalized tip {finalized}" + ); + + // Restart on the same home; slow cadence so its tip stays put while we inspect. + drop(handle_a); + tokio::time::sleep(Duration::from_secs(2)).await; + let handle_b = SequencerSetup::new(slow_blocks(), bedrock_addr) + .with_genesis(genesis) + .setup_at(home.path()) + .await + .context("Failed to restart sequencer")?; + let client_b = sequencer_client(handle_b.addr())?; + + // Reconstruction verified the finalized prefix and preserved the extra blocks. + let tip_b = client_b.get_last_block_id().await?; + assert!( + tip_b >= tip_before, + "restart must not lose locally-produced blocks; tip_b={tip_b}, before={tip_before}" + ); + assert_eq!( + client_b.get_account_balance(vault_id).await?, + 4_242, + "genesis state must survive the restart" + ); + assert!(handle_b.is_healthy(), "sequencer must stay healthy"); + + Ok(()) +} + +/// Case 5: both non-empty, but the local store is *behind* the finalized channel. +/// +/// We snapshot a sequencer's store at an early tip, let it keep extending and +/// finalizing the channel, then restore the early snapshot and restart. Startup +/// must reconstruct forward — replay the finalized blocks the local store is +/// missing — catching the local tip up to the channel. +/// +/// The snapshot/restore is essential here (not a gratuitous copy): a live +/// sequencer's local tip always leads finalization, so the only way to obtain a +/// local store that *lags* the finalized channel is to preserve an earlier state +/// while the same channel advances past it. +#[test] +async fn local_behind_channel_reconstructs_forward() -> Result<()> { + const SNAPSHOT_TIP: u64 = 2; + const FINALIZED_TARGET: u64 = 4; + + let (_bedrock, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock")?; + let (indexer_handle, _indexer_dir) = setup_indexer( + bedrock_addr, + test_fixtures::config::bedrock_channel_id(), + None, + ) + .await + .context("Failed to setup indexer")?; + let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr()) + .context("Failed to build indexer URL")?; + let indexer = IndexerClient::new(&indexer_url) + .await + .context("Failed to build indexer client")?; + + let (vault_id, supply) = supplied_account(5_005); + let genesis = vec![ + supply, + GenesisAction::SupplyBridgeAccount { balance: 1_000_000 }, + ]; + + let home = tempfile::tempdir().context("Failed to create sequencer home")?; + let rocksdb = home.path().join("rocksdb"); + + // Bring the sequencer up to an early tip, then stop it so its store is at rest. + { + let handle = SequencerSetup::new(slow_blocks(), bedrock_addr) + .with_genesis(genesis.clone()) + .setup_at(home.path()) + .await + .context("Failed to start sequencer")?; + wait_for_block_id( + &sequencer_client(handle.addr())?, + SNAPSHOT_TIP, + Duration::from_secs(120), + ) + .await?; + drop(handle); + tokio::time::sleep(Duration::from_secs(2)).await; + } + + // Snapshot the early store (tip == SNAPSHOT_TIP), safe because it is at rest. + let snapshot = tempfile::tempdir().context("Failed to create snapshot dir")?; + copy_dir_recursive(&rocksdb, snapshot.path()).context("Failed to snapshot store")?; + + // Resume the sequencer (fast cadence) so it extends and finalizes the channel + // beyond the snapshot. + let finalized = { + let handle = SequencerSetup::new(fast_blocks(), bedrock_addr) + .with_genesis(genesis.clone()) + .setup_at(home.path()) + .await + .context("Failed to resume sequencer")?; + let finalized = wait_for_finalized(&indexer, FINALIZED_TARGET, L2_TO_L1_TIMEOUT).await?; + drop(handle); + tokio::time::sleep(Duration::from_secs(2)).await; + finalized + }; + + // Restore the early snapshot: the local store now lags the finalized channel. + std::fs::remove_dir_all(&rocksdb).context("Failed to remove store before restore")?; + copy_dir_recursive(snapshot.path(), &rocksdb).context("Failed to restore snapshot")?; + + // Restart: reconstruction must catch the lagging store up to the channel. + let handle = SequencerSetup::new(slow_blocks(), bedrock_addr) + .with_genesis(genesis) + .setup_at(home.path()) + .await + .context("Failed to restart sequencer from a lagging store")?; + let client = sequencer_client(handle.addr())?; + let tip = client.get_last_block_id().await?; + assert!( + tip >= finalized, + "lagging store must reconstruct forward to the finalized tip; tip={tip}, finalized={finalized}" + ); + assert!( + tip > SNAPSHOT_TIP, + "reconstruction must advance beyond the snapshot tip; tip={tip}" + ); + assert_eq!( + client.get_account_balance(vault_id).await?, + 5_005, + "genesis state must be intact after reconstruction" + ); + assert!(handle.is_healthy(), "sequencer must stay healthy"); + + Ok(()) +} diff --git a/integration_tests/tests/two_zone.rs b/integration_tests/tests/two_zone.rs index c895acd1..8f4b2697 100644 --- a/integration_tests/tests/two_zone.rs +++ b/integration_tests/tests/two_zone.rs @@ -13,7 +13,7 @@ use indexer_service_rpc::RpcClient as _; use integration_tests::{ config::{self, SequencerPartialConfig}, indexer_client::IndexerClient, - setup::{setup_bedrock_node, setup_indexer, setup_sequencer}, + setup::{SequencerSetup, setup_bedrock_node, setup_indexer}, }; use sequencer_service_rpc::{RpcClient as _, SequencerClientBuilder}; use tokio::test; @@ -35,13 +35,19 @@ async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> { let channel_b = config::bedrock_channel_id_b(); // Empty genesis is enough: the clock transaction drives block production. - let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None) + let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_a) + .with_genesis(vec![]) + .setup() .await .context("Failed to set up zone A sequencer")?; let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None) .await .context("Failed to set up zone A indexer")?; - let (seq_b, _seq_b_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_b, None) + let (seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr) + .with_channel_id(channel_b) + .with_genesis(vec![]) + .setup() .await .context("Failed to set up zone B sequencer")?; let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, None) diff --git a/lez/chain_consistency/Cargo.toml b/lez/chain_consistency/Cargo.toml new file mode 100644 index 00000000..02bf0ea0 --- /dev/null +++ b/lez/chain_consistency/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "chain_consistency" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[lints] +workspace = true + +[dependencies] +common.workspace = true +lee.workspace = true +lee_core.workspace = true + +logos-blockchain-zone-sdk.workspace = true +logos-blockchain-core.workspace = true +anyhow.workspace = true +borsh.workspace = true +futures.workspace = true +log.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/lez/chain_consistency/src/apply.rs b/lez/chain_consistency/src/apply.rs new file mode 100644 index 00000000..ea97d58a --- /dev/null +++ b/lez/chain_consistency/src/apply.rs @@ -0,0 +1,195 @@ +//! Validating and applying channel L2 blocks to a `V03State`. +//! +//! These primitives are shared by the consumers that reconstruct L2 state from +//! the Bedrock channel. + +use common::{ + HashType, + block::Block, + transaction::{LeeTransaction, clock_invocation}, +}; +use lee::{GENESIS_BLOCK_ID, V03State}; +use lee_core::BlockId; +use serde::{Deserialize, Serialize}; + +/// Why a channel L2 block could not be validated or applied. +/// +/// Persisted in `RocksDB` (via the Indexer's stall reason), so every variant +/// must be `Clone + Serialize + Deserialize`. +#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)] +pub enum BlockIngestError { + #[error("Failed to deserialize L2 block: {0}")] + /// Here we store the error string that is derived from [`borsh::from_slice`]'s [`Err`]. + Deserialize(String), + #[error("Unexpected block id: expected {expected}, got {got}")] + UnexpectedBlockId { expected: BlockId, got: BlockId }, + #[error("Broken chain link: expected prev {expected_prev}, got {got_prev}")] + BrokenChainLink { + expected_prev: HashType, + got_prev: HashType, + }, + #[error("Block hash mismatch: computed {computed}, header {header}")] + HashMismatch { + computed: HashType, + header: HashType, + }, + #[error("Block has no transactions")] + EmptyBlock, + #[error("Last transaction must be the public clock invocation for the block timestamp")] + InvalidClockTransaction, + #[error("Genesis block must contain only public transactions")] + NonPublicGenesisTransaction, + #[error("State transition failed at transaction {tx_index}: {reason}")] + StateTransition { + /// Index of the failing transaction within the block body. + tx_index: u64, + /// Reason string from `lee::Error` to `anyhow::Error` to `{:#}`. + /// + /// This is required because `lee::Error` is not `Clone + Serialize + Deserialize`, so we + /// cannot store it directly. + reason: String, + }, +} + +impl BlockIngestError { + /// Whether the failure may be transient rather than a property of the block. + /// + /// FIXME: `StateTransition` is too coarse — its `reason` string mixes genuine + /// state-transition rejections with infra failures (risc0 executor teardown, + /// storage errors). Once it carries a structured cause, narrow this so only + /// infra failures retry. + #[must_use] + pub const fn is_retryable(&self) -> bool { + matches!(self, Self::StateTransition { .. }) + } +} + +/// The last successfully applied block a candidate must extend. +pub struct Tip { + pub block_id: BlockId, + pub hash: HashType, +} + +/// 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> { + let computed = block.recompute_hash(); + if computed != block.header.hash { + return Err(BlockIngestError::HashMismatch { + computed, + header: block.header.hash, + }); + } + + match tip { + None => { + if block.header.block_id != GENESIS_BLOCK_ID { + return Err(BlockIngestError::UnexpectedBlockId { + expected: GENESIS_BLOCK_ID, + got: block.header.block_id, + }); + } + } + Some(tip) => { + let expected = tip + .block_id + .checked_add(1) + .expect("block id should not overflow"); + if block.header.block_id != expected { + return Err(BlockIngestError::UnexpectedBlockId { + expected, + got: block.header.block_id, + }); + } + if block.header.prev_block_hash != tip.hash { + return Err(BlockIngestError::BrokenChainLink { + expected_prev: tip.hash, + got_prev: block.header.prev_block_hash, + }); + } + } + } + Ok(()) +} + +/// Applies a block's transactions to `state`. +pub fn apply_block(block: &Block, state: &mut V03State) -> Result<(), BlockIngestError> { + let (clock_tx, user_txs) = block + .body + .transactions + .split_last() + .ok_or(BlockIngestError::EmptyBlock)?; + + let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); + if *clock_tx != expected_clock { + return Err(BlockIngestError::InvalidClockTransaction); + } + + let is_genesis = block.header.block_id == GENESIS_BLOCK_ID; + for (tx_index, transaction) in user_txs.iter().enumerate() { + let state_transition = |err: anyhow::Error| BlockIngestError::StateTransition { + tx_index: tx_index.try_into().expect("tx index fits in u64"), + reason: format!("{err:#}"), + }; + if is_genesis { + let LeeTransaction::Public(public_tx) = transaction else { + return Err(BlockIngestError::NonPublicGenesisTransaction); + }; + state + .transition_from_public_transaction( + public_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| state_transition(err.into()))?; + } else { + transaction + .clone() + .execute_on_state(state, block.header.block_id, block.header.timestamp) + .map_err(|err| state_transition(err.into()))?; + } + } + + let LeeTransaction::Public(clock_public_tx) = clock_tx else { + return Err(BlockIngestError::InvalidClockTransaction); + }; + state + .transition_from_public_transaction( + clock_public_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| BlockIngestError::StateTransition { + tx_index: user_txs.len().try_into().expect("tx index fits in u64"), + reason: format!("{:#}", anyhow::Error::from(err)), + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serializes_and_round_trips_externally_tagged() { + let err = BlockIngestError::UnexpectedBlockId { + expected: 5, + got: 7, + }; + let value = serde_json::to_value(&err).expect("serialize"); + assert_eq!( + value, + serde_json::json!({ "UnexpectedBlockId": { "expected": 5, "got": 7 } }) + ); + let back: BlockIngestError = serde_json::from_value(value).expect("deserialize"); + assert!(matches!( + back, + BlockIngestError::UnexpectedBlockId { + expected: 5, + got: 7 + } + )); + } +} diff --git a/lez/indexer/core/src/chain_consistency.rs b/lez/chain_consistency/src/consistency.rs similarity index 53% rename from lez/indexer/core/src/chain_consistency.rs rename to lez/chain_consistency/src/consistency.rs index df4b5ba6..5c87fcad 100644 --- a/lez/indexer/core/src/chain_consistency.rs +++ b/lez/chain_consistency/src/consistency.rs @@ -1,18 +1,18 @@ -//! Startup check that the local store still belongs to the chain the -//! connected channel serves. +//! Startup check that a local store still belongs to the chain the connected +//! channel serves. use anyhow::Result; use common::{HashType, block::Block}; use futures::StreamExt as _; +use lee_core::BlockId; use log::warn; -use logos_blockchain_zone_sdk::{Slot, ZoneMessage, adapter::Node as _}; - -use crate::IndexerCore; +use logos_blockchain_core::mantle::ops::channel::ChannelId; +use logos_blockchain_zone_sdk::{Slot, ZoneMessage, adapter, indexer::ZoneIndexer}; /// Upper bound on the channel reads of the startup consistency check. const CHANNEL_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); -/// Result of comparing the indexer's stored chain against the channel. +/// Result of comparing a caller's stored chain against the channel. pub enum ChainConsistency { /// Channel still serves our anchor block (the stored tip position, or the /// parked block while stalled). @@ -24,7 +24,7 @@ pub enum ChainConsistency { /// - or the channel read was inconclusive (timeout / error / empty stream) /// /// NOTE: None of these prove a reset, so the caller proceeds. - /// A genuine divergence is still caught later when the ingest loop tries to apply and parks. + /// A genuine divergence is still caught later when applying the channel history. Inconclusive, /// Positive evidence that the channel is a different chain than the store. /// @@ -36,13 +36,13 @@ pub enum ChainConsistency { pub enum ChainMismatch { /// The channel serves a different block at the anchor's id. Block { - ours: (u64, HashType), - channel: (u64, HashType), + ours: (BlockId, HashType), + channel: (BlockId, HashType), }, /// The channel serves a block at/below the anchor's id past the anchor /// slot; on the same chain those ids live at earlier slots. ReinscribedBlock { - channel: (u64, HashType), + channel: (BlockId, HashType), slot: Slot, anchor_slot: Slot, }, @@ -97,18 +97,26 @@ impl std::fmt::Display for ChainMismatch { /// A block that must still be inscribed at `slot` if the channel is the chain /// the store was built from: the tip at the read cursor, or the recorded /// parked block while stalled. -struct Anchor { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Anchor { slot: Slot, /// The anchor block's `(id, hash)`. /// - /// `None` when parked on an undeserializable inscription (no header was recorded). - block: Option<(u64, HashType)>, + /// `None` when anchored on an undeserializable inscription (no header was recorded). + block: Option<(BlockId, HashType)>, } impl Anchor { + /// Builds an anchor at `slot` on the block `(id, hash)`, or a headerless + /// anchor (`None`) when only the slot is known. + #[must_use] + pub const fn new(slot: Slot, block: Option<(BlockId, HashType)>) -> Self { + Self { slot, block } + } + /// Probes a channel message read at/after the anchor slot. - /// See [`IndexerCore::verify_chain_at_anchor`]. - pub fn probe_anchor_slot(&self, msg: &ZoneMessage, slot: Slot) -> AnchorProbe { + /// See [`verify_chain_consistency`]. + fn probe_anchor_slot(&self, msg: &ZoneMessage, slot: Slot) -> AnchorProbe { if slot < self.slot { return AnchorProbe::KeepLooking; } @@ -157,6 +165,66 @@ impl Anchor { } } +/// Classifies a stored chain against the channel one message at a time. +/// +/// The shared driver behind both consistency consumers: +/// [`verify_chain_consistency`] runs it over a channel it reads itself, while a +/// caller that already streams the channel history (e.g. a reconstructing +/// sequencer) feeds it the same messages it replays. Run [`Self::check_frontier`] +/// once the channel tip is known, then feed messages in slot order via +/// [`Self::observe`] until a verdict is reached. +pub struct AnchorConsistencyCheck { + anchor: Anchor, + verdict: Option, +} + +impl AnchorConsistencyCheck { + /// New checker for `anchor` with an undetermined verdict. + #[must_use] + pub const fn new(anchor: Anchor) -> Self { + Self { + anchor, + verdict: None, + } + } + + /// Applies the frontier check against a known channel tip. Skip it when the + /// tip could not be read, leaving the verdict to the message scan. + pub fn check_frontier(&mut self, channel_tip_slot: Option) { + if self.verdict.is_none() { + self.verdict = frontier_verdict(self.anchor.slot, channel_tip_slot) + .map(ChainConsistency::Inconsistent); + } + } + + /// Feeds the next channel message in slot order. Returns the verdict once it + /// is known so the caller can stop, or `None` while still undetermined. + pub fn observe(&mut self, msg: &ZoneMessage, slot: Slot) -> Option<&ChainConsistency> { + if self.verdict.is_none() { + self.verdict = match self.anchor.probe_anchor_slot(msg, slot) { + AnchorProbe::SameChain => Some(ChainConsistency::Consistent), + AnchorProbe::Mismatch(mismatch) => Some(ChainConsistency::Inconsistent(mismatch)), + AnchorProbe::Bail => Some(ChainConsistency::Inconclusive), + AnchorProbe::KeepLooking => None, + }; + } + self.verdict.as_ref() + } + + /// The verdict reached so far, or `None` while undetermined. + #[must_use] + pub const fn verdict(&self) -> Option<&ChainConsistency> { + self.verdict.as_ref() + } + + /// Consumes the checker, defaulting an undetermined verdict (the anchor slot + /// was never observed) to [`ChainConsistency::Inconclusive`]. + #[must_use] + pub fn finish(self) -> ChainConsistency { + self.verdict.unwrap_or(ChainConsistency::Inconclusive) + } +} + /// What a single channel message tells the anchored consistency check. enum AnchorProbe { /// The anchor is still in place: same chain. @@ -168,114 +236,65 @@ enum AnchorProbe { KeepLooking, } -#[expect( - clippy::multiple_inherent_impl, - reason = "split for clarity & isolation of relevant code" -)] -impl IndexerCore { - /// Verifies whether the channel still serves the same chain the store was built from. - /// This may change frequently during development where we reset the chain from time to - /// time in devnet/testnet, but we do not expect [`ChainConsistency::Inconsistent`] in - /// production. - /// - /// To compare the chains, we use an [`Anchor`] block that is either the parked L2 block - /// while stalled, or the tip L2 block at its own inscription L1 slot. - pub(crate) async fn verify_chain_consistency(&self) -> Result { - let Some(anchor) = self.get_startup_anchor()? else { - // empty or cold store: nothing to compare - return Ok(ChainConsistency::Inconclusive); - }; - - self.verify_chain_at_anchor(&anchor).await +/// Detects when a local store belongs to a different chain than the connected +/// L1 (e.g. a wiped/restarted Bedrock) so startup can react instead of silently +/// diverging. +/// +/// Verifies the channel still carries the anchor block at its slot. The anchor +/// was finalized at `anchor.slot`, so the same chain must still serve it there, +/// while a reset chain re-inscribes its content only at later wall-clock slots. +/// Only positive evidence of a different chain yields +/// [`ChainConsistency::Inconsistent`]; absence of data stays +/// [`ChainConsistency::Inconclusive`]. +/// +/// `node` need only implement the zone-sdk [`adapter::Node`] trait; a throwaway +/// [`ZoneIndexer`] is built internally for the channel read. +pub async fn verify_chain_consistency( + node: &N, + channel_id: ChannelId, + anchor: &Anchor, +) -> Result +where + N: adapter::Node + Clone + Sync, +{ + let mut check = AnchorConsistencyCheck::new(anchor.clone()); + match node.channel_state(channel_id).await { + Ok(state) => check.check_frontier(state.map(|s| s.tip_slot)), + Err(err) => warn!("Failed to read channel state for the consistency check: {err:#}"), + } + if check.verdict().is_some() { + return Ok(check.finish()); } - /// Builds the anchor for the startup check. - /// - /// - If stalled, returns the recorded _parked_ block - /// - If not stalled, returns the validated tip at its _own_ inscription slot. - /// - If the store is empty, returns `None`. - fn get_startup_anchor(&self) -> Result> { - if let Some(stall) = self.store.get_stall_reason()? { - return Ok(Some(Anchor { - slot: stall.l1_slot, - block: stall.block_id.zip(stall.block_hash), - })); - } + // `next_messages` is exclusive, so `slot - 1` includes the anchor slot. + let Some(from_slot) = anchor.slot.into_inner().checked_sub(1) else { + return Ok(ChainConsistency::Inconclusive); + }; - // not stalled, so anchor on the tip at its own inscription slot - let Some(slot) = self.store.get_tip_slot()?.or(self.store.get_zone_cursor()?) else { - return Ok(None); - }; - let Some(tip_id) = self.store.get_last_block_id()? else { - return Ok(None); - }; - let Some(tip) = self.store.get_block_at_id(tip_id)? else { - return Ok(None); - }; - Ok(Some(Anchor { - slot, - block: Some((tip_id, tip.header.hash)), - })) - } + let zone_indexer = ZoneIndexer::new(channel_id, node.clone()); + let scan = async { + let stream = zone_indexer + .next_messages(Some(Slot::from(from_slot))) + .await?; + let mut stream = std::pin::pin!(stream); - /// Verifies the channel still carries the anchor block at its slot. - /// - /// The anchor was finalized at `anchor.slot`, so the same chain must still - /// serve it there, while a reset chain re-inscribes its content only at - /// later wall-clock slots. - /// - /// Only positive evidence of a different chain yields `Inconsistent`. - /// Absence of data stays `Inconclusive`. - async fn verify_chain_at_anchor(&self, anchor: &Anchor) -> Result { - match self.node.channel_state(self.config.channel_id).await { - Ok(state) => { - if let Some(mismatch) = frontier_verdict(anchor.slot, state.map(|s| s.tip_slot)) { - return Ok(ChainConsistency::Inconsistent(mismatch)); - } - } - Err(err) => { - warn!("Failed to read channel state for the consistency check: {err:#}"); + while let Some((msg, slot)) = stream.next().await { + if check.observe(&msg, slot).is_some() { + break; } } + Ok::<_, anyhow::Error>(()) + }; - // `next_messages` is exclusive, so `slot - 1` includes the anchor slot. - let Some(from_slot) = anchor.slot.into_inner().checked_sub(1) else { - return Ok(ChainConsistency::Inconclusive); - }; - - let scan = async { - let stream = self - .zone_indexer - .next_messages(Some(Slot::from(from_slot))) - .await?; - let mut stream = std::pin::pin!(stream); - - while let Some((msg, slot)) = stream.next().await { - match anchor.probe_anchor_slot(&msg, slot) { - AnchorProbe::SameChain => return Ok(Some(ChainConsistency::Consistent)), - AnchorProbe::Mismatch(mismatch) => { - return Ok(Some(ChainConsistency::Inconsistent(mismatch))); - } - AnchorProbe::Bail => return Ok(Some(ChainConsistency::Inconclusive)), - AnchorProbe::KeepLooking => { /* dont do anything */ } - } - } - Ok::<_, anyhow::Error>(None) - }; - - match tokio::time::timeout(CHANNEL_READ_TIMEOUT, scan).await { - Ok(Ok(Some(outcome))) => Ok(outcome), - Ok(Ok(None)) => Ok(ChainConsistency::Inconclusive), - Ok(Err(err)) => { - warn!( - "Failed to read the anchor slot for the consistency check; proceeding: {err:#}" - ); - Ok(ChainConsistency::Inconclusive) - } - Err(_elapsed) => { - warn!("Timed out reading the anchor slot for the consistency check; proceeding"); - Ok(ChainConsistency::Inconclusive) - } + match tokio::time::timeout(CHANNEL_READ_TIMEOUT, scan).await { + Ok(Ok(())) => Ok(check.finish()), + Ok(Err(err)) => { + warn!("Failed to read the anchor slot for the consistency check; proceeding: {err:#}"); + Ok(ChainConsistency::Inconclusive) + } + Err(_elapsed) => { + warn!("Timed out reading the anchor slot for the consistency check; proceeding"); + Ok(ChainConsistency::Inconclusive) } } } @@ -297,35 +316,13 @@ fn frontier_verdict(anchor_slot: Slot, channel_tip_slot: Option) -> Option #[cfg(test)] mod tests { - use std::time::Duration; - use common::block::HashableBlockData; use logos_blockchain_core::mantle::ops::channel::{MsgId, inscribe::Inscription}; use logos_blockchain_zone_sdk::ZoneBlock; use super::*; - use crate::{ - BlockIngestError, - block_store::AcceptOutcome, - config::{ChannelId, ClientConfig, IndexerConfig}, - }; - fn unreachable_core(dir: &std::path::Path) -> IndexerCore { - let config = IndexerConfig { - consensus_info_polling_interval: Duration::from_secs(1), - bedrock_config: ClientConfig { - addr: "http://localhost:1".parse().expect("url"), - auth: None, - }, - channel_id: ChannelId::from([1; 32]), - allow_chain_reset: false, - cross_zone: None, - bridge_lock_holdings: Vec::new(), - }; - IndexerCore::open(config, dir).expect("open core") - } - - fn test_block(block_id: u64, timestamp: u64) -> Block { + fn test_block(block_id: BlockId, timestamp: u64) -> Block { HashableBlockData { block_id, prev_block_hash: HashType([0; 32]), @@ -344,98 +341,7 @@ mod tests { } fn anchor_for(block: &Block, slot: Slot) -> Anchor { - Anchor { - slot, - block: Some((block.header.block_id, block.header.hash)), - } - } - - #[tokio::test] - async fn cold_store_is_inconclusive() { - // An empty store has no cursor, so there is nothing to compare: the check - // must be Inconclusive (not Consistent), and it returns before any L1 read. - let dir = tempfile::tempdir().expect("tempdir"); - let core = unreachable_core(dir.path()); - assert!(matches!( - core.verify_chain_consistency().await.expect("verify"), - ChainConsistency::Inconclusive - )); - } - - #[tokio::test] - async fn parked_store_with_unreachable_node_is_inconclusive() { - // Network failure is not evidence of a reset: a parked store must stay - // parked (Inconclusive), not error out or trip the wipe path. - let dir = tempfile::tempdir().expect("tempdir"); - let core = unreachable_core(dir.path()); - let parked = test_block(5, 42); - core.store - .record_stall( - Some(&parked.header), - Slot::from(1_000), - BlockIngestError::EmptyBlock, - ) - .expect("record stall"); - assert!(matches!( - core.verify_chain_consistency().await.expect("verify"), - ChainConsistency::Inconclusive - )); - } - - #[tokio::test] - async fn caught_up_store_with_unreachable_node_is_inconclusive() { - let dir = tempfile::tempdir().expect("tempdir"); - let core = unreachable_core(dir.path()); - let genesis = common::test_utils::produce_dummy_block(1, None, vec![]); - assert!(matches!( - core.store - .accept_block(&genesis, Slot::from(1_000)) - .await - .expect("accept"), - AcceptOutcome::Applied - )); - core.store - .set_zone_cursor(&Slot::from(1_000)) - .expect("set cursor"); - assert!(matches!( - core.verify_chain_consistency().await.expect("verify"), - ChainConsistency::Inconclusive - )); - } - - #[tokio::test] - async fn startup_anchor_prefers_tip_slot_over_lagging_cursor() { - // Cursor persist failures are warn-only, so the read cursor can lag the - // tip by several blocks. The anchor must pair the tip with its own - // inscription slot; pairing it with the stale cursor would make the scan - // misread the chain's intermediate blocks as re-inscriptions. - let dir = tempfile::tempdir().expect("tempdir"); - let core = unreachable_core(dir.path()); - - let genesis = common::test_utils::produce_dummy_block(1, None, vec![]); - core.store - .accept_block(&genesis, Slot::from(1_000)) - .await - .expect("accept"); - let block2 = common::test_utils::produce_dummy_block(2, Some(genesis.header.hash), vec![]); - core.store - .accept_block(&block2, Slot::from(1_005)) - .await - .expect("accept"); - let block3 = common::test_utils::produce_dummy_block(3, Some(block2.header.hash), vec![]); - core.store - .accept_block(&block3, Slot::from(1_010)) - .await - .expect("accept"); - - // Cursor last persisted at the genesis slot: two blocks behind the tip. - core.store - .set_zone_cursor(&Slot::from(1_000)) - .expect("set cursor"); - - let anchor = core.get_startup_anchor().expect("anchor").expect("present"); - assert_eq!(anchor.slot, Slot::from(1_010)); - assert_eq!(anchor.block, Some((3, block3.header.hash))); + Anchor::new(slot, Some((block.header.block_id, block.header.hash))) } #[test] @@ -516,10 +422,7 @@ mod tests { fn probe_accepts_any_message_for_a_headerless_anchor() { // A deserialize park records no header: any message still present at // the anchor slot means the history is intact. - let anchor = Anchor { - slot: Slot::from(1_000), - block: None, - }; + let anchor = Anchor::new(Slot::from(1_000), None); let garbage = ZoneMessage::Block(ZoneBlock { id: MsgId::from([0_u8; 32]), data: Inscription::try_from(&[1_u8, 2, 3][..]).expect("inscription"), diff --git a/lez/chain_consistency/src/lib.rs b/lez/chain_consistency/src/lib.rs new file mode 100644 index 00000000..d2b45ca9 --- /dev/null +++ b/lez/chain_consistency/src/lib.rs @@ -0,0 +1,9 @@ +//! Reconstructing and verifying L2 chain state from a Bedrock (L1) channel. + +pub use apply::{BlockIngestError, Tip, apply_block, validate_against_tip}; +pub use consistency::{ + Anchor, AnchorConsistencyCheck, ChainConsistency, ChainMismatch, verify_chain_consistency, +}; + +pub mod apply; +pub mod consistency; diff --git a/lez/indexer/core/Cargo.toml b/lez/indexer/core/Cargo.toml index 14941773..cbe0b7e8 100644 --- a/lez/indexer/core/Cargo.toml +++ b/lez/indexer/core/Cargo.toml @@ -13,6 +13,7 @@ testnet = [] [dependencies] common.workspace = true +chain_consistency.workspace = true logos-blockchain-zone-sdk.workspace = true lee.workspace = true lee_core.workspace = true @@ -32,7 +33,6 @@ futures.workspace = true url.workspace = true logos-blockchain-core.workspace = true serde_json.workspace = true -thiserror.workspace = true async-stream.workspace = true tokio.workspace = true risc0-zkvm.workspace = true diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index 8c974399..774aa87d 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -1,12 +1,12 @@ use std::{path::Path, sync::Arc}; use anyhow::{Context as _, Result}; +use chain_consistency::{BlockIngestError, Tip}; use common::{ - HashType, block::{BedrockStatus, Block, BlockHeader}, - transaction::{LeeTransaction, clock_invocation}, + transaction::LeeTransaction, }; -use lee::{Account, AccountId, GENESIS_BLOCK_ID, V03State}; +use lee::{Account, AccountId, V03State}; use lee_core::BlockId; use log::warn; use logos_blockchain_core::header::HeaderId; @@ -14,12 +14,7 @@ use logos_blockchain_zone_sdk::Slot; use storage::indexer::RocksDBIO; use tokio::sync::RwLock; -use crate::{ingest_error::BlockIngestError, stall_reason::StallReason}; - -struct Tip { - block_id: u64, - hash: HashType, -} +use crate::status::StallReason; /// Outcome of feeding a parsed L2 block to the validated tip. pub enum AcceptOutcome { @@ -258,14 +253,14 @@ impl IndexerStore { return Ok(AcceptOutcome::AlreadyApplied); } - if let Err(err) = validate_against_tip(tip.as_ref(), block) { + if let Err(err) = chain_consistency::validate_against_tip(tip.as_ref(), block) { self.record_stall(Some(&block.header), l1_slot, err.clone())?; return Ok(AcceptOutcome::Parked(err)); } // TODO: we use scratch state to be atomic, but need to revisit how expensive a clone is let mut scratch = self.current_state.read().await.clone(); - if let Err(err) = apply_block_to_scratch(block, &mut scratch) { + if let Err(err) = chain_consistency::apply_block(block, &mut scratch) { if err.is_retryable() { return Ok(AcceptOutcome::RetryableFailure(err)); } @@ -290,112 +285,11 @@ impl IndexerStore { } } -/// 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. -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 { - computed, - header: block.header.hash, - }); - } - - match tip { - None => { - if block.header.block_id != GENESIS_BLOCK_ID { - return Err(BlockIngestError::UnexpectedBlockId { - expected: GENESIS_BLOCK_ID, - got: block.header.block_id, - }); - } - } - Some(tip) => { - let expected = tip - .block_id - .checked_add(1) - .expect("block id should not overflow"); - if block.header.block_id != expected { - return Err(BlockIngestError::UnexpectedBlockId { - expected, - got: block.header.block_id, - }); - } - if block.header.prev_block_hash != tip.hash { - return Err(BlockIngestError::BrokenChainLink { - expected_prev: tip.hash, - got_prev: block.header.prev_block_hash, - }); - } - } - } - Ok(()) -} - -/// Applies a block's transactions to `state`, mapping every failure to a -/// [`BlockIngestError`] so the caller can park rather than crash. Operates on a -/// scratch state; the caller commits only on `Ok`. -fn apply_block_to_scratch(block: &Block, state: &mut V03State) -> Result<(), BlockIngestError> { - let (clock_tx, user_txs) = block - .body - .transactions - .split_last() - .ok_or(BlockIngestError::EmptyBlock)?; - - let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); - if *clock_tx != expected_clock { - return Err(BlockIngestError::InvalidClockTransaction); - } - - let is_genesis = block.header.block_id == GENESIS_BLOCK_ID; - for (tx_index, transaction) in user_txs.iter().enumerate() { - let state_transition = |err: anyhow::Error| BlockIngestError::StateTransition { - tx_index: tx_index.try_into().expect("tx index fits in u64"), - reason: format!("{err:#}"), - }; - if is_genesis { - let LeeTransaction::Public(public_tx) = transaction else { - return Err(BlockIngestError::NonPublicGenesisTransaction); - }; - state - .transition_from_public_transaction( - public_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| state_transition(err.into()))?; - } else { - transaction - .clone() - .execute_on_state(state, block.header.block_id, block.header.timestamp) - .map_err(|err| state_transition(err.into()))?; - } - } - - let LeeTransaction::Public(clock_public_tx) = clock_tx else { - return Err(BlockIngestError::InvalidClockTransaction); - }; - state - .transition_from_public_transaction( - clock_public_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| BlockIngestError::StateTransition { - tx_index: user_txs.len().try_into().expect("tx index fits in u64"), - reason: format!("{:#}", anyhow::Error::from(err)), - })?; - - Ok(()) -} - #[cfg(test)] mod stall_reason_tests { use common::HashType; use super::*; - use crate::{ingest_error::BlockIngestError, stall_reason::StallReason}; #[tokio::test] async fn stall_reason_roundtrips_and_clears() { @@ -541,10 +435,10 @@ mod tests { #[cfg(test)] mod accept_tests { + use chain_consistency::BlockIngestError; use common::{HashType, block::HashableBlockData, test_utils::produce_dummy_block}; use super::*; - use crate::ingest_error::BlockIngestError; fn signing_key() -> lee::PrivateKey { lee::PrivateKey::try_new([7_u8; 32]).expect("valid key") diff --git a/lez/indexer/core/src/ingest_error.rs b/lez/indexer/core/src/ingest_error.rs deleted file mode 100644 index 299013d7..00000000 --- a/lez/indexer/core/src/ingest_error.rs +++ /dev/null @@ -1,80 +0,0 @@ -use common::HashType; -use serde::{Deserialize, Serialize}; - -/// Why the indexer could not apply an L2 block from the channel. -/// -/// Persisted in `RocksDB`, so every variant must have the following -/// traits: `Clone + Serialize + Deserialize`. -#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)] -pub enum BlockIngestError { - #[error("Failed to deserialize L2 block: {0}")] - /// Here we store the error string that is derived from [`borsh::from_slice`]'s [`Err`]. - Deserialize(String), - #[error("Unexpected block id: expected {expected}, got {got}")] - UnexpectedBlockId { expected: u64, got: u64 }, - #[error("Broken chain link: expected prev {expected_prev}, got {got_prev}")] - BrokenChainLink { - expected_prev: HashType, - got_prev: HashType, - }, - #[error("Block hash mismatch: computed {computed}, header {header}")] - HashMismatch { - computed: HashType, - header: HashType, - }, - #[error("Block has no transactions")] - EmptyBlock, - #[error("Last transaction must be the public clock invocation for the block timestamp")] - InvalidClockTransaction, - #[error("Genesis block must contain only public transactions")] - NonPublicGenesisTransaction, - #[error("State transition failed at transaction {tx_index}: {reason}")] - StateTransition { - /// Index of the failing transaction within the block body. - tx_index: u64, - /// Reason string from `lee::Error` to `anyhow::Error` to `{:#}`. - /// - /// This is required because `lee::Error` is not `Clone + Serialize + Deserialize`, so we - /// cannot store it directly. - reason: String, - }, -} - -impl BlockIngestError { - /// Whether the failure may be transient rather than a property of the block. - /// - /// FIXME: `StateTransition` is too coarse — its `reason` string mixes genuine - /// state-transition rejections with infra failures (risc0 executor teardown, - /// storage errors). Once it carries a structured cause, narrow this so only - /// infra failures retry. - #[must_use] - pub const fn is_retryable(&self) -> bool { - matches!(self, Self::StateTransition { .. }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn serializes_and_round_trips_externally_tagged() { - let err = BlockIngestError::UnexpectedBlockId { - expected: 5, - got: 7, - }; - let value = serde_json::to_value(&err).expect("serialize"); - assert_eq!( - value, - serde_json::json!({ "UnexpectedBlockId": { "expected": 5, "got": 7 } }) - ); - let back: BlockIngestError = serde_json::from_value(value).expect("deserialize"); - assert!(matches!( - back, - BlockIngestError::UnexpectedBlockId { - expected: 5, - got: 7 - } - )); - } -} diff --git a/lez/indexer/core/src/lib.rs b/lez/indexer/core/src/lib.rs index 7bee6387..dd58eaae 100644 --- a/lez/indexer/core/src/lib.rs +++ b/lez/indexer/core/src/lib.rs @@ -2,31 +2,29 @@ use std::{path::Path, sync::Arc}; use anyhow::Result; use arc_swap::ArcSwap; +pub use chain_consistency::BlockIngestError; +use chain_consistency::{Anchor, ChainConsistency}; use common::block::Block; // TODO: Remove after testnet use futures::StreamExt as _; -pub use ingest_error::BlockIngestError; use log::{error, info, warn}; use logos_blockchain_zone_sdk::{ CommonHttpClient, Slot, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer, }; use retry::ApplyRetryGate; -pub use stall_reason::StallReason; +pub use status::StallReason; use crate::{ block_store::{AcceptOutcome, IndexerStore}, - chain_consistency::ChainConsistency, config::IndexerConfig, cross_zone_verifier::CrossZoneVerifier, status::{IndexerStatus, IndexerSyncStatus}, }; + pub mod block_store; -pub mod chain_consistency; pub mod config; pub mod cross_zone_verifier; -pub mod ingest_error; mod retry; -pub mod stall_reason; pub mod status; /// Consecutive failed apply attempts of the same block before parking. @@ -125,6 +123,53 @@ impl IndexerCore { }) } + /// Verifies whether the channel still serves the same chain the store was built from. + /// This may change frequently during development where we reset the chain from time to + /// time in devnet/testnet, but we do not expect [`ChainConsistency::Inconsistent`] in + /// production. + /// + /// To compare the chains, we use an [`Anchor`] block that is either the parked L2 block + /// while stalled, or the tip L2 block at its own inscription L1 slot. + pub(crate) async fn verify_chain_consistency(&self) -> Result { + let Some(anchor) = self.get_startup_anchor()? else { + // empty or cold store: nothing to compare + return Ok(ChainConsistency::Inconclusive); + }; + + chain_consistency::verify_chain_consistency(&self.node, self.config.channel_id, &anchor) + .await + } + + /// Builds the anchor for the startup check. + /// + /// - If stalled, returns the recorded _parked_ block + /// - If not stalled, returns the validated tip at its _own_ inscription slot. + /// - If the store is empty, returns `None`. + fn get_startup_anchor(&self) -> Result> { + if let Some(stall) = self.store.get_stall_reason()? { + return Ok(Some(Anchor::new( + stall.l1_slot, + stall.block_id.zip(stall.block_hash), + ))); + } + + // not stalled, so anchor on the tip at its own inscription slot + let Some(slot) = self + .store + .get_tip_slot()? + .map_or_else(|| self.store.get_zone_cursor(), |slot| Ok(Some(slot)))? + else { + return Ok(None); + }; + let Some(tip_id) = self.store.get_last_block_id()? else { + return Ok(None); + }; + let Some(tip) = self.store.get_block_at_id(tip_id)? else { + return Ok(None); + }; + Ok(Some(Anchor::new(slot, Some((tip_id, tip.header.hash))))) + } + /// Snapshot of the current ingestion status (sync state + indexed tip). /// /// Combines the ingest loop's live status with the L2 tip read fresh from the @@ -207,8 +252,8 @@ impl IndexerCore { let mut cursor = initial_cursor; let mut retry_gate = ApplyRetryGate::new(); - if cursor.is_some() { - info!("Resuming indexer from cursor {cursor:?}"); + if let Some(slot) = &cursor { + info!("Resuming indexer from cursor {slot:?}"); } else { info!("Starting indexer from beginning of channel"); } @@ -370,3 +415,127 @@ impl IndexerCore { } } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use common::{HashType, block::HashableBlockData}; + use logos_blockchain_zone_sdk::Slot; + + use super::*; + use crate::config::{ChannelId, ClientConfig, IndexerConfig}; + + fn unreachable_core(dir: &std::path::Path) -> IndexerCore { + let config = IndexerConfig { + consensus_info_polling_interval: Duration::from_secs(1), + bedrock_config: ClientConfig { + addr: "http://localhost:1".parse().expect("url"), + auth: None, + }, + channel_id: ChannelId::from([1; 32]), + allow_chain_reset: false, + cross_zone: None, + bridge_lock_holdings: Vec::new(), + }; + IndexerCore::open(config, dir).expect("open core") + } + + fn test_block(block_id: u64, timestamp: u64) -> Block { + HashableBlockData { + block_id, + prev_block_hash: HashType([0; 32]), + timestamp, + transactions: vec![], + } + .into_pending_block(&lee::PrivateKey::try_new([7; 32]).expect("valid key")) + } + + #[tokio::test] + async fn cold_store_is_inconclusive() { + // An empty store has no cursor, so there is nothing to compare: the check + // must be Inconclusive (not Consistent), and it returns before any L1 read. + let dir = tempfile::tempdir().expect("tempdir"); + let core = unreachable_core(dir.path()); + assert!(matches!( + core.verify_chain_consistency().await.expect("verify"), + ChainConsistency::Inconclusive + )); + } + + #[tokio::test] + async fn parked_store_with_unreachable_node_is_inconclusive() { + // Network failure is not evidence of a reset: a parked store must stay + // parked (Inconclusive), not error out or trip the wipe path. + let dir = tempfile::tempdir().expect("tempdir"); + let core = unreachable_core(dir.path()); + let parked = test_block(5, 42); + core.store + .record_stall( + Some(&parked.header), + Slot::from(1_000), + BlockIngestError::EmptyBlock, + ) + .expect("record stall"); + assert!(matches!( + core.verify_chain_consistency().await.expect("verify"), + ChainConsistency::Inconclusive + )); + } + + #[tokio::test] + async fn caught_up_store_with_unreachable_node_is_inconclusive() { + let dir = tempfile::tempdir().expect("tempdir"); + let core = unreachable_core(dir.path()); + let genesis = common::test_utils::produce_dummy_block(1, None, vec![]); + assert!(matches!( + core.store + .accept_block(&genesis, Slot::from(1_000)) + .await + .expect("accept"), + AcceptOutcome::Applied + )); + core.store + .set_zone_cursor(&Slot::from(1_000)) + .expect("set cursor"); + assert!(matches!( + core.verify_chain_consistency().await.expect("verify"), + ChainConsistency::Inconclusive + )); + } + + #[tokio::test] + async fn startup_anchor_prefers_tip_slot_over_lagging_cursor() { + // Cursor persist failures are warn-only, so the read cursor can lag the + // tip by several blocks. The anchor must pair the tip with its own + // inscription slot; pairing it with the stale cursor would make the scan + // misread the chain's intermediate blocks as re-inscriptions. + let dir = tempfile::tempdir().expect("tempdir"); + let core = unreachable_core(dir.path()); + + let genesis = common::test_utils::produce_dummy_block(1, None, vec![]); + core.store + .accept_block(&genesis, Slot::from(1_000)) + .await + .expect("accept"); + let block2 = common::test_utils::produce_dummy_block(2, Some(genesis.header.hash), vec![]); + core.store + .accept_block(&block2, Slot::from(1_005)) + .await + .expect("accept"); + let block3 = common::test_utils::produce_dummy_block(3, Some(block2.header.hash), vec![]); + core.store + .accept_block(&block3, Slot::from(1_010)) + .await + .expect("accept"); + + // Cursor last persisted at the genesis slot: two blocks behind the tip. + core.store + .set_zone_cursor(&Slot::from(1_000)) + .expect("set cursor"); + + let anchor = core.get_startup_anchor().expect("anchor").expect("present"); + let expected = Anchor::new(Slot::from(1_010), Some((3, block3.header.hash))); + assert_eq!(anchor, expected); + } +} diff --git a/lez/indexer/core/src/stall_reason.rs b/lez/indexer/core/src/stall_reason.rs deleted file mode 100644 index c5118fcc..00000000 --- a/lez/indexer/core/src/stall_reason.rs +++ /dev/null @@ -1,25 +0,0 @@ -use common::HashType; -use logos_blockchain_zone_sdk::Slot; -use serde::{Deserialize, Serialize}; - -use crate::ingest_error::BlockIngestError; - -/// Diagnostic record of the first block that broke the L2 chain. -/// -/// The block-derived fields are `None` for a deserialize break (no header was -/// ever parsed). `l1_slot` is the L1 slot the breaking inscription was read at. -/// `first_seen` is the breaking block's L2 timestamp (`None` for a deserialize break). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StallReason { - pub block_id: Option, - pub block_hash: Option, - pub prev_block_hash: Option, - pub l1_slot: Slot, - pub error: BlockIngestError, - pub first_seen: Option, - /// Number of later non-chaining blocks (orphans, since the tip is frozen). - /// - /// TODO: We could store a different "branch" of blocks following this break, but for now we - /// just count them. - pub orphans_since: u64, -} diff --git a/lez/indexer/core/src/status.rs b/lez/indexer/core/src/status.rs index a483fde6..1307238b 100644 --- a/lez/indexer/core/src/status.rs +++ b/lez/indexer/core/src/status.rs @@ -1,6 +1,27 @@ -use serde::Serialize; +use chain_consistency::BlockIngestError; +use common::HashType; +use logos_blockchain_zone_sdk::Slot; +use serde::{Deserialize, Serialize}; -use crate::stall_reason::StallReason; +/// Diagnostic record of the first block that broke the L2 chain. +/// +/// The block-derived fields are `None` for a deserialize break (no header was +/// ever parsed). `l1_slot` is the L1 slot the breaking inscription was read at. +/// `first_seen` is the breaking block's L2 timestamp (`None` for a deserialize break). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StallReason { + pub block_id: Option, + pub block_hash: Option, + pub prev_block_hash: Option, + pub l1_slot: Slot, + pub error: BlockIngestError, + pub first_seen: Option, + /// Number of later non-chaining blocks (orphans, since the tip is frozen). + /// + /// TODO: We could store a different "branch" of blocks following this break, but for now we + /// just count them. + pub orphans_since: u64, +} /// Coarse lifecycle state of the indexer's ingestion loop, so a client can tell /// "still catching up" apart from "something went wrong". @@ -119,8 +140,6 @@ mod tests { fn stalled_status_serializes_with_stall_reason() { use logos_blockchain_zone_sdk::Slot; - use crate::{ingest_error::BlockIngestError, stall_reason::StallReason}; - let status = IndexerStatus { sync: IndexerSyncStatus::stalled("broken chain link".to_owned()), indexed_block_id: Some(41), diff --git a/lez/sequencer/core/Cargo.toml b/lez/sequencer/core/Cargo.toml index cd89f223..1fa7494d 100644 --- a/lez/sequencer/core/Cargo.toml +++ b/lez/sequencer/core/Cargo.toml @@ -11,6 +11,7 @@ workspace = true lee.workspace = true lee_core.workspace = true common.workspace = true +chain_consistency.workspace = true storage.workspace = true mempool.workspace = true logos-blockchain-zone-sdk.workspace = true diff --git a/lez/sequencer/core/src/block_publisher.rs b/lez/sequencer/core/src/block_publisher.rs index 21551131..3f2b2a62 100644 --- a/lez/sequencer/core/src/block_publisher.rs +++ b/lez/sequencer/core/src/block_publisher.rs @@ -2,14 +2,16 @@ use std::{pin::Pin, sync::Arc, time::Duration}; use anyhow::{Context as _, Result, anyhow}; use common::block::Block; +use futures::Stream; use log::{info, warn}; pub use logos_blockchain_core::mantle::ops::channel::MsgId; use logos_blockchain_core::mantle::ops::channel::{ChannelId, inscribe::Inscription}; pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey}; pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; use logos_blockchain_zone_sdk::{ - CommonHttpClient, - adapter::NodeHttpClient, + CommonHttpClient, Slot, ZoneMessage, + adapter::{Node as _, NodeHttpClient}, + indexer::ZoneIndexer, sequencer::{ DepositInfo, Event, FinalizedOp, InscriptionInfo, SequencerConfig as ZoneSdkSequencerConfig, WithdrawArg, WithdrawInfo, ZoneSequencer, @@ -42,7 +44,7 @@ pub type OnWithdrawEventSink = Box Pin + Send>> + Send + 'static>; #[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")] -pub trait BlockPublisherTrait: Clone { +pub trait BlockPublisherTrait: Sized { #[expect( clippy::too_many_arguments, reason = "Looks better than bundling all those callbacks into a struct" @@ -63,15 +65,30 @@ pub trait BlockPublisherTrait: Clone { async fn publish_block(&self, block: &Block, withdrawals: Vec) -> Result<()>; fn channel_id(&self) -> ChannelId; + + /// Current channel frontier slot on the connected chain, or `None` if the + /// channel does not exist there. Drives the startup frontier check. + async fn channel_tip_slot(&self) -> Result>; + + /// Finalized channel messages from `after_slot` (exclusive) up to LIB, used + /// for the startup consistency check and reconstruction. Pass `None` to read + /// from the channel's genesis. + async fn read_channel_after( + &self, + after_slot: Option, + ) -> Result + '_>; } /// Real block publisher backed by zone-sdk's `ZoneSequencer`. -#[derive(Clone)] pub struct ZoneSdkPublisher { channel_id: ChannelId, + /// Direct node handle retained for channel reads (startup consistency check + /// and reconstruction); the sequencer itself lives in the drive task. + node: NodeHttpClient, publish_tx: mpsc::Sender<(Inscription, Vec)>, // Aborts the drive task when the last clone is dropped. _drive_task: Arc, + indexer: ZoneIndexer, } struct DriveTaskGuard(JoinHandle<()>); @@ -104,7 +121,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher { let mut sequencer = ZoneSequencer::init_with_config( config.channel_id, bedrock_signing_key, - node, + node.clone(), zone_sdk_config, initial_checkpoint, ); @@ -196,6 +213,8 @@ impl BlockPublisherTrait for ZoneSdkPublisher { Ok(Self { channel_id: config.channel_id, + indexer: ZoneIndexer::new(config.channel_id, node.clone()), + node, publish_tx, _drive_task: Arc::new(DriveTaskGuard(drive_task)), }) @@ -218,6 +237,27 @@ impl BlockPublisherTrait for ZoneSdkPublisher { fn channel_id(&self) -> ChannelId { self.channel_id } + + async fn channel_tip_slot(&self) -> Result> { + Ok(self + .node + .channel_state(self.channel_id) + .await + .context("Failed to read channel state")? + .map(|state| state.tip_slot)) + } + + async fn read_channel_after( + &self, + after_slot: Option, + ) -> Result + '_> { + let stream = self + .indexer + .next_messages(after_slot) + .await + .context("Failed to start channel read stream")?; + Ok(stream) + } } /// Deserialize inscription payload as a `Block` and return it's`block_id`. diff --git a/lez/sequencer/core/src/block_store.rs b/lez/sequencer/core/src/block_store.rs index 77b57661..955ae7c0 100644 --- a/lez/sequencer/core/src/block_store.rs +++ b/lez/sequencer/core/src/block_store.rs @@ -12,7 +12,7 @@ use log::info; use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; use storage::sequencer::{ RocksDBIO, - sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey}, + sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey, ZoneAnchorRecord}, }; pub use storage::{DbResult, sequencer::DbDump}; @@ -28,6 +28,45 @@ impl SequencerStore { /// Open existing database at the given location. Fails if no database is found. pub fn open_db(location: &Path, signing_key: lee::PrivateKey) -> DbResult { let dbio = Arc::new(RocksDBIO::open(location)?); + Self::from_dbio_and_signing_key(dbio, signing_key) + } + + /// Create a fresh rocksdb at `location` from `dump`. + pub fn restore_db_from_dump( + location: &Path, + dump: &DbDump, + signing_key: lee::PrivateKey, + ) -> DbResult { + let dbio = Arc::new(RocksDBIO::restore_from_dump(location, dump)?); + Self::from_dbio_and_signing_key(dbio, signing_key) + } + + /// Starting database at the start of new chain. + /// Creates files if necessary. + /// + /// ATTENTION: Will overwrite genesis block. + pub fn create_db_with_genesis( + location: &Path, + genesis_block: &Block, + genesis_state: &V03State, + signing_key: lee::PrivateKey, + ) -> DbResult { + let dbio = Arc::new(RocksDBIO::create(location, genesis_block, genesis_state)?); + let genesis_id = dbio.get_meta_first_block_in_db()?; + let tx_hash_to_block_map = block_to_transactions_map(genesis_block); + + Ok(Self { + dbio, + tx_hash_to_block_map, + genesis_id, + signing_key, + }) + } + + fn from_dbio_and_signing_key( + dbio: Arc, + signing_key: lee::PrivateKey, + ) -> DbResult { let genesis_id = dbio.get_meta_first_block_in_db()?; let last_id = dbio.latest_block_meta()?.id; @@ -53,28 +92,6 @@ impl SequencerStore { }) } - /// Starting database at the start of new chain. - /// Creates files if necessary. - /// - /// ATTENTION: Will overwrite genesis block. - pub fn create_db_with_genesis( - location: &Path, - genesis_block: &Block, - genesis_state: &V03State, - signing_key: lee::PrivateKey, - ) -> DbResult { - let dbio = Arc::new(RocksDBIO::create(location, genesis_block, genesis_state)?); - let genesis_id = dbio.get_meta_first_block_in_db()?; - let tx_hash_to_block_map = block_to_transactions_map(genesis_block); - - Ok(Self { - dbio, - tx_hash_to_block_map, - genesis_id, - signing_key, - }) - } - /// Shared handle to the underlying rocksdb. Used to persist the zone-sdk /// checkpoint from the sequencer's drive task without needing &mut to the /// store. @@ -165,13 +182,6 @@ impl SequencerStore { self.dbio.dump_all() } - /// Create a fresh rocksdb at `location` from `dump`, closing it before returning so a sequencer - /// can open it normally afterwards. - pub fn restore_db_from_dump(location: &Path, dump: &DbDump) -> DbResult<()> { - RocksDBIO::restore_from_dump(location, dump)?; - Ok(()) - } - pub fn get_zone_checkpoint(&self) -> Result> { let Some(bytes) = self.dbio.get_zone_sdk_checkpoint_bytes()? else { return Ok(None); @@ -188,6 +198,16 @@ impl SequencerStore { Ok(()) } + /// The last channel block read back and verified from Bedrock (L1 slot + + /// `id`/`hash`), or `None` before any block has been read from the channel. + pub fn get_zone_anchor(&self) -> DbResult> { + self.dbio.get_zone_cursor() + } + + pub fn set_zone_anchor(&self, anchor: &ZoneAnchorRecord) -> DbResult<()> { + self.dbio.put_zone_cursor(anchor) + } + pub fn get_unfulfilled_deposit_events(&self) -> DbResult> { self.dbio.get_pending_deposit_events() } diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 3a96e6d5..c7b55f97 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -2,18 +2,23 @@ use std::{path::Path, sync::Arc, time::Instant}; use anyhow::{Context as _, Result, anyhow}; use borsh::BorshDeserialize; +use chain_consistency::{Anchor, AnchorConsistencyCheck, ChainConsistency, ChainMismatch, Tip}; use common::{ HashType, block::{BedrockStatus, Block, HashableBlockData}, transaction::{LeeTransaction, clock_invocation}, }; use config::{GenesisAction, SequencerConfig}; +use futures::StreamExt as _; use itertools::Itertools as _; -use lee::{AccountId, PublicTransaction, public_transaction::Message}; +use lee::{AccountId, PublicTransaction, V03State, public_transaction::Message}; use lee_core::GENESIS_BLOCK_ID; use log::{error, info, warn}; use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key}; -use logos_blockchain_zone_sdk::sequencer::{DepositInfo, WithdrawArg}; +use logos_blockchain_zone_sdk::{ + Slot, ZoneMessage, + sequencer::{DepositInfo, WithdrawArg}, +}; use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; @@ -21,7 +26,7 @@ use num_bigint::BigUint; pub use storage::error::DbError; use storage::sequencer::{ RocksDBIO, - sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey}, + sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey, ZoneAnchorRecord}, }; use crate::{ @@ -72,7 +77,7 @@ impl SequencerCore { /// assumed to represent the correct latest state consistent with Bedrock-finalized data. /// If no database is found, the sequencer performs a fresh start from genesis, /// initializing its state with the accounts defined in the configuration file. - fn open_or_create_store(config: &SequencerConfig) -> (SequencerStore, lee::V03State, Block) { + fn open_or_create_store(config: &SequencerConfig) -> (SequencerStore, lee::V03State) { let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); let db_path = config.home.join("rocksdb"); @@ -86,11 +91,7 @@ impl SequencerCore { let state = store .get_lee_state() .expect("Failed to read state from store"); - let genesis_block = store - .get_block_at_id(store.genesis_id()) - .expect("Failed to read genesis block from store") - .expect("Genesis block not found in store"); - (store, state, genesis_block) + (store, state) } else { warn!( "Database not found at {}, starting from genesis", @@ -115,7 +116,7 @@ impl SequencerCore { ) .expect("Failed to create database with genesis block"); - (store, genesis_state, genesis_block) + (store, genesis_state) } } @@ -126,11 +127,7 @@ impl SequencerCore { load_or_create_signing_key(&config.home.join("bedrock_signing_key")) .expect("Failed to load or create bedrock signing key"); - let (store, state, _genesis_block) = Self::open_or_create_store(&config); - - let latest_block_meta = store - .latest_block_meta() - .expect("Failed to read latest block meta from store"); + let (mut store, mut state) = Self::open_or_create_store(&config); let initial_checkpoint = store .get_zone_checkpoint() @@ -153,8 +150,28 @@ impl SequencerCore { .await .expect("Failed to initialize Block Publisher"); - // Fresh start (no checkpoint): republish all pending blocks - if is_fresh_start { + // Cross-zone messaging: start a watcher per configured peer. The inbox + // config account is seeded into genesis state in `build_genesis_state`. + if let Some(cross_zone) = &config.cross_zone { + cross_zone_watcher::spawn_watchers( + &config.bedrock_config, + cross_zone, + config.block_create_timeout, + &mempool_handle, + ); + } + // Before producing, verify our local state still belongs to the chain + // the channel serves and replay any channel blocks we are missing + // (e.g. from other sequencers). + let chanel_was_empty = + Self::verify_and_reconstruct(&block_publisher, &mut store, &mut state, is_fresh_start) + .await + .expect("Failed to verify/reconstruct sequencer state from Bedrock"); + + // Publish our blocks only when bootstrapping an empty channel. If the + // channel already has blocks (another sequencer bootstrapped it), we + // adopted them during reconstruction instead. + if is_fresh_start && chanel_was_empty { let mut pending_blocks = store .get_all_blocks() .filter_ok(|block| matches!(block.bedrock_status, BedrockStatus::Pending)) @@ -182,16 +199,9 @@ impl SequencerCore { } } - // Cross-zone messaging: start a watcher per configured peer. The inbox - // config account is seeded into genesis state in `build_genesis_state`. - if let Some(cross_zone) = &config.cross_zone { - cross_zone_watcher::spawn_watchers( - &config.bedrock_config, - cross_zone, - config.block_create_timeout, - &mempool_handle, - ); - } + let latest_block_meta = store + .latest_block_meta() + .expect("Failed to read latest block meta from store"); let sequencer_core = Self { state, @@ -205,6 +215,204 @@ impl SequencerCore { (sequencer_core, mempool_handle) } + /// Verifies the local store still belongs to the chain the connected channel + /// serves and replays any finalized channel blocks missing locally into + /// `state`/`store`, recording each block's L1 inscription slot as the new + /// anchor. Fails (never parks) on any divergence. + /// + /// Returns whatever channel was empty or not. + async fn verify_and_reconstruct( + publisher: &BP, + store: &mut SequencerStore, + state: &mut V03State, + is_fresh_start: bool, + ) -> Result { + let anchor_record = store + .get_zone_anchor() + .context("Failed to read zone anchor")?; + + let after_slot = anchor_record + .and_then(|record| record.slot.checked_sub(1)) + .map(Slot::from); + let channel_tip_slot = publisher + .channel_tip_slot() + .await + .context("Failed to read channel tip slot")?; + + // If this sequencer has already committed blocks to the channel, that + // channel must still exist. A missing channel then means a wiped/rewound + // Bedrock or a node pointing at a different chain, so refuse to resume + // onto a foreign channel. + // + // "Committed" requires *both* a non-genesis tip and a checkpoint that was + // persisted before this startup: the tip alone is set the moment we produce + // (before the channel confirms it), while a checkpoint alone is written by + // zone-sdk's cold-start backfill even on a brand-new empty channel before we + // publish genesis. We must read the checkpoint presence from before `BP::new` + // ran (`!is_fresh_start`), because its cold-start backfill re-persists a + // checkpoint by the time we reach here — reading the store now would always + // see one. Together they mean we produced blocks and zone-sdk processed + // channel activity in a prior run. + let local_tip = store + .latest_block_meta() + .context("Failed to read latest block 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() { + 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." + )); + } + + let divergence_error = |mismatch: &ChainMismatch| { + anyhow!( + "Sequencer store diverges from the Bedrock channel ({mismatch}). \ + Delete the sequencer storage directory or point at the correct channel." + ) + }; + + // With a recorded anchor, probe the channel for positive evidence of a + // different chain: the frontier upfront (a missing/behind channel serves + // no messages to scan), then the anchor block as messages stream in. + let mut consistency_check = anchor_record.map(|record| { + let anchor = Anchor::new( + Slot::from(record.slot), + Some((record.block_id, record.hash)), + ); + let mut check = AnchorConsistencyCheck::new(anchor); + check.check_frontier(channel_tip_slot); + check + }); + if let Some(ChainConsistency::Inconsistent(mismatch)) = consistency_check + .as_ref() + .and_then(AnchorConsistencyCheck::verdict) + { + return Err(divergence_error(mismatch)); + } + + // Verify each message against the anchor and replay the + // blocks (applying the ones we miss, checking the ones we hold). + let messages = publisher + .read_channel_after(after_slot) + .await + .context("Failed to read channel history for reconstruction")?; + let mut messages = std::pin::pin!(messages); + let mut channel_is_empty = true; + while let Some((message, slot)) = messages.next().await { + if let Some(check) = &mut consistency_check + && let Some(ChainConsistency::Inconsistent(mismatch)) = + check.observe(&message, slot) + { + return Err(divergence_error(mismatch)); + } + + let ZoneMessage::Block(zone_block) = message else { + continue; + }; + let block: Block = borsh::from_slice(&zone_block.data).map_err(|err| { + anyhow!( + "Failed to deserialize channel block at slot {}: {err}", + slot.into_inner() + ) + })?; + channel_is_empty = false; + Self::apply_reconstructed_block(store, state, &block, slot)?; + } + + Ok(channel_is_empty) + } + + /// Applies a single channel block during reconstruction: idempotent for + /// blocks we already hold (verifying their hash), a validated continuation + /// for new ones. Advances the persisted anchor to the block's slot. + fn apply_reconstructed_block( + store: &mut SequencerStore, + state: &mut V03State, + block: &Block, + slot: Slot, + ) -> Result<()> { + let tip = store + .latest_block_meta() + .context("Failed to read latest block meta")?; + let block_id = block.header.block_id; + let block_hash = block.header.hash; + + let record = ZoneAnchorRecord { + slot: slot.into_inner(), + block_id, + hash: block_hash, + }; + + // A block at/below the tip must match what we already stored, otherwise + // the channel is a different chain. + if block_id <= tip.id { + match store + .get_block_at_id(block_id) + .context("Failed to read stored block")? + { + Some(stored) if stored.header.hash == block_hash => { + store + .set_zone_anchor(&record) + .context("Failed to persist zone anchor")?; + return Ok(()); + } + Some(stored) => { + return Err(anyhow!( + "Channel block {block_id} hash {block_hash} does not match stored hash {}", + stored.header.hash + )); + } + None => { + return Err(anyhow!( + "Channel block {block_id} is at/below local tip {} but is missing locally", + tip.id + )); + } + } + } + + // New continuation: validate it chains onto the tip, then apply. + let cc_tip = Tip { + block_id: tip.id, + hash: tip.hash, + }; + chain_consistency::validate_against_tip(Some(&cc_tip), block).map_err(|err| { + anyhow!( + "Channel block {block_id} does not extend local tip {}: {err}", + tip.id + ) + })?; + + let mut scratch = state.clone(); + chain_consistency::apply_block(block, &mut scratch) + .map_err(|err| anyhow!("Failed to apply channel block {block_id}: {err}"))?; + + // Derive bridge bookkeeping from the block's transactions, matching the + // production path so the reconciliation counters stay consistent. + let mut deposit_event_ids = Vec::new(); + let mut withdrawals = Vec::new(); + for tx in &block.body.transactions { + if let Some(deposit_id) = extract_bridge_deposit_id(tx) { + deposit_event_ids.push(deposit_id); + } + if let Some(withdraw) = extract_bridge_withdraw_data(tx) { + withdrawals.push(withdraw_event_reconciliation_key(&withdraw.outputs)?); + } + } + + store + .update(block, &deposit_event_ids, withdrawals, &scratch) + .context("Failed to persist reconstructed block")?; + *state = scratch; + store + .set_zone_anchor(&record) + .context("Failed to persist zone anchor")?; + Ok(()) + } + fn on_checkpoint(dbio: Arc) -> block_publisher::CheckpointSink { Box::new(move |cp| { let bytes = match serde_json::to_vec(&cp) { @@ -561,8 +769,8 @@ impl SequencerCore { .collect()) } - pub fn block_publisher(&self) -> BP { - self.block_publisher.clone() + pub const fn block_publisher(&self) -> &BP { + &self.block_publisher } fn next_block_id(&self) -> u64 { diff --git a/lez/sequencer/core/src/mock.rs b/lez/sequencer/core/src/mock.rs index 39f635f9..f8349fac 100644 --- a/lez/sequencer/core/src/mock.rs +++ b/lez/sequencer/core/src/mock.rs @@ -2,9 +2,10 @@ use std::time::Duration; use anyhow::Result; use common::block::Block; +use futures::Stream; use logos_blockchain_core::mantle::ops::channel::ChannelId; use logos_blockchain_key_management_system_service::keys::Ed25519Key; -use logos_blockchain_zone_sdk::sequencer::WithdrawArg; +use logos_blockchain_zone_sdk::{Slot, ZoneMessage, sequencer::WithdrawArg}; use crate::{ block_publisher::{ @@ -19,6 +20,28 @@ pub type SequencerCoreWithMockClients = crate::SequencerCore #[derive(Clone)] pub struct MockBlockPublisher { channel_id: ChannelId, + /// Canned channel frontier returned by [`Self::channel_tip_slot`]. + tip_slot: Option, + /// Canned finalized channel history returned by [`Self::read_channel_after`]. + messages: Vec<(ZoneMessage, Slot)>, +} + +impl MockBlockPublisher { + /// Builds a mock publisher backed by a canned channel, for reconstruction + /// and consistency tests. The default (via [`BlockPublisherTrait::new`]) + /// serves an empty channel. + #[must_use] + pub const fn with_canned_channel( + channel_id: ChannelId, + tip_slot: Option, + messages: Vec<(ZoneMessage, Slot)>, + ) -> Self { + Self { + channel_id, + tip_slot, + messages, + } + } } impl BlockPublisherTrait for MockBlockPublisher { @@ -34,6 +57,8 @@ impl BlockPublisherTrait for MockBlockPublisher { ) -> Result { Ok(Self { channel_id: config.channel_id, + tip_slot: None, + messages: Vec::new(), }) } @@ -48,4 +73,21 @@ impl BlockPublisherTrait for MockBlockPublisher { fn channel_id(&self) -> ChannelId { self.channel_id } + + async fn channel_tip_slot(&self) -> Result> { + Ok(self.tip_slot) + } + + async fn read_channel_after( + &self, + after_slot: Option, + ) -> Result + '_> { + // Mirror `next_messages`: `after_slot` is exclusive. + let messages = self + .messages + .iter() + .filter(move |(_, slot)| after_slot.is_none_or(|after| *slot > after)) + .cloned(); + Ok(futures::stream::iter(messages)) + } } diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index 05e2ec4c..bcc40a0a 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -37,6 +37,8 @@ use crate::{ mock::SequencerCoreWithMockClients, }; +mod reconstruction; + #[derive(borsh::BorshSerialize)] struct DepositMetadataForEncoding { recipient_id: lee::AccountId, diff --git a/lez/sequencer/core/src/tests/reconstruction.rs b/lez/sequencer/core/src/tests/reconstruction.rs new file mode 100644 index 00000000..2bd9ebcd --- /dev/null +++ b/lez/sequencer/core/src/tests/reconstruction.rs @@ -0,0 +1,401 @@ +#![expect( + clippy::arithmetic_side_effects, + clippy::as_conversions, + reason = "We don't care about it in tests" +)] + +use common::block::Block; +use logos_blockchain_core::mantle::ops::channel::{MsgId, inscribe::Inscription}; +use logos_blockchain_zone_sdk::{Slot, ZoneBlock, ZoneMessage}; +use storage::sequencer::sequencer_cells::ZoneAnchorRecord; + +use super::*; +use crate::{ + SequencerCore, block_store::SequencerStore, config::GenesisAction, mock::MockBlockPublisher, +}; + +fn block_to_channel_message(block: &Block, slot: u64) -> (ZoneMessage, Slot) { + let bytes = borsh::to_vec(block).expect("serialize block"); + let message = ZoneMessage::Block(ZoneBlock { + id: MsgId::from([0_u8; 32]), + data: Inscription::try_from(bytes.as_slice()).expect("inscription"), + }); + (message, Slot::from(slot)) +} + +/// Collects a sequencer's whole chain (genesis..=tip) into a canned channel, +/// 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; + (genesis_id..=tip_id) + .enumerate() + .map(|(index, id)| { + let block = store.get_block_at_id(id).expect("read").expect("present"); + block_to_channel_message(&block, (index as u64 + 1) * slot_step) + }) + .collect() +} + +#[tokio::test] +async fn reconstructs_missing_channel_blocks_into_fresh_store() { + // Sequencer A produces a few blocks; treat its chain as the channel. + let config_a = setup_sequencer_config(); + let (mut seq_a, _handle_a) = + 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 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; + + // Sequencer B starts from a fresh store and reconstructs A's chain. + let config_b = setup_sequencer_config(); + let (mut store_b, mut state_b) = + SequencerCore::::open_or_create_store(&config_b); + let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages); + + let channel_was_empty = SequencerCore::::verify_and_reconstruct( + &mock_b, + &mut store_b, + &mut state_b, + true, + ) + .await + .expect("reconstruct"); + assert!(!channel_was_empty); + + let tip_b = store_b.latest_block_meta().unwrap(); + assert_eq!(tip_b.id, tip_a.id); + assert_eq!(tip_b.hash, tip_a.hash); + + // State matches: initial account balances agree with sequencer A. + for account in initial_public_user_accounts() { + assert_eq!( + state_b.get_account_by_id(account.account_id).balance, + seq_a.state().get_account_by_id(account.account_id).balance, + ); + } + + let anchor = store_b.get_zone_anchor().unwrap().expect("anchor recorded"); + assert_eq!(anchor.block_id, tip_a.id); + assert_eq!(anchor.slot, tip_slot.into_inner()); + + // Re-running is idempotent: everything is already applied, no error. + let channel_was_empty = SequencerCore::::verify_and_reconstruct( + &mock_b, + &mut store_b, + &mut state_b, + true, + ) + .await + .expect("reconstruct idempotent"); + assert!(!channel_was_empty); + assert_eq!(store_b.latest_block_meta().unwrap().id, tip_a.id); +} + +#[tokio::test] +async fn fails_when_channel_serves_a_divergent_block() { + let config = setup_sequencer_config(); + let (mut store, mut state) = SequencerCore::::open_or_create_store(&config); + + // Anchor on the local genesis at some slot. + let genesis_id = store.genesis_id(); + let genesis = store.get_block_at_id(genesis_id).unwrap().unwrap(); + let anchor_slot = 100_u64; + store + .set_zone_anchor(&ZoneAnchorRecord { + slot: anchor_slot, + block_id: genesis_id, + hash: genesis.header.hash, + }) + .unwrap(); + + // The channel serves a different block at the anchor id/slot. + let mut tampered = genesis.clone(); + tampered.header.hash = HashType([9_u8; 32]); + let messages = vec![block_to_channel_message(&tampered, anchor_slot)]; + let mock = MockBlockPublisher::with_canned_channel( + config.bedrock_config.channel_id, + Some(Slot::from(anchor_slot)), + messages, + ); + + let result = SequencerCore::::verify_and_reconstruct( + &mock, &mut store, &mut state, true, + ) + .await; + assert!(result.is_err(), "divergent channel must abort startup"); +} + +#[tokio::test] +async fn fails_when_channel_is_missing() { + let config = setup_sequencer_config(); + let (mut store, mut state) = SequencerCore::::open_or_create_store(&config); + let genesis_id = store.genesis_id(); + let genesis = store.get_block_at_id(genesis_id).unwrap().unwrap(); + store + .set_zone_anchor(&ZoneAnchorRecord { + slot: 100, + block_id: genesis_id, + hash: genesis.header.hash, + }) + .unwrap(); + + // Anchor present, but the channel does not exist on the connected chain. + let mock = + MockBlockPublisher::with_canned_channel(config.bedrock_config.channel_id, None, vec![]); + let result = SequencerCore::::verify_and_reconstruct( + &mock, &mut store, &mut state, true, + ) + .await; + assert!(result.is_err(), "missing channel must abort startup"); +} + +/// A sequencer config whose genesis funds the bridge account, so replayed bridge +/// deposit transactions have a source balance to mint from. +fn bridge_funded_config() -> SequencerConfig { + let mut config = setup_sequencer_config(); + config.genesis = vec![GenesisAction::SupplyBridgeAccount { balance: 1_000_000 }]; + config +} + +/// Builds an unfulfilled pending deposit event for `recipient`, matching the +/// encoding `build_bridge_deposit_tx_from_event` expects. +fn deposit_event_record( + op_id: [u8; 32], + amount: u64, + recipient: lee::AccountId, +) -> PendingDepositEventRecord { + PendingDepositEventRecord { + deposit_op_id: HashType(op_id), + source_tx_hash: HashType([0_u8; 32]), + amount, + metadata: borsh::to_vec(&DepositMetadataForEncoding { + recipient_id: recipient, + }) + .unwrap(), + submitted_in_block_id: None, + } +} + +/// Builds a signed public bridge `Withdraw` transaction (the normal user path). +fn build_public_withdraw_tx( + sender: lee::AccountId, + nonce: u128, + amount: u64, + bedrock_account_pk: [u8; 32], + signing_key: &lee::PrivateKey, +) -> LeeTransaction { + let message = lee::public_transaction::Message::try_new( + programs::bridge().id(), + vec![sender, system_accounts::bridge_account_id()], + vec![nonce.into()], + bridge_core::Instruction::Withdraw { + amount, + bedrock_account_pk, + }, + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + LeeTransaction::Public(lee::PublicTransaction::new(message, witness_set)) +} + +/// The channel is ahead of the local store and its extra blocks carry +/// bridge deposit and withdraw transactions. Reconstruction must replay them, +/// reproducing both the minted/burned balances and the withdraw reconciliation +/// bookkeeping the production path records. +#[tokio::test] +async fn reconstructs_channel_with_deposit_and_withdraw() { + let recipient = initial_public_user_accounts()[0].account_id; + let deposit_amount = 500_u64; + let withdraw_amount = 100_u64; + let bedrock_account_pk = [0x22_u8; 32]; + let deposit_op_id = [0x0d_u8; 32]; + + // Sequencer A: a deposit block (sequencer-origin) then a withdraw block (user). + let config_a = bridge_funded_config(); + let (mut seq_a, mempool_a) = + SequencerCoreWithMockClients::start_from_config(config_a.clone()).await; + + let deposit_record = deposit_event_record(deposit_op_id, deposit_amount, recipient); + let deposit_tx = + crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx"); + mempool_a + .push((TransactionOrigin::Sequencer, deposit_tx)) + .await + .unwrap(); + seq_a.produce_new_block().await.unwrap(); + + let withdraw_tx = build_public_withdraw_tx( + recipient, + 0, + withdraw_amount, + bedrock_account_pk, + &create_signing_key_for_account1(), + ); + mempool_a + .push((TransactionOrigin::User, withdraw_tx.clone())) + .await + .unwrap(); + seq_a.produce_new_block().await.unwrap(); + + let tip_a = seq_a.block_store().latest_block_meta().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; + + // Sequencer B reconstructs A's chain (bridge blocks included) from empty. + let config_b = bridge_funded_config(); + let (mut store_b, mut state_b) = + SequencerCore::::open_or_create_store(&config_b); + let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages); + SequencerCore::::verify_and_reconstruct( + &mock_b, + &mut store_b, + &mut state_b, + true, + ) + .await + .expect("reconstruct"); + + let tip_b = store_b.latest_block_meta().unwrap(); + assert_eq!(tip_b.id, tip_a.id); + assert_eq!(tip_b.hash, tip_a.hash); + + // The deposit minted into the vault and the withdraw burned from the sender — + // reconstructed balances agree with sequencer A for every affected account. + let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient); + let bridge_id = system_accounts::bridge_account_id(); + for account in [vault_id, bridge_id, recipient] { + assert_eq!( + state_b.get_account_by_id(account).balance, + seq_a.state().get_account_by_id(account).balance, + "reconstructed balance mismatch for {account:?}", + ); + } + assert_eq!( + state_b.get_account_by_id(vault_id).balance, + u128::from(deposit_amount), + "deposit must mint into the recipient vault" + ); + + // Reconstruction recorded the withdraw so the later L1 withdraw event reconciles. + let withdraw_arg = crate::extract_bridge_withdraw_data(&withdraw_tx).expect("withdraw data"); + let key = crate::withdraw_event_reconciliation_key(&withdraw_arg.outputs).expect("recon key"); + assert!( + store_b.dbio().consume_unseen_withdraw_count(key).unwrap(), + "reconstruction must record an unseen withdraw for bridge reconciliation" + ); +} + +/// A deposit whose L1 event was observed (an unfulfilled pending record +/// exists) and whose L2 mint is already contained in a finalized channel block. +/// Reconstruction must reconcile the pending record against that block — marking +/// it submitted so the startup replay does not re-inject it — and apply the mint +/// exactly once. +#[tokio::test] +async fn reconstruction_reconciles_already_finished_deposit() { + let recipient = initial_public_user_accounts()[0].account_id; + let deposit_amount = 400_u64; + let deposit_op_id = [0x1a_u8; 32]; + + // Sequencer A: a single block that fully processes the bridge deposit. + let config_a = bridge_funded_config(); + let (mut seq_a, mempool_a) = + SequencerCoreWithMockClients::start_from_config(config_a.clone()).await; + let deposit_record = deposit_event_record(deposit_op_id, deposit_amount, recipient); + let deposit_tx = + crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx"); + mempool_a + .push((TransactionOrigin::Sequencer, deposit_tx)) + .await + .unwrap(); + seq_a.produce_new_block().await.unwrap(); + let deposit_block_id = seq_a.block_store().latest_block_meta().unwrap().id; + + 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; + + // Sequencer B: fresh store, but with the *unfulfilled* pending deposit event + // pre-seeded, as the cold-start backfill would when it re-observes this + // already-finalized deposit. + let config_b = bridge_funded_config(); + let (mut store_b, mut state_b) = + SequencerCore::::open_or_create_store(&config_b); + assert!( + store_b + .dbio() + .add_pending_deposit_event(deposit_record.clone()) + .unwrap() + ); + + let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages); + SequencerCore::::verify_and_reconstruct( + &mock_b, + &mut store_b, + &mut state_b, + true, + ) + .await + .expect("reconstruct"); + + // The mint was applied exactly once. + let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient); + assert_eq!( + state_b.get_account_by_id(vault_id).balance, + u128::from(deposit_amount), + "already-finished deposit must be applied exactly once" + ); + + // The pending event is now marked submitted in the reconstructed block, so the + // startup replay would not re-queue it — no double mint on restart. + let record = store_b + .get_unfulfilled_deposit_events() + .unwrap() + .into_iter() + .find(|event| event.deposit_op_id == HashType(deposit_op_id)) + .expect("pending deposit event should still be recorded"); + assert_eq!( + record.submitted_in_block_id, + Some(deposit_block_id), + "reconstruction must reconcile the already-finished deposit against its channel block" + ); +} + +#[tokio::test] +async fn committed_local_against_missing_channel_fails_without_anchor() { + // A sequencer that has committed blocks — a non-genesis tip plus a persisted + // checkpoint — but only ever produced (so it never recorded a per-block + // anchor). Restarting it against a wiped/missing channel must still fail, + // driven by the committed-blocks invariant rather than an anchor probe. + let config = setup_sequencer_config(); + { + let (mut seq, _handle) = + 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); + } // 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::::open_or_create_store(&config); + assert!(store.get_zone_anchor().unwrap().is_none()); + assert!(store.latest_block_meta().unwrap().id > 1); + + // The channel is gone: no tip, no messages. + let mock = + MockBlockPublisher::with_canned_channel(config.bedrock_config.channel_id, None, vec![]); + let result = SequencerCore::::verify_and_reconstruct( + &mock, &mut store, &mut state, false, + ) + .await; + assert!( + result.is_err(), + "committed blocks against a missing channel must abort startup" + ); +} diff --git a/lez/storage/src/sequencer/mod.rs b/lez/storage/src/sequencer/mod.rs index 1a3a1886..f8557771 100644 --- a/lez/storage/src/sequencer/mod.rs +++ b/lez/storage/src/sequencer/mod.rs @@ -22,7 +22,7 @@ use crate::{ LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell, LatestBlockMetaCellOwned, LatestBlockMetaCellRef, PendingDepositEventRecord, PendingDepositEventsCellOwned, PendingDepositEventsCellRef, UnseenWithdrawCountCell, WithdrawalReconciliationKey, - ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, + ZoneAnchorRecord, ZoneCursorCell, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, }, }; @@ -34,6 +34,10 @@ pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id"; pub const DB_META_LATEST_BLOCK_META_KEY: &str = "latest_block_meta"; /// Key base for storing the zone-sdk sequencer checkpoint (opaque bytes). pub const DB_META_ZONE_SDK_CHECKPOINT_KEY: &str = "zone_sdk_checkpoint"; +/// Key base for storing the last channel block read back and verified from +/// Bedrock (its L1 slot + `id`/`hash`) — the anchor for the startup +/// consistency check and the resume point for reconstruction. +pub const DB_META_ZONE_CURSOR_KEY: &str = "zone_cursor"; /// Key base for storing queued deposit events that were not yet /// fulfilled on L2. pub const DB_META_PENDING_DEPOSIT_EVENTS_KEY: &str = "pending_deposit_events"; @@ -359,6 +363,14 @@ impl RocksDBIO { self.del::(()) } + pub fn get_zone_cursor(&self) -> DbResult> { + Ok(self.get_opt::(())?.map(|cell| cell.0)) + } + + pub fn put_zone_cursor(&self, anchor: &ZoneAnchorRecord) -> DbResult<()> { + self.put(&ZoneCursorCell(*anchor), ()) + } + pub fn get_pending_deposit_events(&self) -> DbResult> { Ok(self .get_opt::(())? diff --git a/lez/storage/src/sequencer/sequencer_cells.rs b/lez/storage/src/sequencer/sequencer_cells.rs index 7672e271..302a0b97 100644 --- a/lez/storage/src/sequencer/sequencer_cells.rs +++ b/lez/storage/src/sequencer/sequencer_cells.rs @@ -9,7 +9,8 @@ use crate::{ sequencer::{ CF_LEE_STATE_NAME, DB_LEE_STATE_KEY, DB_META_LAST_FINALIZED_BLOCK_ID, DB_META_LATEST_BLOCK_META_KEY, DB_META_PENDING_DEPOSIT_EVENTS_KEY, - DB_META_UNSEEN_WITHDRAW_COUNT_KEY, DB_META_ZONE_SDK_CHECKPOINT_KEY, + DB_META_UNSEEN_WITHDRAW_COUNT_KEY, DB_META_ZONE_CURSOR_KEY, + DB_META_ZONE_SDK_CHECKPOINT_KEY, }, }; @@ -132,6 +133,39 @@ impl SimpleWritableCell for ZoneSdkCheckpointCellRef<'_> { } } +/// The last channel block read back and verified from Bedrock. +/// +/// Holds its L1 inscription `slot` plus the block's `id`/`hash`, and serves as +/// both the anchor for the startup consistency check and the resume point for +/// reconstruction. `slot` is stored as a raw `u64` because the zone-sdk `Slot` +/// does not derive borsh; the caller converts to/from `Slot`. +#[derive(Debug, Clone, Copy, BorshSerialize, BorshDeserialize)] +pub struct ZoneAnchorRecord { + pub slot: u64, + pub block_id: u64, + pub hash: HashType, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct ZoneCursorCell(pub ZoneAnchorRecord); + +impl SimpleStorableCell for ZoneCursorCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_ZONE_CURSOR_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for ZoneCursorCell {} + +impl SimpleWritableCell for ZoneCursorCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize zone cursor".to_owned())) + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PendingDepositEventRecord { pub deposit_op_id: HashType, diff --git a/test_fixtures/src/bin/regenerate_test_fixture.rs b/test_fixtures/src/bin/regenerate_test_fixture.rs index 2aa24c8b..038d4b0c 100644 --- a/test_fixtures/src/bin/regenerate_test_fixture.rs +++ b/test_fixtures/src/bin/regenerate_test_fixture.rs @@ -11,9 +11,9 @@ use sequencer_core::block_store::SequencerStore; use test_fixtures::{ config, setup::{ - prebuilt_sequencer_db_dump_path, setup_bedrock_node, + SequencerSetup, prebuilt_sequencer_db_dump_path, setup_bedrock_node, setup_private_accounts_with_initial_supply, setup_public_accounts_with_initial_supply, - setup_sequencer, setup_wallet, + setup_wallet, }, }; use wallet::config::WalletConfigOverrides; @@ -49,15 +49,12 @@ async fn generate_prebuilt_fixture(dest: &Path) -> Result<()> { let genesis = config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts); - let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( - config::SequencerPartialConfig::default(), - bedrock_addr, - genesis, - config::bedrock_channel_id(), - None, - ) - .await - .context("Failed to setup Sequencer for fixture generation")?; + let (sequencer_handle, temp_sequencer_dir) = + SequencerSetup::new(config::SequencerPartialConfig::default(), bedrock_addr) + .with_genesis(genesis) + .setup() + .await + .context("Failed to setup Sequencer for fixture generation")?; let (mut wallet, _temp_wallet_dir, _wallet_password) = setup_wallet( sequencer_handle.addr(), diff --git a/test_fixtures/src/config.rs b/test_fixtures/src/config.rs index 449308eb..f69c9c67 100644 --- a/test_fixtures/src/config.rs +++ b/test_fixtures/src/config.rs @@ -80,7 +80,26 @@ pub fn sequencer_config( home: PathBuf, bedrock_addr: SocketAddr, genesis_transactions: Vec, + cross_zone: Option, +) -> Result { + sequencer_config_with_channel( + partial, + home, + bedrock_addr, + bedrock_channel_id(), + genesis_transactions, + cross_zone, + ) +} + +/// Like [`sequencer_config`] but with an explicit Bedrock `channel_id`, so tests +/// can point a sequencer at a fresh/empty channel (e.g. to model a wiped Bedrock). +pub fn sequencer_config_with_channel( + partial: SequencerPartialConfig, + home: PathBuf, + bedrock_addr: SocketAddr, channel_id: ChannelId, + genesis_transactions: Vec, cross_zone: Option, ) -> Result { let SequencerPartialConfig { diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index 8aa201a7..75eaf941 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -24,8 +24,8 @@ use wallet::{ use crate::{ indexer_client::IndexerClient, setup::{ - setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, - setup_public_accounts_with_initial_supply, setup_sequencer, setup_sequencer_from_prebuilt, + SequencerSetup, setup_bedrock_node, setup_indexer, + setup_private_accounts_with_initial_supply, setup_public_accounts_with_initial_supply, setup_wallet, sync_wallet_from_prebuilt, }, }; @@ -359,11 +359,8 @@ impl TestContextBuilder { let partial_config = sequencer_partial_config.unwrap_or_default(); - let (sequencer_handle, temp_sequencer_dir) = if use_prebuilt { - setup_sequencer_from_prebuilt(partial_config, bedrock_addr) - .await - .context("Failed to setup Sequencer from prebuilt database")? - } else { + let mut sequencer_setup = SequencerSetup::new(partial_config, bedrock_addr); + if !use_prebuilt { // Wallet genesis must always be present so that // setup_public/private_accounts_with_initial_supply can claim from the vault PDAs. // When a test supplies custom genesis, merge rather than replace. @@ -376,16 +373,12 @@ impl TestContextBuilder { } None => wallet_genesis, }; - setup_sequencer( - partial_config, - bedrock_addr, - genesis, - config::bedrock_channel_id(), - None, - ) + sequencer_setup = sequencer_setup.with_genesis(genesis); + } + let (sequencer_handle, temp_sequencer_dir) = sequencer_setup + .setup() .await - .context("Failed to setup Sequencer")? - }; + .context("Failed to setup Sequencer")?; let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet( sequencer_handle.addr(), diff --git a/test_fixtures/src/setup.rs b/test_fixtures/src/setup.rs index 08ce6e88..ce816919 100644 --- a/test_fixtures/src/setup.rs +++ b/test_fixtures/src/setup.rs @@ -1,4 +1,7 @@ -use std::{net::SocketAddr, path::PathBuf}; +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, +}; use anyhow::{Context as _, Result, bail}; use indexer_service::{ChannelId, IndexerHandle}; @@ -20,12 +23,106 @@ use crate::{ private_mention, public_mention, }; -/// How to initialize the sequencer's database. -pub enum SequencerInit<'dump> { - /// Apply these genesis actions from scratch. - Genesis(Vec), - /// Restore an existing chain from a prebuilt dump (genesis actions are then irrelevant). - Prebuilt(&'dump DbDump), +pub struct SequencerSetup { + partial: config::SequencerPartialConfig, + bedrock_addr: SocketAddr, + channel_id: ChannelId, + genesis_transactions: Option>, + cross_zone: Option, +} + +impl SequencerSetup { + #[must_use] + pub fn new(partial: config::SequencerPartialConfig, bedrock_addr: SocketAddr) -> Self { + Self { + partial, + bedrock_addr, + channel_id: config::bedrock_channel_id(), + genesis_transactions: None, + cross_zone: None, + } + } + + /// Set the Bedrock channel ID to use for the sequencer. + /// If not set, the default channel ID from the Bedrock config will be used. + #[must_use] + pub const fn with_channel_id(mut self, channel_id: ChannelId) -> Self { + self.channel_id = channel_id; + self + } + + /// Set the cross-zone configuration to use for the sequencer. + /// If not set, the sequencer will be configured to run in single-zone mode. + #[must_use] + pub fn with_cross_zone(mut self, cross_zone: sequencer_core::config::CrossZoneConfig) -> Self { + self.cross_zone = Some(cross_zone); + self + } + + /// Set the genesis transactions to apply when initializing the sequencer. + /// If not set, the sequencer will be initialized from a prebuilt database dump. + #[must_use] + pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { + self.genesis_transactions = Some(genesis_transactions); + self + } + + /// Set up the sequencer in a fresh temporary home directory, returning the + /// owning [`TempDir`] alongside the handle. + pub async fn setup(self) -> Result<(SequencerHandle, TempDir)> { + let temp_sequencer_dir = + tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; + + let sequencer_handle = self.setup_at(temp_sequencer_dir.path()).await?; + + Ok((sequencer_handle, temp_sequencer_dir)) + } + + /// Set up the sequencer in an explicit `home` directory owned by the caller. + /// + /// Useful for tests that restart the sequencer against the same on-disk store. + pub async fn setup_at(self, home: &Path) -> Result { + let Self { + partial, + bedrock_addr, + channel_id, + genesis_transactions, + cross_zone, + } = self; + + debug!("Using sequencer home at {}", home.display()); + + let genesis_transactions = if let Some(genesis) = genesis_transactions { + genesis + } else { + let dump = load_prebuilt_dump()?; + // `SequencerCore::open_or_create_store` looks for `/rocksdb`. + let dst = home.join("rocksdb"); + let _store = SequencerStore::restore_db_from_dump( + &dst, + &dump, + lee::PrivateKey::try_new(config::SEQUENCER_SIGNING_KEY)?, + ) + .context("Failed to restore prebuilt sequencer database from dump")?; + // TODO: Technically not correct, we should reconstruct the genesis transactions + // from the dump, but this crutch doesn't affect anything for now + Vec::new() + }; + + let config = config::sequencer_config_with_channel( + partial, + home.to_owned(), + bedrock_addr, + channel_id, + genesis_transactions, + cross_zone, + ) + .context("Failed to create Sequencer config")?; + + sequencer_service::run(config, 0) + .await + .context("Failed to run Sequencer Service") + } } /// Committed single-file dump of the prebuilt sequencer database (`just regenerate-test-fixture`). @@ -139,81 +236,6 @@ pub async fn setup_indexer( .map(|handle| (handle, temp_indexer_dir)) } -pub async fn setup_sequencer( - partial: config::SequencerPartialConfig, - bedrock_addr: SocketAddr, - genesis_transactions: Vec, - channel_id: ChannelId, - cross_zone: Option, -) -> Result<(SequencerHandle, TempDir)> { - setup_sequencer_inner( - partial, - bedrock_addr, - SequencerInit::Genesis(genesis_transactions), - channel_id, - cross_zone, - ) - .await -} - -/// Like [`setup_sequencer`], but rebuilds the sequencer database from the committed prebuilt dump -/// so it starts from an existing chain instead of applying genesis from scratch. -pub async fn setup_sequencer_from_prebuilt( - partial: config::SequencerPartialConfig, - bedrock_addr: SocketAddr, -) -> Result<(SequencerHandle, TempDir)> { - let dump = load_prebuilt_dump()?; - setup_sequencer_inner( - partial, - bedrock_addr, - SequencerInit::Prebuilt(&dump), - config::bedrock_channel_id(), - None, - ) - .await -} - -async fn setup_sequencer_inner( - partial: config::SequencerPartialConfig, - bedrock_addr: SocketAddr, - init: SequencerInit<'_>, - channel_id: ChannelId, - cross_zone: Option, -) -> Result<(SequencerHandle, TempDir)> { - let temp_sequencer_dir = - tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; - - debug!( - "Using temp sequencer home at {}", - temp_sequencer_dir.path().display() - ); - - let genesis_transactions = match init { - SequencerInit::Genesis(genesis) => genesis, - SequencerInit::Prebuilt(dump) => { - // `SequencerCore::open_or_create_store` looks for `/rocksdb`. - let dst = temp_sequencer_dir.path().join("rocksdb"); - SequencerStore::restore_db_from_dump(&dst, dump) - .context("Failed to restore prebuilt sequencer database from dump")?; - Vec::new() - } - }; - - let config = config::sequencer_config( - partial, - temp_sequencer_dir.path().to_owned(), - bedrock_addr, - genesis_transactions, - channel_id, - cross_zone, - ) - .context("Failed to create Sequencer config")?; - - let sequencer_handle = sequencer_service::run(config, 0).await?; - - Ok((sequencer_handle, temp_sequencer_dir)) -} - pub async fn setup_wallet( sequencer_addr: SocketAddr, initial_public_accounts: &[(PrivateKey, u128)], diff --git a/tools/cross_zone_chat/src/main.rs b/tools/cross_zone_chat/src/main.rs index 5a066e9e..1606c201 100644 --- a/tools/cross_zone_chat/src/main.rs +++ b/tools/cross_zone_chat/src/main.rs @@ -66,7 +66,7 @@ use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuil use serde::{Deserialize, Serialize}; use test_fixtures::{ config::{self, SequencerPartialConfig, UrlProtocol, bedrock_channel_id, bedrock_channel_id_b}, - setup::{setup_bedrock_node, setup_sequencer}, + setup::{SequencerSetup, setup_bedrock_node}, }; const HTTP_PORT: u16 = 8088; @@ -289,14 +289,20 @@ async fn main() -> Result<()> { let cross_zone_b = watch_peer(zone_a, receiver_id); let partial = SequencerPartialConfig::default(); - let (seq_a, _home_a) = - setup_sequencer(partial, bedrock_addr, vec![], channel_a, Some(cross_zone_a)) - .await - .context("Failed to set up zone A sequencer")?; - let (seq_b, _home_b) = - setup_sequencer(partial, bedrock_addr, vec![], channel_b, Some(cross_zone_b)) - .await - .context("Failed to set up zone B sequencer")?; + let (seq_a, _home_a) = SequencerSetup::new(partial, bedrock_addr) + .with_genesis(vec![]) + .with_channel_id(channel_a) + .with_cross_zone(cross_zone_a) + .setup() + .await + .context("Failed to set up zone A sequencer")?; + let (seq_b, _home_b) = SequencerSetup::new(partial, bedrock_addr) + .with_genesis(vec![]) + .with_channel_id(channel_b) + .with_cross_zone(cross_zone_b) + .setup() + .await + .context("Failed to set up zone B sequencer")?; let state = Arc::new(AppState { zone_a: ZoneRuntime { From da2afba75a066a30ffc0184ebaf4b28b698b6588 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 14 Jul 2026 01:16:16 +0300 Subject: [PATCH 2/8] chore(lock): bump spin to 0.9.9 to satisfy cargo deny --- artifacts/lez/programs/amm.bin | Bin 533820 -> 533820 bytes .../lez/programs/associated_token_account.bin | Bin 452136 -> 452136 bytes .../lez/programs/authenticated_transfer.bin | Bin 406100 -> 406100 bytes artifacts/lez/programs/bridge.bin | Bin 429892 -> 429892 bytes artifacts/lez/programs/bridge_lock.bin | Bin 434844 -> 434820 bytes artifacts/lez/programs/clock.bin | Bin 404448 -> 404448 bytes artifacts/lez/programs/cross_zone_inbox.bin | Bin 497440 -> 497472 bytes artifacts/lez/programs/cross_zone_outbox.bin | Bin 436012 -> 436012 bytes artifacts/lez/programs/faucet.bin | Bin 422448 -> 422448 bytes artifacts/lez/programs/pinata.bin | Bin 394888 -> 394920 bytes artifacts/lez/programs/pinata_token.bin | Bin 400820 -> 400820 bytes artifacts/lez/programs/ping_receiver.bin | Bin 407224 -> 407224 bytes artifacts/lez/programs/ping_sender.bin | Bin 418400 -> 418372 bytes artifacts/lez/programs/token.bin | Bin 484064 -> 484108 bytes artifacts/lez/programs/vault.bin | Bin 414452 -> 414440 bytes artifacts/lez/programs/wrapped_token.bin | Bin 417912 -> 417940 bytes 16 files changed, 0 insertions(+), 0 deletions(-) diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index ce19ce6f0f5791cb1dae2709f4be4f52aec5f504..1f89c3f9ac9db224199e311a4f81d9bcab123bfd 100644 GIT binary patch delta 1644 zcmah}&ubGw6n2y5?bd=sTWU>K8}-nD5O-!bJG4)*b zcQ}LZKEXcz>I2--L`>T{D%v554)bxV34Pf31$%LBIdvS<-THpKI|qXz zvPf!jUaS;>rtKHXSk#YzwW++pc|)w^i``f&%5mMKBev z-mgpE8hnHs_+}Y;(QCjZ;cQX-iw!XW|J{Ht?Z9r6@Wm`RcY^b0+rZ1Le{>M}kxbY0j-#$hOnxq)Rfj56Dro^fW zHHqR=V$0inR`tlJfGXx|7vRFG#N!A}uPM;18$+&K>ep**w0->(|d; zu0wz3GzpmP2X@J)Uf@wTu!-aQ&Q<1=*huM0i5AFCiXL&9SF|0gb?li-sZSWD6Z#|m cJ4`@Z+y_vXzttht7DHPWI~ zl#Y6$%u9Gz&jr+WT-#;L4m_vxnCsxdCJf`hIv669=U;=NW01K5jy0==K#YQv`3kFl&VfR8^zMW{d8r=RH;IgIx^dXjmZ?RG^xo$GSsrj^#Yl{U;n{{fGPt1tin diff --git a/artifacts/lez/programs/associated_token_account.bin b/artifacts/lez/programs/associated_token_account.bin index f5b7b02af15272bcbf4725a48fa844e9dc5f88d7..7db7692da03d2cf38e2e922ff3455c175a022e75 100644 GIT binary patch delta 966 zcmaizK}!Nr6ou2~hENbxV53l*2tyF(&2!#7D8glnYALvhp|MT2iD=`Zh=Ep-SoD%q zEJ6&52#U7~a?_%O>K~N9pg$nwh&!*geTVbi!@2iW>%nS0xbJuzuQTW5y*&`L!ui_Wk224o*6;#jxAJ>q<*yUF=sfbq~F0$`1idZacOItY<{jvx^P0tUZs|5B zrezYt(lyFC-C)#aDSO=}5v>!_)C^|EbQ##+CKYfSvSKu)HgVM_U*Lpo_OPV1Dy RV(%VC4x2TQ&Dv0S<`1OS{Y(G= delta 965 zcmaiyPfG$(6vfl#hENdw0UL$d1Q~+N%$qke4~lTvqFM@WLTy&rCZdgtA_iJTV$n-d zu?R6JA}HP}$W4n9s&7z!g1&%|BksJ~_8!jf9?rehdazm#?mHgG>kK;5BQ$d3RHdpx zn5G$wnu>zUyE5I-s%&mwQir|~*E=cE#Rmkwz4fy$T3E~*|sRU&- zNgI582|mQt?lGwM>`~{5U&+t24VV=EE2y9gKCU5yvCFS$QXZ{9lxN>zZmj!JTAGa( z;N}pY6?l`dRqpks`W-JM>-}ZcR}QGXU*q8x4rJtJ1MBfsgYfS>%!ti9AfpSWs++b# z^<+{@F~v&Q7G-A2unF0q7S%{Z?Sy0oA(pNyj1ki^R7#l2e_kLb($6q0{m;=lX)#Xg Tlfr!O9!3tEH4x3(PX7Bb5QYVI2m}g62nZ*^SU@G*?%wTPq9pzSDXg?WE+UBp3mRi%LP0{TOf1~^ z4=hbISsOY#l}1WSS}TGkekAf?=PuN<^StlOyxZ-?y1iJV>1w*0o;%d&FH%Z$Vp^CO zl-RaOG0XAiK1zw(K6-J<%HnE(;?fGQ2O!Pg-jQF{$2;FBE>=I#t9wbJI=8pG#W1h0 zpcv16q7x}%D3D2zwOZ}?Xib+uMh-FwQ~f|}exYknvC~u8c^5o}{)=+w5W+m`hK>@e z88bYmHCl0sl1l6b~YBWFABV!G`)`8lAGRv9?C(EO)0~kYD|08%4#97arsW zT0S_FO?93@d0z5ER5lQQO2ST98UW`WmEu#(E+n85l4O2l^D+QMsoXGCSydsOn7JD* z2tH*yF9sEx8)w;2&m!7EsCw3QhY{N$WgAi;0BODnvjOB3@Yj1sksqb^rhX delta 1118 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 C{tzet delta 10777 zcmai)XUu>GV6nO)gQE5oS-#Sc1YB+rsq7Dqdk`s^>&Va!yV@Aiy|=_gAywuT2z$O!_^+X z_a~A09{vj)T^E$rs(+iGMY7;{#e?DEy8?0)j*8M`6I{!9N8b4t#@`;0juE*j`)J6g zp;`@SFPbEwuo-L{^bp)0ZmM`W9E6)I?yPBYlJUuEyzbYe;?95s6rXOQ$pKhRC`C;* ziO&m2=dj$=dAZM{82-;V0oa9WhM*+b3K-uuMw4!Y z_#wrE;Rg6F#kX2Hd3xXIxGLIL6u6Q3z^W|JZ_v$quq^jIcm6ktSfPgtvG36MfSlAL z^H(x|%(kFpD*YW=Xi@+>_HU2VWd2r{Uu#9cJbc9FvVbHoo)j_Vj@FvgyA84lJ`R_t zh;?tHN&cjOR7D2JD!2@VEtLT$U=zMtahJB5RD0y(;cajeC0`5wv9Z4yCFf|^O+%dO zkamqGhdm0Og1>>C9DEmU@bv!)_7ed|o_#G5g|AZjX2M0Vvk5+g&%n91Jk))VzE4|g zX@m6<+JJww)1-82K;Dmv1bbP6CgWHPNA;H-G|7A>AoVIXEdu;wN>j?9GDd-x1|8g?8h>B{!Pz$z8lPTkl}3j>m^ zcqLrz3Dj@8Yw|wq6g;B`D+xz3;{GTZO+zjgQ69qeU=>^pJ1KA|Nt3u8s^CyBu20q^ z3wAs@3zwttMm0VqG;xG(rkG?}!^>`6EAGiKc(bx|16;q;Rr8%?6p?8;liHJkuM!Wn zAS=l%pU1qfY=5f)azJJH6Djz!jLcGc%dEX4^+me*IeLcwF(_S?S@9^$haIzr!KXcZ zHHk3%wSXK_^J--5@#O)DQF_x!z-W&hYjC?Bxhg}l6+24cka8uzEsH>{34|TCx8MRe!NZ@!-C)PT#9Oh~!;izcaF)`y1>ObMDh~G1 zWEz|wVN*(JsCY9VYgC87!izs}x5l-7k$ZR;9C#}rWvc&bcrn~jt!!~NOYxz*VsF4j zu(K6U!gaL)IUZgyqTM@3lg^$F)9)c-OgNYc?33E{&u_nd_C;ZZ%U86ODA{{_D*mC zPT1s5(Cw~ECwLI{PVfPI{_TJqRR;YR9Q&?&Dx}{{Ewchr5SE*ojHjVsvpe9o@PT*O z%u2yu;moZ8$qX;4Caqa9p?^eyCU?QfTio#%3ebnaPEnaZfKB|7n=%g$#31-C^lR3y zC+oW3+(nigsCUz-c|T3kK6b~v&X_$!v7@r7!~Gmx@40iXfiwTWoXW<7a3SNJLers; zeeKaV0+v5F>NjO24Y{5UU%^K`eB&S#Y-i(vh=k%4CSLIwhH+HbhQ&Wf3H-!88HT}|;aD~CGPuFR2jMjyP8d%5e8SNd(QnFN z8hpFmi&Pcdu*+5WC7k9tGZG%6B4AK96WRv70?(wsUDE7G9E2Bp#&>vFllw4Wn36vX zCn7HjvmPZYLOZAG+s)xbeCQ$cs!H?^`o(_XZp@q!I1gv5z`Ox_SLh@xo)x>Ph{A+l zPKuPm@vsx9weV(_qhyGsP19ROnWJyxkUH+}@K53S9?rp06bDgdYZFo^%7xyU5!#&O4SQB5;*J)<;Xk7m$Ah~J{Zb037 zIKW!jYyHVs)HBYBY0M+kGLN2n;rKo7bPM45H@ha;rbJA<<#oyqEd!ENqy~X$SxJP-Eeg$ z*Ny9*A`XY#aSvF=I{k>>{Er#T*TrbO7(Vtq?xz}ZTIpE}&-uGM?kwy(?4AofXLE5z z?p!y@;kqM9Z&ULaDrhD=fXPq+@wSKM}fXy6WgZL7aLM{PXnM##6oYPC~; zSU0aiP=mn9&3w3auX{sX1RwAWd~*R8zdMb_d(;_t2W;n{y)=(3#L@@doH_+BVSEeZ z_CeZx5uX+E4Vf0{pDK4OW-UD@++4Vp0o~MqU9e|>w5+s(ALx&1W{-J<;yy!4BATQb zuCKq>kd7);W8l-w^uCf0tfWdDb3^bFT;k#V@JS|iic;Gbxm+PvyHMU6k0SNQC+Dn| zAL-Nek#Eqe)}!SFysN;FL-e%c(eWi}{s2Q}slD`jxU=WRe+FK{__p-7F z71p^w>)T^e9-WDGgN?>F)X&6m2i#fBGnjb`hr0Y~=-B7_k+_;!Ebll&vQ$iqD3k+ktk1&c9dJS9Py z(&-gVBk^A)SImG1`MfP=JmJY&cnf5eWP-q57kw?U~_ z_Ljm~-y2e{>}r#f;nRrN(+1~y{`DdhmluWgD_VsO#%GtVx2)+=May9XwH+h#da}7HNSHta; zqDSB23kvMqVYXX4`}z86T3<`c^_fky3Tyh^zID3R-%7d9w@%}m*RlS-Bwc&fio4%; zCQ7>}qz(2h*FLsZ4)%?RGXHi#B>A)WodU&e>8&8fKH@=NfFMUii1w)|VTp&?ReZ62r-=!=oNsLh+;s-IkL2 zMKdz4cTm_tVULf-@L1!ka-m|gIs36sPy!UjRwfF5pdqVMWbfGuA8ElJC=bXqWl#-# z8n#o(mLGzr>Jr$5$7-ZTF zPtzc0jPPd~n>WA-m9EGCfMZq#WWAc8D2}7}0>6x~YwiA8EB55c$mP)HOxUx=ausLo z@kx=Vv@I`zheod5CcpIfGucWy{qv}5z0 zawo`#6Ds-oq9X7%oQFg9;cV;wI+5}5h6Hpo(lEI*SEqlwNq3|xhef~0VPg*%*PSII z2a1$}<8Wa<2B`^7^kB*E3(5*5zcGmk+Z$3C6Vd-9JnN-kV}$jdDyf-2Y*BhDr#2Sa z*9CjRjd1;VazgP#X$-hC@}RWkZ^5O6umHIo!6R_D@7*POB%L)b35H)-cK?szJkKsG z>lN8`W|f-YFEn_!U0Md$vhKm~6Vz6)FOwX&g%B!_uj`GR2<=kii{J#$5{|r?EAuLop6}~tu zF}!AR{G_u3me2VSKt*6|0d;}GQ?2@c2v_-x@J9&Sp}YZ?M&L8J&~L~#d*37PIFMap zGQinZ{~E-gZ`>?S8H|NwwPR4qQ1&rN=nKjEU= zxZ|oNN*YZBD#&`pWWwYI^|E2^5+)7H7ykOj=Azu%B zYk%CMEP&l?-=imTK<}6|7xp?g12jEN(o1e_1vh_D) zno^iM|I*{Q2KLs4I~K6*1{q-kZ4Uzr+4mlWo8UQj8{uD)YxP?Xx{0P2Sq71Ol|1&gne9Dl38qoh$PA5Vd1KYLzB3xI> z(^JWJ^THeY8|sG2nbEIvgIoYJ!1lP_X7pvL{;|t7nFbdt9tt0ahr;3We+3O$6-Hwz zJ_R50Bw79nwk7f^rC=F+5k9JTH=M;1I(NR@m5hHn@=3%tXfho0j3IMW|GeK&H>Ti_ zU4QHj+i1w0>RKH0TPoWOKKiIyeJflwgTqF7{1jXQJ9VQDE~Bs%s0kY2ZclSosF2sa z!Qu07*MUL2EhDF)I{z!yu*N_02v!rcf0HDk5Uo%Kgy1|qC{y@~V;h|K7D*MyW3qXK zYvJ>TTd1ObN5q0I4~F-6c=HA##1-y+Q&||;NRr$WY&@+F^GM6%(H6_=+O7i~>)-(x zpeCFMzyB}>@P7|%gD%1sxj#5-UimgT^BBjsO5#U1vuikhGgXAH+CtqSVr^@chuSut zu`jDi7CJY#Ne8Rl7@u~rbNCNA?y_BS9`ny@^}p936#JULq}63s7E|Iw6CU?3yRx&P>@t7!mBsDTQmz<_P|{@o zHK7e<{-swQA2KKUn_f}eDYOIeWnEpd)SnTGo#cP*a#CyiB)`!zD
V@z^-+Ne=I zNB2(7962U4y?4gwUSm>Ii_r`TsWmEqfyHF!3 diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index e39955c8be1dfecc9949cbda7a4b581274325dd9..c8cf202df3d0dc69ecf18e478188be9a745472bc 100644 GIT binary patch delta 167 zcmaEGUE;xYiG~)&7N!>F7M3lnA6RV+6U~h+6OB_WjVux^jLZ^^4NWX9P14LwQp{6R zl2eTI3{CWMa>{b`EDX)eEliC~3@pt}&CQIAEDg=K>#(tIU?FUTkr_@Srq_RD(Via1 R#p>GL!Ogn8gPScv0RR|F7M3lnA6RY74Nc5b4NOgvl8llq4J;B{b`EX|CKElf-dEG>=AEzAr}jV%qg>#(tIU?FUTkr_@Srq_RD(Via1 R#p>GL!Ogn8gPScv0RY@cE*t;= diff --git a/artifacts/lez/programs/cross_zone_inbox.bin b/artifacts/lez/programs/cross_zone_inbox.bin index 02af9294881f790d9dc25d49868a10e2157deacb..c356ef223fca6c9799c3a7fba82f623be8ff282a 100644 GIT binary patch delta 12433 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 delta 12395 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 diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index a548a31ec278d9c5de98966d58b4f9cac18b3ed6..7a3ab3a639c30e23667ac7a82a86dea6cb636e02 100644 GIT binary patch delta 860 zcmZ2;PHN3LsfHHD7N!>F7M3lnTmiyHW@eVgCML$l7RCmK2ByX)Mn=;Y=CG>E7#i#4 zHpGLIdRI$n3&);8Dh7IIc^o(7o@YEXToZ_Vg#%3bm?qX zh3Q+fSRL>xNyI7(_P7Zik59jp&02{|tA?~8ZfAo$W`I@a$8{{?+kfY<3Ssrsc7=Ra zQAVs9;|p13x2G1c&cLb>9Fo$e_+6wfZAM4|o~Qs@G5v2bD+gBhfkJb-eF+{}8AE&_ zHn)Ve7puV=S=eN@N0zdxv)dRZnj2dt8mCwqStME*nI#$F7M3lnTmixc28Mu;L32u`icAJ>vRK_o8a;I^h?>SmAJHONE_mIHppWJSap6}$0EM{cMhu%R!?nL$Y&L0 z#HumAkX3ejY60sEtQx@~DQ$}1Me5RKgcRV33a}N^{}!`yV09lTG^g8_;E|Ov#1~?7 zOIUlc8oZH(O=f#!DXTiWjk%$Td8&b_Nm7zgvZaAVVycCyrFpVhsF7M3ln#~p>tEX*t|jZIAqEG#U{je(T$^h8fq4M_{VoSd>; zJzUb}c%`Kb4e-iKn&Onc=fql!%~3#clj+l)S;etPZa?76TFoeo=~j#B_p@1Drq`Kc z)i!;%E9*3@x)eKE5+rds(PH|+K9)E<(tA@`m8O@uW499&xKf5@xD22E(493Mix(l0 zVuVv0*jOV1v0{YVT8Ms2eC8YAaF(W|AwF#y(|NsE*>UNe?(fCQhC@F7M3ln#~p=?EsYEfEe$M;Ow7zpOpQ!TOr|G#vT8_L=;h>; zN35~ z9ILkJyIonQVb!JB$&w(6%ZV1#5B9Od;gQ~(%BnQI%pJR(pum+fG{a^1^oQ=O=~%o7 zi4-H8+Q7yd5r`Ed+}1+$TjDd{0Ee?QB@OXu)0ocd#mbIL?{t4JRyG`x8q@o6>(rQj z!HbmxmriLToaxtM`bJq+neCq5tWnH1=7uKbsRpJdNl8Y@z)X;sYGG<=o@|zCY@A|h zVQOToXJ`WPo28ktv4x3=fu*IfxrLdbsj;Qu_WORU8(0V%0Sk33Mzo&}X5D@|m@QQf E0AioybpQYW diff --git a/artifacts/lez/programs/pinata.bin b/artifacts/lez/programs/pinata.bin index d717d9d6a69ce71bc93094fe5171cdd5afa1022c..f246a46464bbf3917cb4d924f0b142ddd429fb69 100644 GIT binary patch delta 9930 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 D*&48? delta 9890 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 UHyinDoQoxCv(YZkxpnFP0l_h&+W-In diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index cddd3eb180bb96ba7ecc8529a0768d63d99ffcd7..7be14213901d0014530f793c8dbf62c8a1220aef 100644 GIT binary patch delta 1366 zcmah}&r4KM71UTP(hLndQWRF#R18F>>_PZzzWEccF`f!_7UI zkF^q%bz}KK>Em=W^kc^$Ys3#b5XY}Sp^ub_HpHQQNGj1b?r97x?ZfkW)vSr#2aq7q zfdicHd$M>22FXwhQgF@AUko;trfP|)V|a+y zXD~)Fht+EK(ot14O*}kT+<6V{zEGe`*YE(xFW@t!dGnHl#!N)6%PNWkD}NQ@-}0;j z%}YgdqS1184Lik5RU}iM)kv%h`yQ|uP+x&OB>VTqu!IOA2$&B#z6?}~M@IYFA zU=@7f1)gvNi`%|$Pf5EVCkqn=k>&OcoyT42IhLKP{%z6~!sjv^0k#acQ;7c?5fr$# U3(s&q!TM)clPp|K-nnY|2U}ik;{X5v delta 1365 zcmah}&r4KM7hchBNBvL4F*~z zt^5{k`UApR3_Zz3e}bDp+C-R|pyMwrEd=$=xcu&OeeQS8cg{WcR+g>GvNcyulpD*} zD)4o!MF!mQq*U6~T02}x?c=Lsc$5?QQfX!?&xr$J_7|iQOBa#_1;L!}dXT1K+Pf62JY1$D~ZOAwJoHv=MFND#pO#7W6loW^L@=h7^eo zY(tdeS)hg+{=v! X3jDGTT{xd&eXp0&EL=)oKW#Y&7dvlV diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index 9463fa1c4cea0eb256c78b3609da8acb95b17147..dd6945bc9b72f15eb3b7e2a8ebf60a73c3e6d101 100644 GIT binary patch delta 1007 zcma))zb`{k6vsV1r=?9BRMM)DrXo7kyYJWS4MT0FhLJ?1(VA*tFtAk8#6pO{8O0(!<&uZ~XRjamFeWxIwqFmPp-nmg(pPBJ!XCtX(;yMbk~G{YFoZG;lQU-=za{Nbe_W`-F~R8 z<>)5_)d9KD_S=T{t4T{5cdzrc;CDoxceGPHx>%bdO%0Jyj!su73?x~ayqq14|cc*FB zOFa(bh7#&pCR)T-iQpuRvjR6o!7K_RTgVzt!DN;Y)*`oxyi{B$8X0E&hLL3s&)TM? h3vP4AFa+Ziis7EzkK*7zIELfN*{sJ<)?@yw!7nUo1djj! delta 1014 zcma)5y)VO26!p`4>nm+YCH)XhLqr$#`}O@k-U~x*CSfE&d~~aU!N5Yo-w?kFaS z$sl>%#3BZRKY&5Zk_uI61Z}PFHRf~fIrpCPUg0HDc!_M~jeK+7cM0+>n+ zKQrM8dTqx(nv6j|b$&q?B`T27`v~n7WK|$Zj}>T9P0MlxPJAkao<=Z4R~byu!?)U6 z4*x*AI-pv#Jx*G`)kO(5sm;JumABk8u(yG*ZR&;V&bFl`$T1(TsT;dN+~`&nLf6kQ zLi>K4R&7g%Ly)23EF_{@*BWPzI^*b9Doou$MEY9s9+Yss!eBr%T)UVi0;nvw_Q;cT zphM3CxUIE}HArzQh~qHoS+mfS33%MN$aEM?h$kqDPDZri31 h%&Y!AhY5>1!gwT)Lm2;uPTJM#>=eT&i(&t5{1@q*2EzaV diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index dfc27e67ea00373bbc4d6815c60366793be03241..8ed2ab28437238d50c4858d3804f67f5f16b58bd 100644 GIT binary patch delta 10912 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 QTA*nIt^fTzwC2A5067RUZ~y=R delta 10972 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 Thn03ZFiq2jTbn))te*XU+MPZL diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index b76eaaddc0e266f391127c91ceb9cb9c3995f6f4..90432ef5fafd63f7440ef9f8de7efdb00fef3abc 100644 GIT binary patch delta 12953 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 ZMgKqJTX>8;UwMiZrN$V1!n11D{{gzS%FO@( delta 12917 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 UaHIZP?-WH@W!zisUHRz$0o(G_4*&oF diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index ae08f4534e78578ab7c9ee441c2c348638e47c6e..af561c838d0968ce5242eebfa5c2d4a356499fbc 100644 GIT binary patch delta 10701 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 delta 10680 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 V?76|-w?DX0)B4*t9SE*}=s)u~+G_v+ diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index dc9e3454e2078fb46e7c66db489e5bbab13e4673..0b6450b61b84c2a86423710ab8291ed9dabaf187 100644 GIT binary patch delta 10760 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 o*u-ehtwF9=wdDg{9sVD3Vpz2% Date: Tue, 14 Jul 2026 14:43:11 +0300 Subject: [PATCH 3/8] fix(justfile): regenerate test fixture on artifacts rebuild --- Justfile | 10 ++++++++-- artifacts/lez/programs/amm.bin | Bin 533820 -> 533820 bytes .../lez/programs/associated_token_account.bin | Bin 452136 -> 452136 bytes .../lez/programs/authenticated_transfer.bin | Bin 406100 -> 406100 bytes artifacts/lez/programs/bridge.bin | Bin 429892 -> 429892 bytes artifacts/lez/programs/bridge_lock.bin | Bin 434820 -> 434844 bytes artifacts/lez/programs/clock.bin | Bin 404448 -> 404448 bytes artifacts/lez/programs/cross_zone_inbox.bin | Bin 497472 -> 497440 bytes artifacts/lez/programs/cross_zone_outbox.bin | Bin 436012 -> 436012 bytes artifacts/lez/programs/faucet.bin | Bin 422448 -> 422448 bytes artifacts/lez/programs/pinata.bin | Bin 394920 -> 394888 bytes artifacts/lez/programs/pinata_token.bin | Bin 400820 -> 400820 bytes artifacts/lez/programs/ping_receiver.bin | Bin 407224 -> 407224 bytes artifacts/lez/programs/ping_sender.bin | Bin 418372 -> 418400 bytes artifacts/lez/programs/token.bin | Bin 484108 -> 484064 bytes artifacts/lez/programs/vault.bin | Bin 414440 -> 414452 bytes artifacts/lez/programs/wrapped_token.bin | Bin 417940 -> 417912 bytes 17 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 277403c7..b838f4fc 100644 --- a/Justfile +++ b/Justfile @@ -11,13 +11,19 @@ ARTIFACTS := "artifacts" # Linux/CI, which is unaffected. DEMO_ENV := if os() == "macos" { "DYLD_FALLBACK_FRAMEWORK_PATH=/Library/Developer/CommandLineTools/Library/Frameworks" } else { "" } -# Build risc0 program artifacts. +# Build risc0 program artifacts and test fixture. build-artifacts: @echo "🔨 Building artifacts" @rm -rf {{ARTIFACTS}} @just build-artifact lee/privacy_preserving_circuit @just build-artifact lez/programs programs + @if [ "${GITHUB_ACTIONS:-}" = "true" ]; then \ + echo "Skipping test fixture regeneration because CI doesn't need it"; \ + else \ + just regenerate-test-fixture; \ + fi + build-artifact methods_path features="": @echo "Building artifacts for {{methods_path}}" @rm -rf target/{{methods_path}}/riscv32im-risc0-zkvm-elf/docker/*.bin @@ -42,7 +48,7 @@ test: # Regenerate the prebuilt sequencer db dump for fast TestContext::new() (needs Docker; commit the dump). regenerate-test-fixture: - @echo "🧪 Regenerating test fixtures" + @echo "🧪 Regenerating test fixture" RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture # Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup). diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index 1f89c3f9ac9db224199e311a4f81d9bcab123bfd..ce19ce6f0f5791cb1dae2709f4be4f52aec5f504 100644 GIT binary patch delta 1650 zcmah}&ubGw6n2y5?bd=uQff_ByXv6J4`@Z+y_vXzttht7DHPWI~ zl#Y6$%u9Gz&jr+WT-#;L4m_vxnCsxdCJf`hIv669=U;=NW01K5jy0==K#YQv`3kFl&VfR8^zMW{d8r=RH;IgIx^dXjmZ?RG^xo$GSsrj^#Yl{U;n{{fGPt1tin delta 1644 zcmah}&ubGw6n2y5?bd=sTWU>K8}-nD5O-!bJG4)*b zcQ}LZKEXcz>I2--L`>T{D%v554)bxV34Pf31$%LBIdvS<-THpKI|qXz zvPf!jUaS;>rtKHXSk#YzwW++pc|)w^i``f&%5mMKBev z-mgpE8hnHs_+}Y;(QCjZ;cQX-iw!XW|J{Ht?Z9r6@Wm`RcY^b0+rZ1Le{>M}kxbY0j-#$hOnxq)Rfj56Dro^fW zHHqR=V$0inR`tlJfGXx|7vRFG#N!A}uPM;18$+&K>ep**w0->(|d; zu0wz3GzpmP2X@J)Uf@wTu!-aQ&Q<1=*huM0i5AFCiXL&9SF|0gb?li-sZSWD6Z#|m ckK;5BQ$d3RHdpx zn5G$wnu>zUyE5I-s%&mwQir|~*E=cE#Rmkwz4fy$T3E~*|sRU&- zNgI582|mQt?lGwM>`~{5U&+t24VV=EE2y9gKCU5yvCFS$QXZ{9lxN>zZmj!JTAGa( z;N}pY6?l`dRqpks`W-JM>-}ZcR}QGXU*q8x4rJtJ1MBfsgYfS>%!ti9AfpSWs++b# z^<+{@F~v&Q7G-A2unF0q7S%{Z?Sy0oA(pNyj1ki^R7#l2e_kLb($6q0{m;=lX)#Xg Tlfr!O9!3tEH4x3(PX7K~N9pg$nwh&!*geTVbi!@2iW>%nS0xbJuzuQTW5y*&`L!ui_Wk224o*6;#jxAJ>q<*yUF=sfbq~F0$`1idZacOItY<{jvx^P0tUZs|5B zrezYt(lyFC-C)#aDSO=}5v>!_)C^|EbQ##+CKYfSvSKu)HgVM_U*Lpo_OPV1Dy RV(%VC4x2TQ&Dv0S<`1OS{Y(G= diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin index 9fe3e93706cf42bfc96d4749196d7365f9112f6f..2b90722dc9472d51892695505eb9512987dd2adb 100644 GIT binary patch delta 1118 zcmaizKTiTd5XA*%2*?#iF`jS|jRjPAdwYKqHSr5bVP}CL(hw|YjExBeiLo;lZu|yT z#zd2~p|c~}NNH(nBS90j5YWy!uAZIue*4~T_eJf#sLfWW6>ddtq1l@;JZ8HdX9jl+ z*Ya%LVB(<%i}I!iPa$e7FZz%oC!&$StayEgIJGApzab@;Kj1k$PqB?bn$G|$)T2m5(OhztpGY1{vR-1ve^^wZ9c)m{%_hukc^0T+cZUXjybD4G=j zoz)ogr~VDL9YH$Sk-q|1ru=0SaLvCi%^Z`~$X7GXAhM$<#;5<+A-=ajmDf?cr~Qv( zctRnI(;TdcQXJa}#?ygbsbhlg`d|{_zS2^FaT+85j_6O+MoV%u}dmeXO gn_1jrqL#!n*`LA;1nU@ouY}M!F3h4V%tmqH2Ym|Bb5QYVI2m}g62nZ*^SU@G*?%wTPq9pzSDXg?WE+UBp3mRi%LP0{TOf1~^ z4=hbISsOY#l}1WSS}TGkekAf?=PuN<^StlOyxZ-?y1iJV>1w*0o;%d&FH%Z$Vp^CO zl-RaOG0XAiK1zw(K6-J<%HnE(;?fGQ2O!Pg-jQF{$2;FBE>=I#t9wbJI=8pG#W1h0 zpcv16q7x}%D3D2zwOZ}?Xib+uMh-FwQ~f|}exYknvC~u8c^5o}{)=+w5W+m`hK>@e z88bYmHCl0sl1l6b~YBWFABV!G`)`8lAGRv9?C(EO)0~kYD|08%4#97arsW zT0S_FO?93@d0z5ER5lQQO2ST98UW`WmEu#(E+n85l4O2l^D+QMsoXGCSydsOn7JD* z2tH*yF9sEx8)w;2&m!7EsCw3QhY{N$WgAi;0BODnvjOB3@Yj1sksqb^rhX diff --git a/artifacts/lez/programs/bridge.bin b/artifacts/lez/programs/bridge.bin index 4b1ff99706235264828d6cfc92304f214622632c..3ac001d725e4414d7105fc0b2c20af2de9be8cf5 100644 GIT binary patch delta 1013 zcmah{Jxc>Y6g0^_G57%@2qqWBLK29C+uPl{ThQX#fD$!=7*sAXR#C*DwZ(yjq=*)4 zW7J<@BW%Dn*!U|_`46mwcnK-@_Uf58Z{E!7byB@f>ei3>aX;Z}AJBFeESq3z+9pC4 zL6j1V4eq&+<=-wOWvKXZp*tr-`1&st`Ckd-s-+$1OUHtBIo{ZXsc`gNfo64L6&j73 z^8yMxMyD)_VPXX%u#WlHUie{{ZcX0MuE1wZ3b&iE$OmUIBU(prAMP?u6&8j)r;R9r zs)vyRiBRCjp5m3#s*px8nn~z|i`OQr zU(hCaLR)FmuW;2rXcd^2%lBs1=iGD8J@<54iB2nV?MM7kKkCQcpy|$H>R`)eHl=x+ zQ;T9mgy%v^e7X=5O_pCYJLi<{6@j`i22MQsbl_2_E=nlh=KtJSVf zb4YiLY4I=yCIO7Tbs|2t^@kC$Uecg1CGh7J#zqeJHYgE`<=B#VzJwWhz60@5T?G9j zS%AU8#|-Ld8Di}ovT}77(oyAxVjGs_V;SBfO1N2vInk@aq-^ZNt=^Uu>GV6nO)gQE5oS-#Sc1YB+rsq7Dqdk`s^>&Va!yV@Aiy|=_gAywuT2z$O!_^+X z_a~A09{vj)T^E$rs(+iGMY7;{#e?DEy8?0)j*8M`6I{!9N8b4t#@`;0juE*j`)J6g zp;`@SFPbEwuo-L{^bp)0ZmM`W9E6)I?yPBYlJUuEyzbYe;?95s6rXOQ$pKhRC`C;* ziO&m2=dj$=dAZM{82-;V0oa9WhM*+b3K-uuMw4!Y z_#wrE;Rg6F#kX2Hd3xXIxGLIL6u6Q3z^W|JZ_v$quq^jIcm6ktSfPgtvG36MfSlAL z^H(x|%(kFpD*YW=Xi@+>_HU2VWd2r{Uu#9cJbc9FvVbHoo)j_Vj@FvgyA84lJ`R_t zh;?tHN&cjOR7D2JD!2@VEtLT$U=zMtahJB5RD0y(;cajeC0`5wv9Z4yCFf|^O+%dO zkamqGhdm0Og1>>C9DEmU@bv!)_7ed|o_#G5g|AZjX2M0Vvk5+g&%n91Jk))VzE4|g zX@m6<+JJww)1-82K;Dmv1bbP6CgWHPNA;H-G|7A>AoVIXEdu;wN>j?9GDd-x1|8g?8h>B{!Pz$z8lPTkl}3j>m^ zcqLrz3Dj@8Yw|wq6g;B`D+xz3;{GTZO+zjgQ69qeU=>^pJ1KA|Nt3u8s^CyBu20q^ z3wAs@3zwttMm0VqG;xG(rkG?}!^>`6EAGiKc(bx|16;q;Rr8%?6p?8;liHJkuM!Wn zAS=l%pU1qfY=5f)azJJH6Djz!jLcGc%dEX4^+me*IeLcwF(_S?S@9^$haIzr!KXcZ zHHk3%wSXK_^J--5@#O)DQF_x!z-W&hYjC?Bxhg}l6+24cka8uzEsH>{34|TCx8MRe!NZ@!-C)PT#9Oh~!;izcaF)`y1>ObMDh~G1 zWEz|wVN*(JsCY9VYgC87!izs}x5l-7k$ZR;9C#}rWvc&bcrn~jt!!~NOYxz*VsF4j zu(K6U!gaL)IUZgyqTM@3lg^$F)9)c-OgNYc?33E{&u_nd_C;ZZ%U86ODA{{_D*mC zPT1s5(Cw~ECwLI{PVfPI{_TJqRR;YR9Q&?&Dx}{{Ewchr5SE*ojHjVsvpe9o@PT*O z%u2yu;moZ8$qX;4Caqa9p?^eyCU?QfTio#%3ebnaPEnaZfKB|7n=%g$#31-C^lR3y zC+oW3+(nigsCUz-c|T3kK6b~v&X_$!v7@r7!~Gmx@40iXfiwTWoXW<7a3SNJLers; zeeKaV0+v5F>NjO24Y{5UU%^K`eB&S#Y-i(vh=k%4CSLIwhH+HbhQ&Wf3H-!88HT}|;aD~CGPuFR2jMjyP8d%5e8SNd(QnFN z8hpFmi&Pcdu*+5WC7k9tGZG%6B4AK96WRv70?(wsUDE7G9E2Bp#&>vFllw4Wn36vX zCn7HjvmPZYLOZAG+s)xbeCQ$cs!H?^`o(_XZp@q!I1gv5z`Ox_SLh@xo)x>Ph{A+l zPKuPm@vsx9weV(_qhyGsP19ROnWJyxkUH+}@K53S9?rp06bDgdYZFo^%7xyU5!#&O4SQB5;*J)<;Xk7m$Ah~J{Zb037 zIKW!jYyHVs)HBYBY0M+kGLN2n;rKo7bPM45H@ha;rbJA<<#oyqEd!ENqy~X$SxJP-Eeg$ z*Ny9*A`XY#aSvF=I{k>>{Er#T*TrbO7(Vtq?xz}ZTIpE}&-uGM?kwy(?4AofXLE5z z?p!y@;kqM9Z&ULaDrhD=fXPq+@wSKM}fXy6WgZL7aLM{PXnM##6oYPC~; zSU0aiP=mn9&3w3auX{sX1RwAWd~*R8zdMb_d(;_t2W;n{y)=(3#L@@doH_+BVSEeZ z_CeZx5uX+E4Vf0{pDK4OW-UD@++4Vp0o~MqU9e|>w5+s(ALx&1W{-J<;yy!4BATQb zuCKq>kd7);W8l-w^uCf0tfWdDb3^bFT;k#V@JS|iic;Gbxm+PvyHMU6k0SNQC+Dn| zAL-Nek#Eqe)}!SFysN;FL-e%c(eWi}{s2Q}slD`jxU=WRe+FK{__p-7F z71p^w>)T^e9-WDGgN?>F)X&6m2i#fBGnjb`hr0Y~=-B7_k+_;!Ebll&vQ$iqD3k+ktk1&c9dJS9Py z(&-gVBk^A)SImG1`MfP=JmJY&cnf5eWP-q57kw?U~_ z_Ljm~-y2e{>}r#f;nRrN(+1~y{`DdhmluWgD_VsO#%GtVx2)+=May9XwH+h#da}7HNSHta; zqDSB23kvMqVYXX4`}z86T3<`c^_fky3Tyh^zID3R-%7d9w@%}m*RlS-Bwc&fio4%; zCQ7>}qz(2h*FLsZ4)%?RGXHi#B>A)WodU&e>8&8fKH@=NfFMUii1w)|VTp&?ReZ62r-=!=oNsLh+;s-IkL2 zMKdz4cTm_tVULf-@L1!ka-m|gIs36sPy!UjRwfF5pdqVMWbfGuA8ElJC=bXqWl#-# z8n#o(mLGzr>Jr$5$7-ZTF zPtzc0jPPd~n>WA-m9EGCfMZq#WWAc8D2}7}0>6x~YwiA8EB55c$mP)HOxUx=ausLo z@kx=Vv@I`zheod5CcpIfGucWy{qv}5z0 zawo`#6Ds-oq9X7%oQFg9;cV;wI+5}5h6Hpo(lEI*SEqlwNq3|xhef~0VPg*%*PSII z2a1$}<8Wa<2B`^7^kB*E3(5*5zcGmk+Z$3C6Vd-9JnN-kV}$jdDyf-2Y*BhDr#2Sa z*9CjRjd1;VazgP#X$-hC@}RWkZ^5O6umHIo!6R_D@7*POB%L)b35H)-cK?szJkKsG z>lN8`W|f-YFEn_!U0Md$vhKm~6Vz6)FOwX&g%B!_uj`GR2<=kii{J#$5{|r?EAuLop6}~tu zF}!AR{G_u3me2VSKt*6|0d;}GQ?2@c2v_-x@J9&Sp}YZ?M&L8J&~L~#d*37PIFMap zGQinZ{~E-gZ`>?S8H|NwwPR4qQ1&rN=nKjEU= zxZ|oNN*YZBD#&`pWWwYI^|E2^5+)7H7ykOj=Azu%B zYk%CMEP&l?-=imTK<}6|7xp?g12jEN(o1e_1vh_D) zno^iM|I*{Q2KLs4I~K6*1{q-kZ4Uzr+4mlWo8UQj8{uD)YxP?Xx{0P2Sq71Ol|1&gne9Dl38qoh$PA5Vd1KYLzB3xI> z(^JWJ^THeY8|sG2nbEIvgIoYJ!1lP_X7pvL{;|t7nFbdt9tt0ahr;3We+3O$6-Hwz zJ_R50Bw79nwk7f^rC=F+5k9JTH=M;1I(NR@m5hHn@=3%tXfho0j3IMW|GeK&H>Ti_ zU4QHj+i1w0>RKH0TPoWOKKiIyeJflwgTqF7{1jXQJ9VQDE~Bs%s0kY2ZclSosF2sa z!Qu07*MUL2EhDF)I{z!yu*N_02v!rcf0HDk5Uo%Kgy1|qC{y@~V;h|K7D*MyW3qXK zYvJ>TTd1ObN5q0I4~F-6c=HA##1-y+Q&||;NRr$WY&@+F^GM6%(H6_=+O7i~>)-(x zpeCFMzyB}>@P7|%gD%1sxj#5-UimgT^BBjsO5#U1vuikhGgXAH+CtqSVr^@chuSut zu`jDi7CJY#Ne8Rl7@u~rbNCNA?y_BS9`ny@^}p936#JULq}63s7E|Iw6CU?3yRx&P>@t7!mBsDTQmz<_P|{@o zHK7e<{-swQA2KKUn_f}eDYOIeWnEpd)SnTGo#cP*a#CyiB)`!zD
V@z^-+Ne=I zNB2(7962U4y?4gwUSm>Ii_r`TsWmEqfyHF!3 delta 10729 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 C{tzet diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index c8cf202df3d0dc69ecf18e478188be9a745472bc..e39955c8be1dfecc9949cbda7a4b581274325dd9 100644 GIT binary patch delta 167 zcmaEGUE;xYiG~)&7N!>F7M3lnA6RY74Nc5b4NOgvl8llq4J;B{b`EX|CKElf-dEG>=AEzAr}jV%qg>#(tIU?FUTkr_@Srq_RD(Via1 R#p>GL!Ogn8gPScv0RY@cE*t;= delta 167 zcmaEGUE;xYiG~)&7N!>F7M3lnA6RV+6U~h+6OB_WjVux^jLZ^^4NWX9P14LwQp{6R zl2eTI3{CWMa>{b`EDX)eEliC~3@pt}&CQIAEDg=K>#(tIU?FUTkr_@Srq_RD(Via1 R#p>GL!Ogn8gPScv0RR|L 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 delta 12433 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 diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index 7a3ab3a639c30e23667ac7a82a86dea6cb636e02..a548a31ec278d9c5de98966d58b4f9cac18b3ed6 100644 GIT binary patch delta 860 zcmZ2;PHN3LsfHHD7N!>F7M3lnTmixc28Mu;L32u`icAJ>vRK_o8a;I^h?>SmAJHONE_mIHppWJSap6}$0EM{cMhu%R!?nL$Y&L0 z#HumAkX3ejY60sEtQx@~DQ$}1Me5RKgcRV33a}N^{}!`yV09lTG^g8_;E|Ov#1~?7 zOIUlc8oZH(O=f#!DXTiWjk%$Td8&b_Nm7zgvZaAVVycCyrFpVhsF7M3lnTmiyHW@eVgCML$l7RCmK2ByX)Mn=;Y=CG>E7#i#4 zHpGLIdRI$n3&);8Dh7IIc^o(7o@YEXToZ_Vg#%3bm?qX zh3Q+fSRL>xNyI7(_P7Zik59jp&02{|tA?~8ZfAo$W`I@a$8{{?+kfY<3Ssrsc7=Ra zQAVs9;|p13x2G1c&cLb>9Fo$e_+6wfZAM4|o~Qs@G5v2bD+gBhfkJb-eF+{}8AE&_ zHn)Ve7puV=S=eN@N0zdxv)dRZnj2dt8mCwqStME*nI#$F7M3ln#~p=?EsYEfEe$M;Ow7zpOpQ!TOr|G#vT8_L=;h>; zN35~ z9ILkJyIonQVb!JB$&w(6%ZV1#5B9Od;gQ~(%BnQI%pJR(pum+fG{a^1^oQ=O=~%o7 zi4-H8+Q7yd5r`Ed+}1+$TjDd{0Ee?QB@OXu)0ocd#mbIL?{t4JRyG`x8q@o6>(rQj z!HbmxmriLToaxtM`bJq+neCq5tWnH1=7uKbsRpJdNl8Y@z)X;sYGG<=o@|zCY@A|h zVQOToXJ`WPo28ktv4x3=fu*IfxrLdbsj;Qu_WORU8(0V%0Sk33Mzo&}X5D@|m@QQf E0AioybpQYW delta 923 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 UHyinDoQoxCv(YZkxpnFP0l_h&+W-In delta 9930 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 D*&48? diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index 7be14213901d0014530f793c8dbf62c8a1220aef..cddd3eb180bb96ba7ecc8529a0768d63d99ffcd7 100644 GIT binary patch delta 1365 zcmah}&r4KM7hchBNBvL4F*~z zt^5{k`UApR3_Zz3e}bDp+C-R|pyMwrEd=$=xcu&OeeQS8cg{WcR+g>GvNcyulpD*} zD)4o!MF!mQq*U6~T02}x?c=Lsc$5?QQfX!?&xr$J_7|iQOBa#_1;L!}dXT1K+Pf62JY1$D~ZOAwJoHv=MFND#pO#7W6loW^L@=h7^eo zY(tdeS)hg+{=v! X3jDGTT{xd&eXp0&EL=)oKW#Y&7dvlV delta 1366 zcmah}&r4KM71UTP(hLndQWRF#R18F>>_PZzzWEccF`f!_7UI zkF^q%bz}KK>Em=W^kc^$Ys3#b5XY}Sp^ub_HpHQQNGj1b?r97x?ZfkW)vSr#2aq7q zfdicHd$M>22FXwhQgF@AUko;trfP|)V|a+y zXD~)Fht+EK(ot14O*}kT+<6V{zEGe`*YE(xFW@t!dGnHl#!N)6%PNWkD}NQ@-}0;j z%}YgdqS1184Lik5RU}iM)kv%h`yQ|uP+x&OB>VTqu!IOA2$&B#z6?}~M@IYFA zU=@7f1)gvNi`%|$Pf5EVCkqn=k>&OcoyT42IhLKP{%z6~!sjv^0k#acQ;7c?5fr$# U3(s&q!TM)clPp|K-nnY|2U}ik;{X5v diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index dd6945bc9b72f15eb3b7e2a8ebf60a73c3e6d101..9463fa1c4cea0eb256c78b3609da8acb95b17147 100644 GIT binary patch delta 1014 zcma)5y)VO26!p`4>nm+YCH)XhLqr$#`}O@k-U~x*CSfE&d~~aU!N5Yo-w?kFaS z$sl>%#3BZRKY&5Zk_uI61Z}PFHRf~fIrpCPUg0HDc!_M~jeK+7cM0+>n+ zKQrM8dTqx(nv6j|b$&q?B`T27`v~n7WK|$Zj}>T9P0MlxPJAkao<=Z4R~byu!?)U6 z4*x*AI-pv#Jx*G`)kO(5sm;JumABk8u(yG*ZR&;V&bFl`$T1(TsT;dN+~`&nLf6kQ zLi>K4R&7g%Ly)23EF_{@*BWPzI^*b9Doou$MEY9s9+Yss!eBr%T)UVi0;nvw_Q;cT zphM3CxUIE}HArzQh~qHoS+mfS33%MN$aEM?h$kqDPDZri31 h%&Y!AhY5>1!gwT)Lm2;uPTJM#>=eT&i(&t5{1@q*2EzaV delta 1007 zcma))zb`{k6vsV1r=?9BRMM)DrXo7kyYJWS4MT0FhLJ?1(VA*tFtAk8#6pO{8O0(!<&uZ~XRjamFeWxIwqFmPp-nmg(pPBJ!XCtX(;yMbk~G{YFoZG;lQU-=za{Nbe_W`-F~R8 z<>)5_)d9KD_S=T{t4T{5cdzrc;CDoxceGPHx>%bdO%0Jyj!su73?x~ayqq14|cc*FB zOFa(bh7#&pCR)T-iQpuRvjR6o!7K_RTgVzt!DN;Y)*`oxyi{B$8X0E&hLL3s&)TM? h3vP4AFa+Ziis7EzkK*7zIELfN*{sJ<)?@yw!7nUo1djj! diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index 8ed2ab28437238d50c4858d3804f67f5f16b58bd..dfc27e67ea00373bbc4d6815c60366793be03241 100644 GIT binary patch delta 10972 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 Thn03ZFiq2jTbn))te*XU+MPZL delta 10912 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 QTA*nIt^fTzwC2A5067RUZ~y=R diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 90432ef5fafd63f7440ef9f8de7efdb00fef3abc..b76eaaddc0e266f391127c91ceb9cb9c3995f6f4 100644 GIT binary patch delta 12917 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 UaHIZP?-WH@W!zisUHRz$0o(G_4*&oF delta 12953 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 ZMgKqJTX>8;UwMiZrN$V1!n11D{{gzS%FO@( diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index af561c838d0968ce5242eebfa5c2d4a356499fbc..ae08f4534e78578ab7c9ee441c2c348638e47c6e 100644 GIT binary patch delta 10680 zcmai)dw3PqwZPY$$$=CCBq2z6&m5vWijqJe2Z+3k5FiRspokF&j}kNj+Eh@`;SmrJ zC1}ctf;=>86s%}-JE@}38f~l~h)AL33MeAADIy|*V(o9v*^?RWN59KIe82rzYwfkx zUVHC3IrUtdbI-LYYp><4?a)~(Up!>l5N+9z!2b-J!8K~aeQ;TGLry4u9bV)KkW28wL_^9|f9{teDanTHQVZA$7cXv5 zD2x6nav;T!vS@#AP2Q{*ncmuv;<%W_FMTbtgBA8fptIu4vm%)&9HICv`<`Zcr+}4( zXR?=ig1<&cKIEIf z1J9*nzzrEso_yFuUCJ?YR`RIF*0-u2GKagWS{u&<33R^P4Ns-={L{`A5 zihm7H@Oi|BbA7!0N09}LZ=%N6!875;4qFIp@^6vZG&EBKzW+(&0v4vg&aS#kG)d^^ zt>8mg`g?r6rb&CaNV)j;aOnVCuI6vlH5tr;S}6HP;n8qYc$~8$BTkcI-+)4RHJqR( zJPTLAjTCj1>d3iXW3?|{z@Q!G<9a|XfGEUH_>Fp3bk_d4sWW7G0KoS zW%z<-$jLJ#SMfK^HCe;>pptvQHrwfQG;0KMc8_JA+DsoKC-oLXx~h3&m{&f*OQAk3 zG)ceB5Q<0Cp}P`s$gPH~R^z86GafzedOm}9z;4c8-BOcnuu9QeWF`&al?@H@BAhzW zi%4RMCYe5d5FP^;DTUv`HMe^a+H4ng(AVhJ=LwqROf_VR8n-n`lLM2y-0z5CQ&xGK zT5qk%T-Y`IMtCfCxx5^%px;yutb%0fN1B#(xL1>QnlgDZU8u?+^n?m{AP zHU?HG&*rw%qzVJvRQ@wu2S;8G#uKSo*sta_ClgMg-_^IWE%Sb9$RSl` zn{Ay)?ykQSb(SmoVD zbPG9q_!%Ns?=$396z9;Nt`xs)Xp-V_FWD8+q{M5GuhKNx4tG@(?zmc$Loi!0>Y;;n zepkJxrY*4-chg&FNpo6hQZ&aaBCYK$-SxeiHqpMPhu$J+Rd>^5Ux^_lO5P>dm}f*6 z=omb#I}z}489W$HP~(rlc|Pv?b4@0|*{c6u_!zuR@!#QcI4{PQT-}2rGvAOZHQ+JW zSnK8PJ~-aT$=9-{?)MI=DR3p6rb5}_I!($L??z}cdETyqd5O-VD2&>8;4Cz4X0>x&2w_VlQ%K@EwnP#qmRU8jPB#-sAyPXJ5+S z3j6nPIlKaS?HTVl{)m0Nx4tQ0?d+n-xuxC~D8=OJM?Ae>!lkgYK4;#|nfMbn6x;Bn z#oRa)pMv+nhT_(J*ebsHi(#L9OTM7NuV7T)%N10^=}SBXpTm9yDOr~*cnD5dX2>yR zz;-xorB}62!?nu|858a2U``8bGW}t1LIGTlfI~{aV{qOo@6NRwPGNjZfF=O~n6tv0 ze{mr4Ft|p|Uo{AMM9R&W+XfSGIC}T9f6UVLptYjr9*5_81Eli}n#8SP z-*GrQX;1~{QK$?Ia?<{}8!7SYyb68aFimgd3|KQ(T?HetQN_DDH{k>Wvsi?R5;hY6SZx5|VC8 z(J$a!*!5%uT-GLYW`u^@Xc?Rr^zSlLUNnxFMxlCK~tj4S+y}TWQe}gnuy1Xw<1>+qGOCp ztMWGHpqubGJXkrV681;vB%I)jSl64WOW5J2$enPN&#rB7?voAc3dlHn@ld@*z-pgC zl2v;<{9$s&O7sGBvFne(r!)^r`10VB^e*@m-<1{Kx*f)P1obJt!w75|}o?Jm% z_`NRuBY|Q1B<*RtaG3s6Pz&3IdHSaKjAw7*s`3WP)y8Ojt0o&>_crG=_{dWYtCW+G zjB)xaNtHC$`8v2lJ8hrXe{h2)7}=c89n2;5cq*>piCqgX3X0SssR~d|U^g zV7^KzZp?cBsZ=GmkCc{`lQ`0grupN1E~ z?t+iPWgbtJPyy$S&(P^`^+7Knufp}-1UU!4%=jFopzj@;^rzqD`S4~qT1_c5Z$#G4 zwvW%! ztw6?AQ@N*nZOCrSa(4MDc*PlS83*A-&wKM)+iQ#UolUGx_i(+q;N1kPO31%}DfLRu zDLC~-Z(jFbQ+;6fdQl9I_3>}vI^C25YW`_B-Zww}J`$`dB%NdKOO|_S=*j>$VAYZ1 z^YpLsC(og5Uej5<+k^5PTpctUs?C15#wRyrF8c^>7n3XF;T6ost#-^mGgbw`@`yZ=^Z!88}zPV+R~gFdHtcjc1bQ=t)(2M!-eB{&M)3um2!i z;q_Z$7U3Y@fN2ghU>gFRz@^UO+?ZI=xTF~iPzLUUS2XuHa3_vv;c+Is zJ<*iPsC-M4w`mxgWHyw;Tkb*tOkzb1I1Ud+VH>r=v|>$egWa7w6ZRYY?cdQ&LPhqZFsIhfgJ&GF^G%pZ8O1TAH#@vGD+c;5@~nBU_eG z8WYIS`mLUW2HJ&7^~t(*!>`EfZJr~S!`V-J7u5IQBKlp&ezt&}d6n64jq0|L8wi}P z)NO&wd>NJYAm<(Po73;yC}zTo+L^K_HoliEdWZwicfh?3XEULPn$U<~_$J5*`<{oX z6oo~OA3B&)6cZ#l_@TDFDQU`2n_z)!RsU+_)cO{(2fpCr=8Jel!fr(xZ4X(gZ_*2o z(VqRX=KymtsoTkvBTDWhc!yz1soE-EzzM6oM`gDqIDq-B)OzlOD|`-m%^vcob2L;^ zVlG{6HawBS_~8KDRp}|ij-oCee;mnpNsW{J<>>pL4;LmKfFB*&Dn znm!Bh#o1=VTXrq;cKgh@nf%BeM8B$_f1bw+lKxl)3(9M_yR1JuUdfxkj0!uzlw!rz zE6I!P4SCc{zNKL`4Q}H6e%0kKn+tF{{T&eCBt}1b?X$WSpY+2TO6N_cB*euw+V5?1 zo4zKf&9Gm6RX^G;>48Uh+08bkPWk({_Le=|I0Ma|u=nrPKZqML<8jWy9bO{rg1f%y zCBncbaMc@T^eA#l$y&JJP0!Ge?BfUYPI39I%Bg8@nX-@156-yh@KcPdQwBc|kEu1K zL>YS1ZwM^?*cDySX?TTC?p!Q!Wa`7|}ymsl^rJ!{>sBLO)Dm!yOmzH^|fUCq{`2S#kv z&N1-&K7In;?Hhj(u7urMp1MJkcYXc0!+U-6H^LuyY{_vNcKRlC#?TMF0TSM%$$lT# zZe~Zp_$6v!qb-zUALqh-$*nDgc?r9HSno$ zNX{wm7TasT)A#Du0*u)(G$dzKRBGS}gSfaTZd|F!i+eofBjFM_q>Nn+{|LL@-3$L4 zb{CWU9NXO2KMvMDXz0H|DrktK!4>d#cpDSkebn)JyaQjW6c)nuzJBcmJ9J5(9kjlE zhLs@5o%qQMhkeG+D^!GCisMg+)ZH2T znB?>i_*`W0q)_yTb@(j2ucH~gjXM0%Nlu#cv9DhaKk$W!(J>VLRRgx<_J0W1pHTG7 zb_P5S*L1=oO5q-OZhT0#;~}TN<(K?i6Bj!J9X<_j#^5~F|N1{gcHe8rb~Qg-&yNzn zGGwgk*S^8v-x#tv+HYy{hupjFLDe4 z&LQXU(-%eJSfNt`94`4D1!wtoPg*;i@&mtm*s6iGY%`LT;_3)I4Ci9-HZ?x{lSnET^7e{z7&GQP$Dmr^ zwE<4g8x1LdEl0o%8q#wOS)nFug-b`qeg$y)zk_S&->nSjqqC~FI7QU>yW#XHUP!hE zxgk&TTprJPI~Rq{dFjZT37@(;db76p2oaQ*X{fqnMt^K{1{{I2|Ktgn(wOb}2T#C8 zIKDP^;5y^~4R3pyH>Oe$jOP|iNZpW(Pr~5ih8QX&pSI$C{=V}>v0@h8a1{b}$KEWC zz)HBb&WwJYcer_5;+hmYe;ghUm$i<4(Qvp$J6udsPEiJ|f{WU5E-05j+MbJAhmcA- z&J;NF1PinhVg|JDzzU~^8eF=*qjp(L_jJN2TwJUaj5P34e@eFEZ{g~zVsA*tprNL7 zW9l3&V2+39!GmkPUGWoqA}b`;0VSX?#7Y02x8nHD#L&lg!mIz{_3wrovKWOu(=H#s z2KJAie2peXlaS;pk30t#ObJ=hOOs>qej2K9@or^Mn=T}kFNSTqQW=-VzF|7!C&9JD zVkOt%6l!r1A?d2-4~Ody$36=<{lB}G5I<-%}}6w@5Ax7A=OGjhiv*UaLZFkH6G4+hmWeN|2_B&E+4Bn>v}vwNSs}h#P2Hi!ztGD zSg2gmX&{#PRy-X}BTF|Z0kv@H9+>0o_6f8XHaA`^ItOY3J10!SCGiUrt4uNt!5G}p%&aoB`oAKT@>hcQ&a1$z43Rc1X z5PS*83P;R|ObPW2=J1eX@ z8qU$+WTm69WCm9(-)VOG9+GGrr9W4%hiOct0WT zXv(6PJWC#W;PS%p3G6Q%ix!aeX=ZfAj)4;w;&PwBf598tn9*OSoc@;{E#AJ2>T5>3a4JjzE=WUE@M|@n$cfQ z9fc#{L;X!DRUWh9n)HSh%Qx`YY;`y}=YPNDRIUtD(v`rs;fh|o;*@~ghj|_LHl@zL{tB@H#WG=T=^u;|P4 zvu=PHeGfYV8n5Pke2+ctQcp1gAgnD2%{tWxzIuSk$032q1<9s{bMQ4E^rDeg!s0m<<=2C1uo& zQ7lM#_&hw}CdXiP{tsJArOP*?FB?a}Ja}6^hml(GtMEeDRd59o+j59m8e( zeSpI!;e@YapBNm@eiD!QxC}nw@1AC<&!B*ZVq!2>SSG;* z2KRcUaI4dQEyru)m;rsCB_xz)H|Fodb=QZaOu4x7d3FuQvs(#oc!5HO$F`{j4tR+| zH6vuV?h==FJS8KuPgX|1K3Q3P)5CqkeS7!H$O=amKM-sgDU1&;xnj3{K0eq*yK+~0 zli=|{t1AhW$d>ML*R<<%U2*Y!cVBm<5s?|qf=jO`Hq!jbV82La^I+u_yVoTJjY!iL z!39_BbygFKywM`q`oDI!jC{}p5vhs6L024O-wXZgx1+v8JSr#()(mio!VMxWmOG9s_{2|jM$*elpE?*Et5tM7l6V{hsk V?76|-w?DX0)B4*t9SE*}=s)u~+G_v+ delta 10701 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 diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index 0b6450b61b84c2a86423710ab8291ed9dabaf187..dc9e3454e2078fb46e7c66db489e5bbab13e4673 100644 GIT binary patch delta 10755 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 Date: Tue, 21 Jul 2026 15:18:31 +0300 Subject: [PATCH 4/8] fix(sequencer): fix double mint on deposit when reconstructing state --- lez/sequencer/core/src/block_store.rs | 4 ++ lez/sequencer/core/src/lib.rs | 8 +++ .../core/src/tests/reconstruction.rs | 72 +++++++++++++------ lez/storage/src/sequencer/mod.rs | 8 +++ 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/lez/sequencer/core/src/block_store.rs b/lez/sequencer/core/src/block_store.rs index 955ae7c0..658c8e1f 100644 --- a/lez/sequencer/core/src/block_store.rs +++ b/lez/sequencer/core/src/block_store.rs @@ -211,6 +211,10 @@ impl SequencerStore { pub fn get_unfulfilled_deposit_events(&self) -> DbResult> { self.dbio.get_pending_deposit_events() } + + pub fn is_deposit_event_submitted(&self, deposit_op_id: HashType) -> DbResult { + self.dbio.is_deposit_event_submitted(deposit_op_id) + } } pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap { diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index c7b55f97..88d2d020 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -615,6 +615,14 @@ impl SequencerCore { }; if let Some(deposit_op_id) = extract_bridge_deposit_id(tx) { + if self + .store + .is_deposit_event_submitted(deposit_op_id) + .context("Failed to check whether deposit was already submitted")? + { + info!("Skipping already-submitted bridge deposit {deposit_op_id}"); + return Ok(false); + } deposit_event_ids.push(deposit_op_id); } diff --git a/lez/sequencer/core/src/tests/reconstruction.rs b/lez/sequencer/core/src/tests/reconstruction.rs index 2bd9ebcd..9d3407b9 100644 --- a/lez/sequencer/core/src/tests/reconstruction.rs +++ b/lez/sequencer/core/src/tests/reconstruction.rs @@ -203,19 +203,20 @@ fn build_public_withdraw_tx( LeeTransaction::Public(lee::PublicTransaction::new(message, witness_set)) } -/// The channel is ahead of the local store and its extra blocks carry -/// bridge deposit and withdraw transactions. Reconstruction must replay them, -/// reproducing both the minted/burned balances and the withdraw reconciliation -/// bookkeeping the production path records. +/// The cold-start backfill re-delivers an already-finalized deposit into the +/// mempool before reconstruction applies the same deposit block, and the queued +/// mint cannot be pulled back out. Since the bridge program does not dedup on +/// `l1_deposit_op_id`, block production must skip the already-submitted deposit +/// so the vault is minted exactly once. #[tokio::test] -async fn reconstructs_channel_with_deposit_and_withdraw() { +async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() { let recipient = initial_public_user_accounts()[0].account_id; let deposit_amount = 500_u64; let withdraw_amount = 100_u64; let bedrock_account_pk = [0x22_u8; 32]; let deposit_op_id = [0x0d_u8; 32]; - // Sequencer A: a deposit block (sequencer-origin) then a withdraw block (user). + // Sequencer A produces a deposit block then a withdraw block. let config_a = bridge_funded_config(); let (mut seq_a, mempool_a) = SequencerCoreWithMockClients::start_from_config(config_a.clone()).await; @@ -224,7 +225,7 @@ async fn reconstructs_channel_with_deposit_and_withdraw() { let deposit_tx = crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx"); mempool_a - .push((TransactionOrigin::Sequencer, deposit_tx)) + .push((TransactionOrigin::Sequencer, deposit_tx.clone())) .await .unwrap(); seq_a.produce_new_block().await.unwrap(); @@ -247,46 +248,77 @@ async fn reconstructs_channel_with_deposit_and_withdraw() { let tip_slot = messages.last().unwrap().1; let channel_id = config_a.bedrock_config.channel_id; - // Sequencer B reconstructs A's chain (bridge blocks included) from empty. let config_b = bridge_funded_config(); - let (mut store_b, mut state_b) = - SequencerCore::::open_or_create_store(&config_b); + let (mut seq_b, mempool_b) = SequencerCoreWithMockClients::start_from_config(config_b).await; + + // Backfill re-delivery: persist the pending record and queue the mint, as + // `on_deposit_event` does, before reconstruction runs. + assert!( + seq_b + .block_store() + .dbio() + .add_pending_deposit_event(deposit_record.clone()) + .unwrap() + ); + mempool_b + .push((TransactionOrigin::Sequencer, deposit_tx)) + .await + .unwrap(); + let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages); SequencerCore::::verify_and_reconstruct( &mock_b, - &mut store_b, - &mut state_b, + &mut seq_b.store, + &mut seq_b.state, true, ) .await .expect("reconstruct"); + seq_b.chain_height = seq_b.store.latest_block_meta().unwrap().id; - let tip_b = store_b.latest_block_meta().unwrap(); + let tip_b = seq_b.block_store().latest_block_meta().unwrap(); assert_eq!(tip_b.id, tip_a.id); assert_eq!(tip_b.hash, tip_a.hash); - // The deposit minted into the vault and the withdraw burned from the sender — - // reconstructed balances agree with sequencer A for every affected account. + seq_b.produce_new_block().await.unwrap(); + let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient); let bridge_id = system_accounts::bridge_account_id(); for account in [vault_id, bridge_id, recipient] { assert_eq!( - state_b.get_account_by_id(account).balance, + seq_b.state().get_account_by_id(account).balance, seq_a.state().get_account_by_id(account).balance, "reconstructed balance mismatch for {account:?}", ); } assert_eq!( - state_b.get_account_by_id(vault_id).balance, + seq_b.state().get_account_by_id(vault_id).balance, u128::from(deposit_amount), - "deposit must mint into the recipient vault" + "deposit must mint into the recipient vault exactly once, not twice" + ); + + let produced = seq_b + .block_store() + .get_block_at_id(tip_b.id + 1) + .unwrap() + .expect("produced block present"); + assert!( + !produced + .body + .transactions + .iter() + .any(|tx| crate::extract_bridge_deposit_id(tx) == Some(HashType(deposit_op_id))), + "the re-delivered mint must be skipped, not re-included in a block" ); - // Reconstruction recorded the withdraw so the later L1 withdraw event reconciles. let withdraw_arg = crate::extract_bridge_withdraw_data(&withdraw_tx).expect("withdraw data"); let key = crate::withdraw_event_reconciliation_key(&withdraw_arg.outputs).expect("recon key"); assert!( - store_b.dbio().consume_unseen_withdraw_count(key).unwrap(), + seq_b + .block_store() + .dbio() + .consume_unseen_withdraw_count(key) + .unwrap(), "reconstruction must record an unseen withdraw for bridge reconciliation" ); } diff --git a/lez/storage/src/sequencer/mod.rs b/lez/storage/src/sequencer/mod.rs index f8557771..8d60ae18 100644 --- a/lez/storage/src/sequencer/mod.rs +++ b/lez/storage/src/sequencer/mod.rs @@ -446,6 +446,14 @@ impl RocksDBIO { Ok(removed) } + /// Whether a bridge deposit for `deposit_op_id` is already recorded as + /// included in a block (its pending record is marked submitted). + pub fn is_deposit_event_submitted(&self, deposit_op_id: HashType) -> DbResult { + Ok(self.get_pending_deposit_events()?.iter().any(|record| { + record.deposit_op_id == deposit_op_id && record.submitted_in_block_id.is_some() + })) + } + fn increment_unseen_withdraw_count( &self, withdrawal: WithdrawalReconciliationKey, From 8fdc3f203dbf3fd38ebe6d78fb0dd2b3732682e1 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 21 Jul 2026 21:36:16 +0300 Subject: [PATCH 5/8] fix(sequencer): don't increment withdrawals count on reconstruction to prevent phantom counts --- lez/sequencer/core/src/lib.rs | 25 ++++--- .../core/src/tests/reconstruction.rs | 70 ++++++++++++++++++- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 88d2d020..29418919 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -390,21 +390,20 @@ impl SequencerCore { chain_consistency::apply_block(block, &mut scratch) .map_err(|err| anyhow!("Failed to apply channel block {block_id}: {err}"))?; - // Derive bridge bookkeeping from the block's transactions, matching the - // production path so the reconciliation counters stay consistent. - let mut deposit_event_ids = Vec::new(); - let mut withdrawals = Vec::new(); - for tx in &block.body.transactions { - if let Some(deposit_id) = extract_bridge_deposit_id(tx) { - deposit_event_ids.push(deposit_id); - } - if let Some(withdraw) = extract_bridge_withdraw_data(tx) { - withdrawals.push(withdraw_event_reconciliation_key(&withdraw.outputs)?); - } - } + // Mark the deposits' pending records submitted so the production-time + // guard drops the mints cold-start backfill re-queued for them. Withdraw + // intents are deliberately not counted: backfill already re-delivered and + // dropped their finalized L1 events, so an increment here would never be + // consumed and would leave a phantom count. + let deposit_event_ids: Vec<_> = block + .body + .transactions + .iter() + .filter_map(extract_bridge_deposit_id) + .collect(); store - .update(block, &deposit_event_ids, withdrawals, &scratch) + .update(block, &deposit_event_ids, Vec::new(), &scratch) .context("Failed to persist reconstructed block")?; *state = scratch; store diff --git a/lez/sequencer/core/src/tests/reconstruction.rs b/lez/sequencer/core/src/tests/reconstruction.rs index 9d3407b9..4f08631f 100644 --- a/lez/sequencer/core/src/tests/reconstruction.rs +++ b/lez/sequencer/core/src/tests/reconstruction.rs @@ -311,15 +311,81 @@ async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() { "the re-delivered mint must be skipped, not re-included in a block" ); + // A reconstructed withdraw's finalized L1 event was already re-delivered (and + // dropped) by cold-start backfill, so it will never be consumed again. + // Reconstruction must not count it, or the count stays phantom-inflated forever. let withdraw_arg = crate::extract_bridge_withdraw_data(&withdraw_tx).expect("withdraw data"); let key = crate::withdraw_event_reconciliation_key(&withdraw_arg.outputs).expect("recon key"); assert!( - seq_b + !seq_b .block_store() .dbio() .consume_unseen_withdraw_count(key) .unwrap(), - "reconstruction must record an unseen withdraw for bridge reconciliation" + "reconstruction must not leave a phantom unseen-withdraw count" + ); +} + +/// A reconstructed withdraw block must not touch the unseen-withdraw counter. +/// Its finalized L1 Withdraw event was already re-delivered (and dropped as a +/// no-op) by cold-start backfill, so counting it during reconstruction would +/// leave a permanent phantom that nothing ever consumes. +#[tokio::test] +async fn reconstructed_withdraw_leaves_no_phantom_unseen_count() { + let recipient = initial_public_user_accounts()[0].account_id; + let withdraw_amount = 100_u64; + let bedrock_account_pk = [0x33_u8; 32]; + + // Sequencer A produces a single withdraw block; treat its chain as the channel. + let config_a = bridge_funded_config(); + let (mut seq_a, mempool_a) = + SequencerCoreWithMockClients::start_from_config(config_a.clone()).await; + let withdraw_tx = build_public_withdraw_tx( + recipient, + 0, + withdraw_amount, + bedrock_account_pk, + &create_signing_key_for_account1(), + ); + mempool_a + .push((TransactionOrigin::User, withdraw_tx.clone())) + .await + .unwrap(); + seq_a.produce_new_block().await.unwrap(); + + let withdraw_arg = crate::extract_bridge_withdraw_data(&withdraw_tx).expect("withdraw data"); + let key = crate::withdraw_event_reconciliation_key(&withdraw_arg.outputs).expect("recon key"); + // Producing the withdraw counts it as unseen, awaiting its L1 event. + assert!( + seq_a + .block_store() + .dbio() + .consume_unseen_withdraw_count(key) + .unwrap(), + "producing a withdraw must count it as unseen" + ); + + 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; + + // Sequencer B reconstructs A's chain from a fresh store. + let config_b = bridge_funded_config(); + let (mut store_b, mut state_b) = + SequencerCore::::open_or_create_store(&config_b); + let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages); + SequencerCore::::verify_and_reconstruct( + &mock_b, + &mut store_b, + &mut state_b, + true, + ) + .await + .expect("reconstruct"); + + assert!( + !store_b.dbio().consume_unseen_withdraw_count(key).unwrap(), + "reconstruction must not leave a phantom unseen-withdraw count" ); } From e3442c695f0d478d1ee4f55fdf45dd34c0ae86b8 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 21 Jul 2026 22:15:20 +0300 Subject: [PATCH 6/8] fix(sequencer): fix silent reconstruction when only genesis was committed --- lez/chain_consistency/src/apply.rs | 3 +- lez/indexer/core/src/block_store.rs | 2 +- lez/sequencer/core/src/block_store.rs | 31 ++++++++++-------- lez/sequencer/core/src/lib.rs | 32 +++++++++++-------- lez/sequencer/core/src/tests.rs | 2 +- .../core/src/tests/reconstruction.rs | 20 ++++++------ lez/storage/src/sequencer/mod.rs | 5 +-- 7 files changed, 53 insertions(+), 42 deletions(-) diff --git a/lez/chain_consistency/src/apply.rs b/lez/chain_consistency/src/apply.rs index ea97d58a..28961502 100644 --- a/lez/chain_consistency/src/apply.rs +++ b/lez/chain_consistency/src/apply.rs @@ -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, block: &Block) -> Result<(), BlockIngestError> { let computed = block.recompute_hash(); if computed != block.header.hash { return Err(BlockIngestError::HashMismatch { diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index 774aa87d..3d840f90 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -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)); } diff --git a/lez/sequencer/core/src/block_store.rs b/lez/sequencer/core/src/block_store.rs index 658c8e1f..e58ac644 100644 --- a/lez/sequencer/core/src/block_store.rs +++ b/lez/sequencer/core/src/block_store.rs @@ -68,21 +68,24 @@ impl SequencerStore { signing_key: lee::PrivateKey, ) -> DbResult { 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 { + pub fn latest_block_meta(&self) -> DbResult> { 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); } diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 29418919..9d1329a9 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -201,7 +201,8 @@ impl SequencerCore { 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 SequencerCore { 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 SequencerCore { // 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 SequencerCore { } // 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 SequencerCore { 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"); diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index bcc40a0a..b398e160 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -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 diff --git a/lez/sequencer/core/src/tests/reconstruction.rs b/lez/sequencer/core/src/tests/reconstruction.rs index 4f08631f..18cff11c 100644 --- a/lez/sequencer/core/src/tests/reconstruction.rs +++ b/lez/sequencer/core/src/tests/reconstruction.rs @@ -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::::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 = diff --git a/lez/storage/src/sequencer/mod.rs b/lez/storage/src/sequencer/mod.rs index 8d60ae18..8910b044 100644 --- a/lez/storage/src/sequencer/mod.rs +++ b/lez/storage/src/sequencer/mod.rs @@ -344,8 +344,9 @@ impl RocksDBIO { self.put_batch(&LatestBlockMetaCellRef(block_meta), (), batch) } - pub fn latest_block_meta(&self) -> DbResult { - self.get::(()).map(|val| val.0) + pub fn latest_block_meta(&self) -> DbResult> { + self.get_opt::(()) + .map(|val| val.map(|cell| cell.0)) } pub fn get_zone_sdk_checkpoint_bytes(&self) -> DbResult>> { From 61f17612d97aa469e66eab098af89b4670869625 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 21 Jul 2026 22:25:13 +0300 Subject: [PATCH 7/8] fix(storage): rename `ZonCursor` to `ZoneAnchor` --- lez/sequencer/core/src/block_store.rs | 4 ++-- lez/storage/src/sequencer/mod.rs | 10 +++++----- lez/storage/src/sequencer/sequencer_cells.rs | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lez/sequencer/core/src/block_store.rs b/lez/sequencer/core/src/block_store.rs index e58ac644..3a2de375 100644 --- a/lez/sequencer/core/src/block_store.rs +++ b/lez/sequencer/core/src/block_store.rs @@ -204,11 +204,11 @@ impl SequencerStore { /// The last channel block read back and verified from Bedrock (L1 slot + /// `id`/`hash`), or `None` before any block has been read from the channel. pub fn get_zone_anchor(&self) -> DbResult> { - self.dbio.get_zone_cursor() + self.dbio.get_zone_anchor() } pub fn set_zone_anchor(&self, anchor: &ZoneAnchorRecord) -> DbResult<()> { - self.dbio.put_zone_cursor(anchor) + self.dbio.put_zone_anchor(anchor) } pub fn get_unfulfilled_deposit_events(&self) -> DbResult> { diff --git a/lez/storage/src/sequencer/mod.rs b/lez/storage/src/sequencer/mod.rs index 8910b044..47ebc41b 100644 --- a/lez/storage/src/sequencer/mod.rs +++ b/lez/storage/src/sequencer/mod.rs @@ -22,7 +22,7 @@ use crate::{ LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell, LatestBlockMetaCellOwned, LatestBlockMetaCellRef, PendingDepositEventRecord, PendingDepositEventsCellOwned, PendingDepositEventsCellRef, UnseenWithdrawCountCell, WithdrawalReconciliationKey, - ZoneAnchorRecord, ZoneCursorCell, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, + ZoneAnchorCell, ZoneAnchorRecord, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, }, }; @@ -364,12 +364,12 @@ impl RocksDBIO { self.del::(()) } - pub fn get_zone_cursor(&self) -> DbResult> { - Ok(self.get_opt::(())?.map(|cell| cell.0)) + pub fn get_zone_anchor(&self) -> DbResult> { + Ok(self.get_opt::(())?.map(|cell| cell.0)) } - pub fn put_zone_cursor(&self, anchor: &ZoneAnchorRecord) -> DbResult<()> { - self.put(&ZoneCursorCell(*anchor), ()) + pub fn put_zone_anchor(&self, anchor: &ZoneAnchorRecord) -> DbResult<()> { + self.put(&ZoneAnchorCell(*anchor), ()) } pub fn get_pending_deposit_events(&self) -> DbResult> { diff --git a/lez/storage/src/sequencer/sequencer_cells.rs b/lez/storage/src/sequencer/sequencer_cells.rs index 302a0b97..368fb648 100644 --- a/lez/storage/src/sequencer/sequencer_cells.rs +++ b/lez/storage/src/sequencer/sequencer_cells.rs @@ -147,18 +147,18 @@ pub struct ZoneAnchorRecord { } #[derive(Debug, BorshSerialize, BorshDeserialize)] -pub struct ZoneCursorCell(pub ZoneAnchorRecord); +pub struct ZoneAnchorCell(pub ZoneAnchorRecord); -impl SimpleStorableCell for ZoneCursorCell { +impl SimpleStorableCell for ZoneAnchorCell { type KeyParams = (); const CELL_NAME: &'static str = DB_META_ZONE_CURSOR_KEY; const CF_NAME: &'static str = CF_META_NAME; } -impl SimpleReadableCell for ZoneCursorCell {} +impl SimpleReadableCell for ZoneAnchorCell {} -impl SimpleWritableCell for ZoneCursorCell { +impl SimpleWritableCell for ZoneAnchorCell { fn value_constructor(&self) -> DbResult> { borsh::to_vec(&self).map_err(|err| { DbError::borsh_cast_message(err, Some("Failed to serialize zone cursor".to_owned())) From e440430a508a5dbdd34c1461e9e7198da5603b45 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 21 Jul 2026 22:43:03 +0300 Subject: [PATCH 8/8] feat(sequencer): add more reconstruction tests --- .../core/src/tests/reconstruction.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/lez/sequencer/core/src/tests/reconstruction.rs b/lez/sequencer/core/src/tests/reconstruction.rs index 18cff11c..40cbaa6c 100644 --- a/lez/sequencer/core/src/tests/reconstruction.rs +++ b/lez/sequencer/core/src/tests/reconstruction.rs @@ -154,6 +154,124 @@ async fn fails_when_channel_is_missing() { assert!(result.is_err(), "missing channel must abort startup"); } +// The following cases exercise the divergence branches of +// `apply_reconstructed_block` reached with no recorded anchor, so the block's own +// validation fires rather than the up-front `AnchorConsistencyCheck`. + +#[tokio::test] +async fn fails_when_channel_reinscribes_genesis_with_a_different_hash() { + let config = setup_sequencer_config(); + let (mut store, mut state) = SequencerCore::::open_or_create_store(&config); + + // Fresh store, no anchor. The channel serves a genesis at the same id but a + // different hash — a foreign chain reinscribing genesis. + let mut reinscribed = store.get_block_at_id(store.genesis_id()).unwrap().unwrap(); + reinscribed.header.hash = HashType([0xAB_u8; 32]); + + let messages = vec![block_to_channel_message(&reinscribed, 10)]; + let mock = MockBlockPublisher::with_canned_channel( + config.bedrock_config.channel_id, + Some(Slot::from(10)), + messages, + ); + let result = SequencerCore::::verify_and_reconstruct( + &mock, &mut store, &mut state, true, + ) + .await; + assert!( + result.is_err(), + "a reinscribed genesis with a different hash must abort startup" + ); +} + +#[tokio::test] +async fn fails_when_a_stored_block_hash_diverges_from_the_channel() { + // A sequencer that committed blocks past genesis but never recorded an anchor. + let config = setup_sequencer_config(); + let (mut seq, _handle) = SequencerCoreWithMockClients::start_from_config(config.clone()).await; + seq.produce_new_block().await.unwrap(); + seq.produce_new_block().await.unwrap(); + + // A below-tip block re-served with a corrupted hash: we already hold this id + // with a different hash, so the channel is a different chain. + let below_tip_id = seq.block_store().genesis_id() + 1; + let mut block = seq + .block_store() + .get_block_at_id(below_tip_id) + .unwrap() + .unwrap(); + block.header.hash = HashType([0xCD_u8; 32]); + + let messages = vec![block_to_channel_message(&block, 10)]; + let mock = MockBlockPublisher::with_canned_channel( + config.bedrock_config.channel_id, + Some(Slot::from(10)), + messages, + ); + let result = SequencerCore::::verify_and_reconstruct( + &mock, + &mut seq.store, + &mut seq.state, + true, + ) + .await; + assert!( + result.is_err(), + "a diverging below-tip block hash must abort startup" + ); +} + +#[tokio::test] +async fn fails_when_a_channel_block_is_missing_locally() { + let config = setup_sequencer_config(); + let (mut store, mut state) = SequencerCore::::open_or_create_store(&config); + + // A block numbered below our genesis is at/below the local tip yet absent from + // the store — a foreign chain with a lower numbering. + let mut foreign = store.get_block_at_id(store.genesis_id()).unwrap().unwrap(); + foreign.header.block_id = store.genesis_id() - 1; + + let messages = vec![block_to_channel_message(&foreign, 10)]; + let mock = MockBlockPublisher::with_canned_channel( + config.bedrock_config.channel_id, + Some(Slot::from(10)), + messages, + ); + let result = SequencerCore::::verify_and_reconstruct( + &mock, &mut store, &mut state, true, + ) + .await; + assert!( + result.is_err(), + "a channel block below the local range must abort startup" + ); +} + +#[tokio::test] +async fn fails_when_a_channel_block_does_not_extend_the_tip() { + let config = setup_sequencer_config(); + let (mut store, mut state) = SequencerCore::::open_or_create_store(&config); + + // A block claiming an id far past genesis does not chain onto the local tip. + let mut orphan = store.get_block_at_id(store.genesis_id()).unwrap().unwrap(); + orphan.header.block_id = store.genesis_id() + 5; + + let messages = vec![block_to_channel_message(&orphan, 10)]; + let mock = MockBlockPublisher::with_canned_channel( + config.bedrock_config.channel_id, + Some(Slot::from(10)), + messages, + ); + let result = SequencerCore::::verify_and_reconstruct( + &mock, &mut store, &mut state, true, + ) + .await; + assert!( + result.is_err(), + "a non-contiguous channel block must abort startup" + ); +} + /// A sequencer config whose genesis funds the bridge account, so replayed bridge /// deposit transactions have a source balance to mint from. fn bridge_funded_config() -> SequencerConfig {