From c9c999e0de03895efb26313da4af91ca6f7605e9 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 24 Jun 2026 11:24:28 +0200 Subject: [PATCH] feat(cross-zone): seed bridge-lock holdings into indexer genesis for consistency --- lez/indexer/core/Cargo.toml | 1 + lez/indexer/core/src/block_store.rs | 34 +++++++++++++++++++++++------ lez/indexer/core/src/config.rs | 14 ++++++++++++ lez/indexer/core/src/lib.rs | 23 ++++++++++++++----- test_fixtures/src/config.rs | 1 + 5 files changed, 60 insertions(+), 13 deletions(-) diff --git a/lez/indexer/core/Cargo.toml b/lez/indexer/core/Cargo.toml index 249e6e49..3b120f48 100644 --- a/lez/indexer/core/Cargo.toml +++ b/lez/indexer/core/Cargo.toml @@ -13,6 +13,7 @@ logos-blockchain-zone-sdk.workspace = true lee.workspace = true lee_core.workspace = true cross_zone_inbox_core = { workspace = true, features = ["host"] } +bridge_lock_core = { workspace = true, features = ["host"] } ping_core.workspace = true storage.workspace = true testnet_initial_state.workspace = true diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index 6b7c5c83..362d994f 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -22,11 +22,12 @@ pub struct IndexerStore { impl IndexerStore { /// Starting database at the start of new chain. /// Creates files if necessary. - pub fn open_db(location: &Path, genesis_seed: Option<(AccountId, Account)>) -> Result { + pub fn open_db(location: &Path, genesis_seed: Vec<(AccountId, Account)>) -> Result { let mut initial_state = testnet_initial_state::initial_state(); - // Seed any zone-specific genesis accounts (e.g. the cross-zone inbox - // config) so the indexer's replayed state matches the sequencer's. - if let Some((account_id, account)) = genesis_seed { + // Seed any zone-specific genesis accounts (the cross-zone inbox config and + // bridge-lock holdings) so the indexer's replayed state matches the + // sequencer's; none are produced by a transaction. + for (account_id, account) in genesis_seed { initial_state.insert_genesis_account(account_id, account); } let dbio = RocksDBIO::open_or_create(location, &initial_state)?; @@ -220,18 +221,37 @@ mod tests { fn correct_startup() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db(home.as_ref(), None).unwrap(); + let storage = IndexerStore::open_db(home.as_ref(), Vec::new()).unwrap(); let final_id = storage.get_last_block_id().unwrap(); assert_eq!(final_id, None); } + #[tokio::test] + async fn seeds_bridge_lock_holding_into_genesis_state() { + // The holding is force-inserted, not produced by a transaction, so the + // indexer must seed it to match the sequencer. Use the same builder both + // sides use, so this also guards against the two drifting. + let home = tempdir().unwrap(); + let holder = AccountId::new([5; 32]); + let (id, account) = bridge_lock_core::build_holding_account(holder, 42); + + let storage = IndexerStore::open_db(home.as_ref(), vec![(id, account)]).unwrap(); + + let seeded = storage.account_current_state(&holder).await.unwrap(); + assert_eq!( + bridge_lock_core::read_balance(&seeded.data.into_inner()), + 42, + "indexer genesis must hold the seeded bridge-lock balance" + ); + } + #[tokio::test] async fn state_transition() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db(home.as_ref(), None).unwrap(); + let storage = IndexerStore::open_db(home.as_ref(), Vec::new()).unwrap(); let initial_accounts = initial_pub_accounts_private_keys(); let from = initial_accounts[0].account_id; @@ -283,7 +303,7 @@ mod tests { async fn account_state_at_block() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db(home.as_ref(), None).unwrap(); + let storage = IndexerStore::open_db(home.as_ref(), Vec::new()).unwrap(); let mut prev_hash = None; diff --git a/lez/indexer/core/src/config.rs b/lez/indexer/core/src/config.rs index bcf4a31a..4d3c9502 100644 --- a/lez/indexer/core/src/config.rs +++ b/lez/indexer/core/src/config.rs @@ -9,6 +9,7 @@ use anyhow::{Context as _, Result}; use common::config::BasicAuth; use cross_zone_inbox_core::CrossZoneConfig; use humantime_serde; +use lee::AccountId; pub use logos_blockchain_core::mantle::ops::channel::ChannelId; use serde::{Deserialize, Serialize}; use url::Url; @@ -31,6 +32,19 @@ pub struct IndexerConfig { /// Cross-zone configuration. `None` disables the indexer's cross-zone handling. #[serde(default)] pub cross_zone: Option, + /// Bridge-lock holdings to seed into genesis, mirroring the sequencer's + /// `SupplyBridgeLockHolding` actions. They are not produced by any + /// transaction, so the indexer must seed them to match the sequencer's state. + #[serde(default)] + pub bridge_lock_holdings: Vec, +} + +/// A genesis-funded bridge-lock holder balance, configured identically on the +/// sequencer (via `SupplyBridgeLockHolding`) and the indexer. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BridgeLockHolding { + pub holder: AccountId, + pub amount: u128, } impl IndexerConfig { diff --git a/lez/indexer/core/src/lib.rs b/lez/indexer/core/src/lib.rs index 46d2cbb6..6395ca0d 100644 --- a/lez/indexer/core/src/lib.rs +++ b/lez/indexer/core/src/lib.rs @@ -37,12 +37,23 @@ impl IndexerCore { ); let zone_indexer = ZoneIndexer::new(config.channel_id, node); - // Seed the inbox config so the indexer can replay cross-zone dispatch - // transactions, matching the account the sequencer seeds at genesis. - let inbox_config_seed = config.cross_zone.as_ref().map(|cross_zone| { + // Genesis accounts the indexer must seed to match the sequencer's state, + // since none are produced by a transaction: the cross-zone inbox config + // and any bridge-lock holdings. Both go through the same builders the + // sequencer uses, so the states are byte-identical. + let mut genesis_seed = Vec::new(); + if let Some(cross_zone) = config.cross_zone.as_ref() { let self_zone: [u8; 32] = *config.channel_id.as_ref(); - cross_zone_inbox_core::build_inbox_config_account(self_zone, cross_zone) - }); + genesis_seed.push(cross_zone_inbox_core::build_inbox_config_account( + self_zone, cross_zone, + )); + } + for holding in &config.bridge_lock_holdings { + genesis_seed.push(bridge_lock_core::build_holding_account( + holding.holder, + holding.amount, + )); + } // Option B verifier: re-derives each cross-zone dispatch from the peer's // finalized blocks. `None` when cross-zone messaging is disabled. @@ -51,7 +62,7 @@ impl IndexerCore { Ok(Self { zone_indexer: Arc::new(zone_indexer), config, - store: IndexerStore::open_db(&home, inbox_config_seed)?, + store: IndexerStore::open_db(&home, genesis_seed)?, verifier, }) } diff --git a/test_fixtures/src/config.rs b/test_fixtures/src/config.rs index a58be4fe..08eb952f 100644 --- a/test_fixtures/src/config.rs +++ b/test_fixtures/src/config.rs @@ -182,6 +182,7 @@ pub fn indexer_config( }, channel_id, cross_zone, + bridge_lock_holdings: Vec::new(), }) }