mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-07-17 13:20:17 +00:00
refactor!(cross-zone): deploy programs at genesis instead of builtins
Cross-zone builtin programs are no longer registered in the production genesis. A zone that participates declares the ones it uses via a new GenesisAction::DeployProgram (sequencer) and a matching deploy_programs list (indexer), both resolved through CrossZoneProgram and registered with with_programs. Cross-zone genesis accounts (inbox config, wrapped-token config) are seeded through the state constructor for a receiving zone, and bridge-lock holdings are seeded from their actions regardless of receiving config, dropping V03State::insert_genesis_account. GenesisAction amounts now use the Balance alias. Documents cross_zone as the reference LEZ adapter and the bridge demo as not production-safe. The sequencer's DeployProgram set and the indexer's deploy_programs are configured separately, so both nodes now log a deterministic genesis fingerprint (V03State::genesis_fingerprint) at startup: equal values confirm the two genesis states agree, a mismatch flags a divergent deploy set. BREAKING CHANGE: the genesis state root changes (cross-zone builtins are out of production genesis) and the sequencer/indexer configs gain the DeployProgram / deploy_programs list that cross-zone-participating zones must set.
This commit is contained in:
parent
de2dc8e04d
commit
a16e22c46c
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1863,6 +1863,8 @@ dependencies = [
|
||||
"ping_core",
|
||||
"programs",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9976,7 +9978,6 @@ dependencies = [
|
||||
"programs",
|
||||
"serde",
|
||||
"system_accounts",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -8,6 +8,13 @@
|
||||
//! zone B, where the indexer re-derives and verifies it (Option B) before the
|
||||
//! wrapped token is minted to the recipient. Reuses the M3/M4 spine unchanged;
|
||||
//! only the source caller (`bridge_lock`) and target (`wrapped_token`) are new.
|
||||
//!
|
||||
//! Not production-safe. The inbox allowlist gates the target program, not the
|
||||
//! source emitter, and `extract_emission` recognizes any known emitter, so in a
|
||||
//! zone that allows `wrapped_token` as a target a permissionless `ping_sender`
|
||||
//! send can carry a `wrapped_token::Mint` and mint with no lock. Making this safe
|
||||
//! needs source verification, where a value-bearing target checks the message
|
||||
//! originated from `bridge_lock`; that is out of scope for the demo.
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
@ -58,25 +65,31 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
|
||||
|
||||
// Zone A seeds the holder's bridgeable balance. Zone B runs the watcher on its
|
||||
// sequencer and the verifier on its indexer.
|
||||
let genesis_a = vec![GenesisAction::SupplyBridgeLockHolding {
|
||||
let mut genesis_a = vec![GenesisAction::SupplyBridgeLockHolding {
|
||||
holder: holder_id,
|
||||
amount: INITIAL_BALANCE,
|
||||
}];
|
||||
genesis_a.extend(config::cross_zone_deploy_actions());
|
||||
let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, genesis_a, channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (_seq_b, _seq_b_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
vec![],
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel_b,
|
||||
Some(cross_zone.clone()),
|
||||
)
|
||||
.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")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(
|
||||
bedrock_addr,
|
||||
channel_b,
|
||||
Some(cross_zone),
|
||||
config::all_cross_zone_programs(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
// Lock LOCK_AMOUNT on zone A, addressed to the recipient on zone B.
|
||||
let lock = build_lock_tx(&holder_key, holder_id, zone_b);
|
||||
|
||||
@ -34,9 +34,15 @@ 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)
|
||||
.await
|
||||
.context("Failed to set up sequencer")?;
|
||||
let (seq, _seq_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up sequencer")?;
|
||||
|
||||
// A user hand-builds a top-level inbox Dispatch and submits it via RPC.
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
|
||||
@ -54,13 +54,24 @@ 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)
|
||||
.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_a, _seq_a_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel_a,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (seq_b, _seq_b_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel_b,
|
||||
Some(cross_zone),
|
||||
)
|
||||
.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);
|
||||
|
||||
@ -54,7 +54,7 @@ fn seed_inbox_config(
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
};
|
||||
state.insert_genesis_account(
|
||||
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
|
||||
inbox_config_account_id(inbox_id),
|
||||
Account {
|
||||
program_owner: inbox_id,
|
||||
@ -65,14 +65,14 @@ fn seed_inbox_config(
|
||||
.expect("config fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
);
|
||||
)]);
|
||||
}
|
||||
|
||||
/// Seeds the wrapped-token config account pinning the inbox as authorized minter,
|
||||
/// matching what genesis seeds for a real zone.
|
||||
fn seed_wrapped_config(state: &mut V03State) {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
state.insert_genesis_account(
|
||||
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
Account {
|
||||
program_owner: wrapped_token_id,
|
||||
@ -82,7 +82,7 @@ fn seed_wrapped_config(state: &mut V03State) {
|
||||
.expect("minter id fits in account data"),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
)]);
|
||||
}
|
||||
|
||||
/// The wrapped-token `Mint` the bridge forwards, serialized as the cross-zone
|
||||
@ -170,7 +170,7 @@ fn lock_escrows_balance_and_emits_to_outbox() {
|
||||
|
||||
let holder_key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let holder_id = AccountId::from(&PublicKey::new_from_private_key(&holder_key));
|
||||
state.insert_genesis_account(
|
||||
state = state.with_public_accounts([(
|
||||
holder_id,
|
||||
Account {
|
||||
program_owner: bridge_lock_id,
|
||||
@ -181,7 +181,7 @@ fn lock_escrows_balance_and_emits_to_outbox() {
|
||||
.expect("balance fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
);
|
||||
)]);
|
||||
|
||||
let payload = mint_payload();
|
||||
let target_accounts = vec![
|
||||
@ -314,7 +314,7 @@ fn mint_replay_rejected() {
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let mut shard = SeenShard::default();
|
||||
shard.insert(message_key(&src_zone, src_block_id, src_tx_index));
|
||||
state.insert_genesis_account(
|
||||
state = state.with_public_accounts([(
|
||||
seen_id,
|
||||
Account {
|
||||
program_owner: inbox_id,
|
||||
@ -325,7 +325,7 @@ fn mint_replay_rejected() {
|
||||
.expect("shard fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
);
|
||||
)]);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
|
||||
@ -53,24 +53,40 @@ 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)
|
||||
.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_a, _seq_a_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel_a,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (_idx_a, _idx_a_home) = setup_indexer(
|
||||
bedrock_addr,
|
||||
channel_a,
|
||||
None,
|
||||
config::all_cross_zone_programs(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone A indexer")?;
|
||||
let (_seq_b, _seq_b_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
vec![],
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel_b,
|
||||
Some(cross_zone.clone()),
|
||||
)
|
||||
.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")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(
|
||||
bedrock_addr,
|
||||
channel_b,
|
||||
Some(cross_zone),
|
||||
config::all_cross_zone_programs(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
// Submit the ping on zone A, addressed to ping_receiver on zone B.
|
||||
let ping = build_ping_tx(zone_b, receiver_id);
|
||||
|
||||
@ -54,6 +54,7 @@ pub fn setup_indexer_ffi(bedrock_addr: SocketAddr) -> Result<(IndexerServiceFFI,
|
||||
bedrock_addr,
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
.context("Failed to create Indexer config")?;
|
||||
|
||||
|
||||
@ -38,13 +38,13 @@ async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> {
|
||||
let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
|
||||
let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None, Vec::new())
|
||||
.await
|
||||
.context("Failed to set up zone A indexer")?;
|
||||
let (seq_b, _seq_b_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_b, None)
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, None)
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, None, Vec::new())
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
pub use lee_core::{
|
||||
GENESIS_BLOCK_ID, SharedSecretKey,
|
||||
account::{Account, AccountId, Data},
|
||||
account::{Account, AccountId, Balance, Data},
|
||||
encryption::EphemeralPublicKey,
|
||||
program::ProgramId,
|
||||
};
|
||||
|
||||
@ -194,14 +194,6 @@ impl V03State {
|
||||
self.programs.insert(program.id(), program);
|
||||
}
|
||||
|
||||
/// Seeds a single genesis account that is not produced by any transaction
|
||||
/// (e.g. the cross-zone inbox config or a bridge-lock holding). Lets the
|
||||
/// sequencer and indexer seed identical zone-specific state after building
|
||||
/// the shared initial state.
|
||||
pub fn insert_genesis_account(&mut self, account_id: AccountId, account: Account) {
|
||||
self.public_state.insert(account_id, account);
|
||||
}
|
||||
|
||||
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
|
||||
let StateDiff {
|
||||
signer_account_ids,
|
||||
@ -287,6 +279,50 @@ impl V03State {
|
||||
self.private_state.0.digest()
|
||||
}
|
||||
|
||||
/// Order-independent fingerprint of the genesis-relevant state: the public
|
||||
/// account set, the deployed program set, and the commitment-set digest.
|
||||
///
|
||||
/// The sequencer and the indexer build genesis separately from their own
|
||||
/// configs (the cross-zone deploy set is seeded directly, not carried by a
|
||||
/// genesis transaction), so a mismatched `DeployProgram` / `deploy_programs`
|
||||
/// list would otherwise diverge silently. Both nodes log this at startup;
|
||||
/// equal values mean the two genesis states agree. Entries are sorted by id
|
||||
/// before hashing, so the value does not depend on `HashMap` iteration order.
|
||||
#[must_use]
|
||||
pub fn genesis_fingerprint(&self) -> [u8; 32] {
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
let mut accounts: Vec<(&AccountId, &Account)> = self.public_state.iter().collect();
|
||||
accounts.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
|
||||
|
||||
let mut program_ids: Vec<ProgramId> = self.programs.keys().copied().collect();
|
||||
program_ids.sort_unstable();
|
||||
|
||||
let account_count = u64::try_from(accounts.len()).expect("account count fits in u64");
|
||||
let program_count = u64::try_from(program_ids.len()).expect("program count fits in u64");
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(account_count.to_le_bytes());
|
||||
for (id, account) in accounts {
|
||||
hasher.update(id.as_ref());
|
||||
let bytes = borsh::to_vec(account).expect("Account is BorshSerialize");
|
||||
let len = u64::try_from(bytes.len()).expect("account encoding fits in u64");
|
||||
hasher.update(len.to_le_bytes());
|
||||
hasher.update(&bytes);
|
||||
}
|
||||
hasher.update(program_count.to_le_bytes());
|
||||
for id in program_ids {
|
||||
for word in id {
|
||||
hasher.update(word.to_le_bytes());
|
||||
}
|
||||
}
|
||||
hasher.update(self.commitment_set_digest());
|
||||
|
||||
let mut out = [0_u8; 32];
|
||||
out.copy_from_slice(&hasher.finalize());
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn check_commitments_are_new(
|
||||
&self,
|
||||
new_commitments: &[Commitment],
|
||||
|
||||
@ -14,4 +14,6 @@ programs.workspace = true
|
||||
cross_zone_inbox_core.workspace = true
|
||||
bridge_lock_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
wrapped_token_core.workspace = true
|
||||
serde.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
//! machine (`lee`), kept out of the guest-pure cores. Mirrors `system_accounts`:
|
||||
//! it resolves builtin program ids and bakes them into transactions and genesis
|
||||
//! accounts for the watcher (sequencer) and verifier (indexer).
|
||||
//!
|
||||
//! This crate is the reference LEZ-to-LEZ adapter: it re-derives each delivery
|
||||
//! byte-for-byte from a peer LEZ zone's finalized blocks, valid only because the
|
||||
//! peer runs identical LEZ code. A non-LEZ peer needs a separate adapter with its
|
||||
//! own block-reading, emission-extraction, delivery-building, and trust model; a
|
||||
//! shared trait is best lifted from that first real adapter, not from this one.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@ -10,10 +16,12 @@ use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, InboxConfig, Instruction, ZoneId, inbox_config_account_id,
|
||||
inbox_seen_shard_account_id,
|
||||
};
|
||||
use lee::program::Program;
|
||||
use lee_core::{
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The cross-zone emission fields a watcher or verifier reads off a source
|
||||
/// transaction, common to every emitter program.
|
||||
@ -24,6 +32,47 @@ pub struct Emission {
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A cross-zone builtin a zone deploys at genesis via `DeployProgram`.
|
||||
///
|
||||
/// Keeps the cross-zone programs out of the production builtin set. A zone lists
|
||||
/// the ones it uses (a sender needs the outbox and its emitter, a receiver the
|
||||
/// inbox and its targets); [`CrossZoneProgram::ALL`] deploys the whole set.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CrossZoneProgram {
|
||||
Outbox,
|
||||
Inbox,
|
||||
PingSender,
|
||||
PingReceiver,
|
||||
BridgeLock,
|
||||
WrappedToken,
|
||||
}
|
||||
|
||||
impl CrossZoneProgram {
|
||||
/// Every cross-zone builtin, for a zone that participates in all flows.
|
||||
pub const ALL: [Self; 6] = [
|
||||
Self::Outbox,
|
||||
Self::Inbox,
|
||||
Self::PingSender,
|
||||
Self::PingReceiver,
|
||||
Self::BridgeLock,
|
||||
Self::WrappedToken,
|
||||
];
|
||||
|
||||
/// Resolves to the builtin program to register in genesis state.
|
||||
#[must_use]
|
||||
pub const fn program(self) -> Program {
|
||||
match self {
|
||||
Self::Outbox => programs::cross_zone_outbox(),
|
||||
Self::Inbox => programs::cross_zone_inbox(),
|
||||
Self::PingSender => programs::ping_sender(),
|
||||
Self::PingReceiver => programs::ping_receiver(),
|
||||
Self::BridgeLock => programs::bridge_lock(),
|
||||
Self::WrappedToken => programs::wrapped_token(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a program may only be invoked by sequencer-origin transactions.
|
||||
///
|
||||
/// The cross-zone inbox is injected solely by the watcher; a user-submitted call
|
||||
@ -187,3 +236,42 @@ pub fn build_holding_account(holder: AccountId, amount: u128) -> (AccountId, Acc
|
||||
};
|
||||
(holder, account)
|
||||
}
|
||||
|
||||
/// Resolves a list of [`CrossZoneProgram`] selectors to the builtin programs to
|
||||
/// register in genesis state.
|
||||
#[must_use]
|
||||
pub fn deployed_programs(programs: &[CrossZoneProgram]) -> Vec<Program> {
|
||||
programs.iter().map(|p| p.program()).collect()
|
||||
}
|
||||
|
||||
/// The wrapped-token config account, pinning the cross-zone inbox as the
|
||||
/// authorized minter without importing the inbox id into the guest. Fixed for
|
||||
/// every zone, part of the cross-zone genesis setup.
|
||||
fn wrapped_token_config_account() -> (AccountId, Account) {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
(
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
Account {
|
||||
program_owner: wrapped_token_id,
|
||||
data: wrapped_token_core::minter_bytes(programs::cross_zone_inbox().id())
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("minter id fits in account data"),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The receiving-side cross-zone genesis accounts a zone seeds.
|
||||
///
|
||||
/// The wrapped-token config and this zone's inbox config, for a zone that
|
||||
/// receives (one with `cross_zone` config). Bridge-lock holdings are seeded
|
||||
/// separately via [`build_holding_account`], since they belong to the locking
|
||||
/// (source) side and must not depend on receiving config.
|
||||
#[must_use]
|
||||
pub fn genesis_accounts(self_zone: ZoneId, config: &CrossZoneConfig) -> Vec<(AccountId, Account)> {
|
||||
vec![
|
||||
wrapped_token_config_account(),
|
||||
build_inbox_config_account(self_zone, config),
|
||||
]
|
||||
}
|
||||
|
||||
@ -6,9 +6,9 @@ use common::{
|
||||
block::{BedrockStatus, Block, BlockHeader},
|
||||
transaction::{LeeTransaction, clock_invocation},
|
||||
};
|
||||
use lee::{Account, AccountId, GENESIS_BLOCK_ID, V03State};
|
||||
use lee::{Account, AccountId, GENESIS_BLOCK_ID, V03State, program::Program};
|
||||
use lee_core::BlockId;
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
use logos_blockchain_core::header::HeaderId;
|
||||
use logos_blockchain_zone_sdk::Slot;
|
||||
use storage::indexer::RocksDBIO;
|
||||
@ -45,19 +45,28 @@ 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: Vec<(AccountId, Account)>) -> Result<Self> {
|
||||
pub fn open_db(
|
||||
location: &Path,
|
||||
genesis_programs: Vec<Program>,
|
||||
genesis_accounts: Vec<(AccountId, Account)>,
|
||||
) -> Result<Self> {
|
||||
#[cfg(not(feature = "testnet"))]
|
||||
let mut initial_state = testnet_initial_state::initial_state();
|
||||
let base = testnet_initial_state::initial_state();
|
||||
|
||||
#[cfg(feature = "testnet")]
|
||||
let mut initial_state = testnet_initial_state::initial_state_testnet();
|
||||
let base = testnet_initial_state::initial_state_testnet();
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Extend with this zone's cross-zone genesis (programs + accounts) through
|
||||
// the state constructor, matching how the sequencer builds genesis so the
|
||||
// replayed states stay byte-identical.
|
||||
let initial_state = base
|
||||
.with_programs(genesis_programs)
|
||||
.with_public_accounts(genesis_accounts);
|
||||
// Same fingerprint the sequencer logs; a mismatch means a divergent deploy set.
|
||||
info!(
|
||||
"Genesis fingerprint: {}",
|
||||
hex::encode(initial_state.genesis_fingerprint())
|
||||
);
|
||||
let dbio = RocksDBIO::open_or_create(location, &initial_state)?;
|
||||
|
||||
let current_state = dbio.final_state()?;
|
||||
@ -400,7 +409,7 @@ mod stall_reason_tests {
|
||||
#[tokio::test]
|
||||
async fn stall_reason_roundtrips_and_clears() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
assert!(store.get_stall_reason().expect("get").is_none());
|
||||
|
||||
@ -447,7 +456,7 @@ mod tests {
|
||||
fn correct_startup() {
|
||||
let home = tempdir().unwrap();
|
||||
|
||||
let storage = IndexerStore::open_db(home.as_ref(), Vec::new()).unwrap();
|
||||
let storage = IndexerStore::open_db(home.as_ref(), Vec::new(), Vec::new()).unwrap();
|
||||
|
||||
let final_id = storage.get_last_block_id().unwrap();
|
||||
|
||||
@ -457,7 +466,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn accept_block_applies_transfers_and_advances_tip() {
|
||||
let home = tempdir().unwrap();
|
||||
let store = IndexerStore::open_db(home.as_ref(), Vec::new()).unwrap();
|
||||
let store = IndexerStore::open_db(home.as_ref(), Vec::new(), Vec::new()).unwrap();
|
||||
|
||||
let initial_accounts = initial_pub_accounts_private_keys();
|
||||
let from = initial_accounts[0].account_id;
|
||||
@ -499,7 +508,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn account_state_at_block_reflects_history() {
|
||||
let home = tempdir().unwrap();
|
||||
let store = IndexerStore::open_db(home.as_ref(), Vec::new()).unwrap();
|
||||
let store = IndexerStore::open_db(home.as_ref(), Vec::new(), Vec::new()).unwrap();
|
||||
|
||||
let initial_accounts = initial_pub_accounts_private_keys();
|
||||
let from = initial_accounts[0].account_id;
|
||||
@ -565,7 +574,7 @@ mod accept_tests {
|
||||
#[tokio::test]
|
||||
async fn non_genesis_first_block_parks_with_unexpected_id() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let block = valid_hash_block(2, HashType([0_u8; 32]));
|
||||
let outcome = store
|
||||
@ -588,7 +597,7 @@ mod accept_tests {
|
||||
#[tokio::test]
|
||||
async fn hash_mismatch_parks() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let mut block = valid_hash_block(1, HashType([0_u8; 32]));
|
||||
block.header.timestamp = 999; // invalidates the stored hash
|
||||
@ -606,7 +615,7 @@ mod accept_tests {
|
||||
#[tokio::test]
|
||||
async fn second_break_bumps_orphan_count_and_keeps_first() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let first = valid_hash_block(2, HashType([0_u8; 32]));
|
||||
store
|
||||
@ -627,7 +636,7 @@ mod accept_tests {
|
||||
#[tokio::test]
|
||||
async fn deserialize_break_records_stall_without_header() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
store
|
||||
.record_stall(
|
||||
@ -645,7 +654,7 @@ mod accept_tests {
|
||||
#[tokio::test]
|
||||
async fn parks_then_recovers_on_valid_continuation() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
// Genesis (block 1, clock-only) applies and advances the tip.
|
||||
let genesis = produce_dummy_block(1, None, vec![]);
|
||||
@ -693,7 +702,7 @@ mod accept_tests {
|
||||
#[tokio::test]
|
||||
async fn accept_block_records_tip_inscription_slot() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
assert_eq!(store.get_tip_slot().expect("get"), None);
|
||||
|
||||
@ -735,7 +744,7 @@ mod accept_tests {
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let accounts = initial_pub_accounts_private_keys();
|
||||
let from = accounts[0].account_id;
|
||||
@ -785,7 +794,7 @@ mod accept_tests {
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let accounts = initial_pub_accounts_private_keys();
|
||||
let from = accounts[0].account_id;
|
||||
@ -845,7 +854,7 @@ mod accept_tests {
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let accounts = initial_pub_accounts_private_keys();
|
||||
let from = accounts[0].account_id;
|
||||
@ -882,7 +891,7 @@ mod accept_tests {
|
||||
|
||||
// The #605 restart: reopening past the boundary must work.
|
||||
drop(store);
|
||||
let reopened = IndexerStore::open_db(dir.path(), Vec::new()).expect("reopen");
|
||||
let reopened = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("reopen");
|
||||
assert_eq!(reopened.last_block().unwrap(), Some(101));
|
||||
}
|
||||
|
||||
@ -891,7 +900,7 @@ mod accept_tests {
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new()).expect("open store");
|
||||
let store = IndexerStore::open_db(dir.path(), Vec::new(), Vec::new()).expect("open store");
|
||||
|
||||
let accounts = initial_pub_accounts_private_keys();
|
||||
let from = accounts[0].account_id;
|
||||
|
||||
@ -321,6 +321,7 @@ mod tests {
|
||||
allow_chain_reset: false,
|
||||
cross_zone: None,
|
||||
bridge_lock_holdings: Vec::new(),
|
||||
deploy_programs: Vec::new(),
|
||||
};
|
||||
IndexerCore::open(config, dir).expect("open core")
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ use std::{fs::File, io::BufReader, path::Path, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::config::BasicAuth;
|
||||
use cross_zone::CrossZoneProgram;
|
||||
use cross_zone_inbox_core::CrossZoneConfig;
|
||||
use humantime_serde;
|
||||
use lee::AccountId;
|
||||
@ -37,6 +38,12 @@ pub struct IndexerConfig {
|
||||
/// Defaults to `false`: on mismatch the indexer refuses to start.
|
||||
#[serde(default)]
|
||||
pub allow_chain_reset: bool,
|
||||
/// Cross-zone builtins to deploy at genesis, mirroring the sequencer's
|
||||
/// `DeployProgram` actions so the replayed genesis is identical. This set
|
||||
/// must match the sequencer's; compare the genesis fingerprint both nodes
|
||||
/// log at startup to confirm.
|
||||
#[serde(default)]
|
||||
pub deploy_programs: Vec<CrossZoneProgram>,
|
||||
}
|
||||
|
||||
/// A genesis-funded bridge-lock holder balance, configured identically on the
|
||||
|
||||
@ -93,22 +93,18 @@ impl IndexerCore {
|
||||
);
|
||||
let zone_indexer = ZoneIndexer::new(config.channel_id, node.clone());
|
||||
|
||||
// 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();
|
||||
// Cross-zone programs, mirroring the sequencer's DeployProgram set.
|
||||
let genesis_programs = cross_zone::deployed_programs(&config.deploy_programs);
|
||||
// Bridge-lock holdings (source side) always; inbox + wrapped-token config only when
|
||||
// cross_zone is set.
|
||||
let mut genesis_accounts: Vec<_> = config
|
||||
.bridge_lock_holdings
|
||||
.iter()
|
||||
.map(|holding| cross_zone::build_holding_account(holding.holder, holding.amount))
|
||||
.collect();
|
||||
if let Some(cross_zone) = config.cross_zone.as_ref() {
|
||||
let self_zone: [u8; 32] = *config.channel_id.as_ref();
|
||||
genesis_seed.push(cross_zone::build_inbox_config_account(
|
||||
self_zone, cross_zone,
|
||||
));
|
||||
}
|
||||
for holding in &config.bridge_lock_holdings {
|
||||
genesis_seed.push(cross_zone::build_holding_account(
|
||||
holding.holder,
|
||||
holding.amount,
|
||||
));
|
||||
genesis_accounts.extend(cross_zone::genesis_accounts(self_zone, cross_zone));
|
||||
}
|
||||
|
||||
// Option B verifier: re-derives each cross-zone dispatch from the peer's
|
||||
@ -117,7 +113,7 @@ impl IndexerCore {
|
||||
|
||||
Ok(Self {
|
||||
zone_indexer: Arc::new(zone_indexer),
|
||||
store: IndexerStore::open_db(&home, genesis_seed)?,
|
||||
store: IndexerStore::open_db(&home, genesis_programs, genesis_accounts)?,
|
||||
node,
|
||||
config,
|
||||
status: Arc::new(ArcSwap::from_pointee(IndexerSyncStatus::starting())),
|
||||
|
||||
@ -8,9 +8,10 @@ use std::{
|
||||
use anyhow::Result;
|
||||
use bytesize::ByteSize;
|
||||
use common::config::BasicAuth;
|
||||
pub use cross_zone::CrossZoneProgram;
|
||||
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer};
|
||||
use humantime_serde;
|
||||
use lee::AccountId;
|
||||
use lee::{AccountId, Balance};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
@ -21,15 +22,22 @@ use url::Url;
|
||||
pub enum GenesisAction {
|
||||
SupplyAccount {
|
||||
account_id: AccountId,
|
||||
balance: u128,
|
||||
balance: Balance,
|
||||
},
|
||||
SupplyBridgeAccount {
|
||||
balance: u128,
|
||||
balance: Balance,
|
||||
},
|
||||
/// Seeds a bridge-lock holder's initial bridgeable balance into genesis state.
|
||||
SupplyBridgeLockHolding {
|
||||
holder: AccountId,
|
||||
amount: u128,
|
||||
amount: Balance,
|
||||
},
|
||||
/// Registers a cross-zone builtin at genesis, keeping the cross-zone programs
|
||||
/// out of the production builtin set. A zone lists the ones it uses. This set
|
||||
/// must match the indexer's `deploy_programs`, or the two genesis states
|
||||
/// diverge; compare the genesis fingerprint both nodes log at startup.
|
||||
DeployProgram {
|
||||
program: CrossZoneProgram,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -632,10 +632,30 @@ fn replay_unfulfilled_deposit_events(
|
||||
/// committed to the genesis block so external observers can replay them.
|
||||
fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec<LeeTransaction>) {
|
||||
#[cfg(not(feature = "testnet"))]
|
||||
let mut state = testnet_initial_state::initial_state();
|
||||
let base = testnet_initial_state::initial_state();
|
||||
|
||||
#[cfg(feature = "testnet")]
|
||||
let mut state = testnet_initial_state::initial_state_testnet();
|
||||
let base = testnet_initial_state::initial_state_testnet();
|
||||
|
||||
// Cross-zone programs this zone declares; the indexer reproduces the set from config.
|
||||
let mut state = base.with_programs(deployed_programs(&config.genesis));
|
||||
// Bridge-lock holdings belong to the source side, seeded regardless of receiving config.
|
||||
let holdings: Vec<_> = bridge_lock_holdings(&config.genesis)
|
||||
.into_iter()
|
||||
.map(|(holder, amount)| cross_zone::build_holding_account(holder, amount))
|
||||
.collect();
|
||||
state = state.with_public_accounts(holdings);
|
||||
// Receiving-side accounts (inbox + wrapped-token config), only when cross_zone is set.
|
||||
if let Some(cross_zone) = &config.cross_zone {
|
||||
let self_zone = *config.bedrock_config.channel_id.as_ref();
|
||||
state = state.with_public_accounts(cross_zone::genesis_accounts(self_zone, cross_zone));
|
||||
}
|
||||
|
||||
// Fingerprint the directly-seeded state, before genesis txs, so it matches the indexer's.
|
||||
info!(
|
||||
"Genesis fingerprint: {}",
|
||||
hex::encode(state.genesis_fingerprint())
|
||||
);
|
||||
|
||||
let genesis_txs = config
|
||||
.genesis
|
||||
@ -650,8 +670,11 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec<LeeTrans
|
||||
GenesisAction::SupplyBridgeAccount { balance } => {
|
||||
Some(build_supply_bridge_account_genesis_transaction(*balance))
|
||||
}
|
||||
// Force-inserted below: bridge_lock has no mint transaction.
|
||||
GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
// Seeded/registered directly (accounts via `cross_zone::genesis_accounts`,
|
||||
// programs via `with_programs`), not by a genesis tx.
|
||||
GenesisAction::SupplyBridgeLockHolding { .. } | GenesisAction::DeployProgram { .. } => {
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(std::iter::once(clock_invocation(0)))
|
||||
.inspect(|tx| {
|
||||
@ -662,26 +685,37 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec<LeeTrans
|
||||
.map(LeeTransaction::Public)
|
||||
.collect();
|
||||
|
||||
// Seed bridge-lock holder balances directly: they are not produced by any tx.
|
||||
for action in &config.genesis {
|
||||
if let GenesisAction::SupplyBridgeLockHolding { holder, amount } = action {
|
||||
let (holder_id, account) = cross_zone::build_holding_account(*holder, *amount);
|
||||
state.insert_genesis_account(holder_id, account);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed this zone's cross-zone inbox config so the inbox guest can authorize
|
||||
// inbound peer messages (zone-specific config, not produced by any tx).
|
||||
if let Some(cross_zone) = &config.cross_zone {
|
||||
let self_zone = *config.bedrock_config.channel_id.as_ref();
|
||||
let (config_id, config_account) =
|
||||
cross_zone::build_inbox_config_account(self_zone, cross_zone);
|
||||
state.insert_genesis_account(config_id, config_account);
|
||||
}
|
||||
|
||||
(state, genesis_txs)
|
||||
}
|
||||
|
||||
/// Bridge-lock holder balances configured for this zone's genesis.
|
||||
fn bridge_lock_holdings(genesis: &[GenesisAction]) -> Vec<(lee::AccountId, u128)> {
|
||||
genesis
|
||||
.iter()
|
||||
.filter_map(|action| match action {
|
||||
GenesisAction::SupplyBridgeLockHolding { holder, amount } => Some((*holder, *amount)),
|
||||
GenesisAction::SupplyAccount { .. }
|
||||
| GenesisAction::SupplyBridgeAccount { .. }
|
||||
| GenesisAction::DeployProgram { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The cross-zone builtins this zone deploys at genesis, from its `DeployProgram`
|
||||
/// actions.
|
||||
fn deployed_programs(genesis: &[GenesisAction]) -> Vec<lee::program::Program> {
|
||||
let selectors: Vec<_> = genesis
|
||||
.iter()
|
||||
.filter_map(|action| match action {
|
||||
GenesisAction::DeployProgram { program } => Some(*program),
|
||||
GenesisAction::SupplyAccount { .. }
|
||||
| GenesisAction::SupplyBridgeAccount { .. }
|
||||
| GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
cross_zone::deployed_programs(&selectors)
|
||||
}
|
||||
|
||||
/// Whether a program may only be invoked by sequencer-origin transactions.
|
||||
///
|
||||
/// The cross-zone inbox is injected solely by the watcher; a user-submitted call
|
||||
|
||||
@ -10,7 +10,6 @@ lee.workspace = true
|
||||
lee_core.workspace = true
|
||||
system_accounts.workspace = true
|
||||
programs.workspace = true
|
||||
wrapped_token_core.workspace = true
|
||||
|
||||
serde.workspace = true
|
||||
|
||||
|
||||
@ -258,30 +258,9 @@ fn initial_public_accounts() -> HashMap<AccountId, Account> {
|
||||
.into_iter()
|
||||
.map(|clock_id| (clock_id, system_accounts::clock_account())),
|
||||
)
|
||||
.chain(std::iter::once(wrapped_token_config_account()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The wrapped-token config account.
|
||||
///
|
||||
/// Seeded so the `wrapped_token` guest can pin its authorized minter (the
|
||||
/// cross-zone inbox) without importing the inbox id. Fixed for every zone, so it
|
||||
/// lives in the shared initial state.
|
||||
fn wrapped_token_config_account() -> (AccountId, Account) {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
(
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
Account {
|
||||
program_owner: wrapped_token_id,
|
||||
data: wrapped_token_core::minter_bytes(programs::cross_zone_inbox().id())
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("minter id fits in account data"),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn initial_programs() -> Vec<Program> {
|
||||
vec![
|
||||
programs::authenticated_transfer(),
|
||||
@ -292,12 +271,6 @@ fn initial_programs() -> Vec<Program> {
|
||||
programs::vault(),
|
||||
programs::faucet(),
|
||||
programs::bridge(),
|
||||
programs::cross_zone_outbox(),
|
||||
programs::cross_zone_inbox(),
|
||||
programs::ping_sender(),
|
||||
programs::ping_receiver(),
|
||||
programs::bridge_lock(),
|
||||
programs::wrapped_token(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@ -6,7 +6,9 @@ use indexer_service::{ChannelId, ClientConfig, IndexerConfig};
|
||||
use key_protocol::key_management::{KeyChain, secret_holders::SeedHolder};
|
||||
use lee::{AccountId, PrivateKey, PublicKey};
|
||||
use lee_core::Identifier;
|
||||
use sequencer_core::config::{BedrockConfig, CrossZoneConfig, GenesisAction, SequencerConfig};
|
||||
use sequencer_core::config::{
|
||||
BedrockConfig, CrossZoneConfig, CrossZoneProgram, GenesisAction, SequencerConfig,
|
||||
};
|
||||
use url::Url;
|
||||
use wallet::config::WalletConfig;
|
||||
|
||||
@ -208,6 +210,7 @@ pub fn indexer_config(
|
||||
bedrock_addr: SocketAddr,
|
||||
channel_id: ChannelId,
|
||||
cross_zone: Option<CrossZoneConfig>,
|
||||
deploy_programs: Vec<CrossZoneProgram>,
|
||||
) -> Result<IndexerConfig> {
|
||||
Ok(IndexerConfig {
|
||||
consensus_info_polling_interval: Duration::from_secs(1),
|
||||
@ -220,9 +223,26 @@ pub fn indexer_config(
|
||||
cross_zone,
|
||||
bridge_lock_holdings: Vec::new(),
|
||||
allow_chain_reset: false,
|
||||
deploy_programs,
|
||||
})
|
||||
}
|
||||
|
||||
/// All cross-zone builtins as `DeployProgram` genesis actions, for a demo zone's
|
||||
/// sequencer config.
|
||||
#[must_use]
|
||||
pub fn cross_zone_deploy_actions() -> Vec<GenesisAction> {
|
||||
CrossZoneProgram::ALL
|
||||
.into_iter()
|
||||
.map(|program| GenesisAction::DeployProgram { program })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// All cross-zone builtins, for a demo zone's indexer `deploy_programs`.
|
||||
#[must_use]
|
||||
pub fn all_cross_zone_programs() -> Vec<CrossZoneProgram> {
|
||||
CrossZoneProgram::ALL.to_vec()
|
||||
}
|
||||
|
||||
pub fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result<Url> {
|
||||
// Convert 0.0.0.0 to 127.0.0.1 for client connections
|
||||
// When binding to port 0, the server binds to 0.0.0.0:<random_port>
|
||||
@ -255,3 +275,58 @@ pub fn bedrock_channel_id_b() -> ChannelId {
|
||||
.unwrap_or_else(|_| unreachable!());
|
||||
ChannelId::from(channel_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A demo zone declares its cross-zone deploy set twice: as the sequencer's
|
||||
/// `DeployProgram` genesis actions and as the indexer's `deploy_programs`. Pin
|
||||
/// the two helpers to the same genesis fingerprint, and check the fingerprint
|
||||
/// actually detects a divergent set and ignores registration order, since that
|
||||
/// is what makes it a usable drift guard.
|
||||
#[test]
|
||||
fn cross_zone_demo_genesis_matches_across_sequencer_and_indexer() {
|
||||
let seq_selectors: Vec<CrossZoneProgram> = cross_zone_deploy_actions()
|
||||
.into_iter()
|
||||
.filter_map(|action| match action {
|
||||
GenesisAction::DeployProgram { program } => Some(program),
|
||||
GenesisAction::SupplyAccount { .. }
|
||||
| GenesisAction::SupplyBridgeAccount { .. }
|
||||
| GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
let idx_selectors = all_cross_zone_programs();
|
||||
|
||||
let fingerprint = |selectors: &[CrossZoneProgram]| {
|
||||
lee::V03State::new()
|
||||
.with_programs(selectors.iter().map(|p| p.program()))
|
||||
.genesis_fingerprint()
|
||||
};
|
||||
|
||||
// Matched configs agree.
|
||||
assert_eq!(
|
||||
fingerprint(&seq_selectors),
|
||||
fingerprint(&idx_selectors),
|
||||
"sequencer DeployProgram set and indexer deploy_programs must agree"
|
||||
);
|
||||
|
||||
// A dropped program on one side changes the fingerprint.
|
||||
let mut drifted = idx_selectors.clone();
|
||||
drifted.pop().expect("deploy set is non-empty");
|
||||
assert_ne!(
|
||||
fingerprint(&seq_selectors),
|
||||
fingerprint(&drifted),
|
||||
"genesis fingerprint must detect a divergent deploy set"
|
||||
);
|
||||
|
||||
// Registration order does not change the fingerprint.
|
||||
let mut reversed = idx_selectors.clone();
|
||||
reversed.reverse();
|
||||
assert_eq!(
|
||||
fingerprint(&idx_selectors),
|
||||
fingerprint(&reversed),
|
||||
"genesis fingerprint must be independent of program order"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,7 +337,7 @@ impl TestContextBuilder {
|
||||
|
||||
let indexer_components = if enable_indexer {
|
||||
let (indexer_handle, temp_indexer_dir) =
|
||||
setup_indexer(bedrock_addr, config::bedrock_channel_id(), None)
|
||||
setup_indexer(bedrock_addr, config::bedrock_channel_id(), None, Vec::new())
|
||||
.await
|
||||
.context("Failed to setup Indexer")?;
|
||||
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr())
|
||||
|
||||
@ -116,6 +116,7 @@ pub async fn setup_indexer(
|
||||
bedrock_addr: SocketAddr,
|
||||
channel_id: ChannelId,
|
||||
cross_zone: Option<sequencer_core::config::CrossZoneConfig>,
|
||||
deploy_programs: Vec<sequencer_core::config::CrossZoneProgram>,
|
||||
) -> Result<(IndexerHandle, TempDir)> {
|
||||
let temp_indexer_dir =
|
||||
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
|
||||
@ -125,8 +126,9 @@ pub async fn setup_indexer(
|
||||
temp_indexer_dir.path().display()
|
||||
);
|
||||
|
||||
let indexer_config = config::indexer_config(bedrock_addr, channel_id, cross_zone)
|
||||
.context("Failed to create Indexer config")?;
|
||||
let indexer_config =
|
||||
config::indexer_config(bedrock_addr, channel_id, cross_zone, deploy_programs)
|
||||
.context("Failed to create Indexer config")?;
|
||||
|
||||
indexer_service::run_server(
|
||||
indexer_config,
|
||||
|
||||
@ -289,14 +289,24 @@ 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) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
config::cross_zone_deploy_actions(),
|
||||
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,
|
||||
config::cross_zone_deploy_actions(),
|
||||
channel_b,
|
||||
Some(cross_zone_b),
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
zone_a: ZoneRuntime {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user