mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-07-22 15:49:41 +00:00
refactor(cross-zone): seed cross-zone config via genesis transactions
This commit is contained in:
parent
a806ebb94a
commit
f4a5d85c65
@ -20,6 +20,7 @@ use lee_core::{
|
||||
account::{Account, AccountId, Balance},
|
||||
program::ProgramId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
/// The cross-zone emission fields a watcher or verifier reads off a source
|
||||
/// transaction, common to every emitter program.
|
||||
@ -142,38 +143,35 @@ pub fn build_dispatch_from_emission(
|
||||
build_inbox_dispatch_tx(programs::cross_zone_inbox().id(), &msg, target_ids)
|
||||
}
|
||||
|
||||
/// Builds the inbox config account a zone seeds into genesis state.
|
||||
///
|
||||
/// Lets the inbox guest authorize inbound peer messages. The sequencer and
|
||||
/// indexer seed the same account from the same config, keeping their replayed
|
||||
/// state consistent.
|
||||
#[must_use]
|
||||
pub fn build_inbox_config_account(
|
||||
self_zone: ZoneId,
|
||||
cross_zone: &CrossZoneConfig,
|
||||
) -> (AccountId, Account) {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
|
||||
/// The inbox config a zone derives from its cross-zone config: the per-peer target
|
||||
/// allowlists plus its own zone id.
|
||||
fn inbox_config(self_zone: ZoneId, cross_zone: &CrossZoneConfig) -> InboxConfig {
|
||||
let mut allowed_targets = BTreeMap::new();
|
||||
for peer in &cross_zone.peers {
|
||||
allowed_targets.insert(peer.channel_id, peer.allowed_targets.clone());
|
||||
}
|
||||
let config = InboxConfig {
|
||||
InboxConfig {
|
||||
self_zone,
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let account = Account {
|
||||
program_owner: inbox_id,
|
||||
balance: 0,
|
||||
data: config
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("inbox config fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
};
|
||||
(inbox_config_account_id(inbox_id), account)
|
||||
/// The genesis transaction that initializes this zone's inbox config PDA.
|
||||
///
|
||||
/// Lets the inbox guest authorize inbound peer messages; replaying it seeds the
|
||||
/// same account on every node, keeping their state consistent.
|
||||
#[must_use]
|
||||
pub fn build_inbox_init_config_tx(
|
||||
self_zone: ZoneId,
|
||||
cross_zone: &CrossZoneConfig,
|
||||
) -> lee::PublicTransaction {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
genesis_public_tx(
|
||||
inbox_id,
|
||||
vec![inbox_config_account_id(inbox_id)],
|
||||
Instruction::InitConfig(inbox_config(self_zone, cross_zone)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the genesis holding account funding a holder's bridgeable balance.
|
||||
@ -194,34 +192,32 @@ pub fn build_holding_account(holder: AccountId, amount: Balance) -> (AccountId,
|
||||
(holder, account)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
/// The genesis transaction that pins the cross-zone inbox as the wrapped-token
|
||||
/// minter, without importing the inbox id into the guest.
|
||||
#[must_use]
|
||||
pub fn build_wrapped_token_init_config_tx() -> lee::PublicTransaction {
|
||||
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()
|
||||
genesis_public_tx(
|
||||
wrapped_token_id,
|
||||
vec![wrapped_token_core::config_account_id(wrapped_token_id)],
|
||||
wrapped_token_core::Instruction::InitConfig {
|
||||
minter: programs::cross_zone_inbox().id(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// 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),
|
||||
]
|
||||
/// Builds an unsigned, sequencer-origin genesis transaction invoking `instruction`
|
||||
/// on `program_id` over `account_ids`.
|
||||
fn genesis_public_tx<I: Serialize>(
|
||||
program_id: ProgramId,
|
||||
account_ids: Vec<AccountId>,
|
||||
instruction: I,
|
||||
) -> lee::PublicTransaction {
|
||||
let message =
|
||||
lee::public_transaction::Message::try_new(program_id, account_ids, vec![], instruction)
|
||||
.expect("genesis instruction must serialize");
|
||||
lee::PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
)
|
||||
}
|
||||
|
||||
@ -186,7 +186,9 @@ impl CrossZoneVerifier {
|
||||
&public_tx.message().instruction_data,
|
||||
) {
|
||||
Ok(InboxInstruction::Dispatch(msg)) => Some(msg),
|
||||
Err(_) => None,
|
||||
// Only a dispatch carries a cross-zone message to re-derive; a genesis
|
||||
// `InitConfig` is not verifier-relevant.
|
||||
Ok(InboxInstruction::InitConfig(_)) | Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -93,18 +93,15 @@ impl IndexerCore {
|
||||
);
|
||||
let zone_indexer = ZoneIndexer::new(config.channel_id, node.clone());
|
||||
|
||||
// Cross-zone programs are base builtins, so none are seeded here.
|
||||
// Bridge-lock holdings (source side) always; inbox + wrapped-token config only when
|
||||
// cross_zone is set.
|
||||
let mut genesis_accounts: Vec<_> = config
|
||||
// Cross-zone programs are base builtins, and their config accounts are
|
||||
// reconstructed by replaying the genesis block's InitConfig transactions;
|
||||
// neither is seeded here. Only bridge-lock holdings (source side), not
|
||||
// produced by any transaction, are still seeded directly.
|
||||
let 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_accounts.extend(cross_zone::genesis_accounts(self_zone, cross_zone));
|
||||
}
|
||||
|
||||
// Option B verifier: re-derives each cross-zone dispatch from the peer's
|
||||
// finalized blocks. `None` when cross-zone messaging is disabled.
|
||||
|
||||
@ -36,7 +36,10 @@ fn main() {
|
||||
let WrappedInstruction::Mint {
|
||||
amount: mint_amount,
|
||||
..
|
||||
} = decode_mint(&payload);
|
||||
} = decode_mint(&payload)
|
||||
else {
|
||||
panic!("bridge_lock payload must be a wrapped-token mint");
|
||||
};
|
||||
assert_eq!(
|
||||
mint_amount, amount,
|
||||
"locked amount must equal the wrapped mint amount"
|
||||
|
||||
@ -116,6 +116,10 @@ impl SeenShard {
|
||||
pub enum Instruction {
|
||||
/// Delivers a finalized peer message to its target program.
|
||||
Dispatch(CrossZoneMessage),
|
||||
/// Initializes the inbox config account at genesis. Written once, into a
|
||||
/// default (unclaimed) config PDA; the guest refuses a non-default pre-state,
|
||||
/// so it cannot be re-run to overwrite the allowlists.
|
||||
InitConfig(InboxConfig),
|
||||
}
|
||||
|
||||
/// Content-addressed replay key for a delivered message.
|
||||
@ -142,7 +146,14 @@ pub fn message_key(src_zone: &ZoneId, src_block_id: u64, src_tx_index: u32) -> M
|
||||
/// The config account holding the allowlists.
|
||||
#[must_use]
|
||||
pub fn inbox_config_account_id(inbox_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(&inbox_id, &PdaSeed::new(INBOX_CONFIG_SEED))
|
||||
AccountId::for_public_pda(&inbox_id, &inbox_config_seed())
|
||||
}
|
||||
|
||||
/// Seed of the config PDA, exposed so the guest can claim the account when it
|
||||
/// initializes the config at genesis.
|
||||
#[must_use]
|
||||
pub const fn inbox_config_seed() -> PdaSeed {
|
||||
PdaSeed::new(INBOX_CONFIG_SEED)
|
||||
}
|
||||
|
||||
/// The seen-set shard for the `(src_zone, epoch)` the message falls in.
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
use cross_zone_inbox_core::{
|
||||
InboxConfig, Instruction, SeenShard, inbox_config_account_id, inbox_seen_shard_account_id,
|
||||
inbox_seen_shard_seed, message_key,
|
||||
CrossZoneMessage, InboxConfig, Instruction, SeenShard, inbox_config_account_id,
|
||||
inbox_config_seed, inbox_seen_shard_account_id, inbox_seen_shard_seed, message_key,
|
||||
};
|
||||
use lee_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{
|
||||
AccountPostState, ChainedCall, Claim, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_lee_inputs,
|
||||
},
|
||||
};
|
||||
|
||||
fn unchanged(pre: &AccountWithMetadata) -> AccountPostState {
|
||||
@ -27,8 +30,32 @@ fn main() {
|
||||
"Inbox is only invoked as a top-level sequencer-origin transaction"
|
||||
);
|
||||
|
||||
let Instruction::Dispatch(msg) = instruction;
|
||||
match instruction {
|
||||
Instruction::Dispatch(msg) => dispatch(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_words,
|
||||
&msg,
|
||||
),
|
||||
Instruction::InitConfig(config) => init_config(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_words,
|
||||
&config,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delivers a finalized peer message to its target program, no-op on replay.
|
||||
fn dispatch(
|
||||
self_program_id: ProgramId,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_words: Vec<u32>,
|
||||
msg: &CrossZoneMessage,
|
||||
) {
|
||||
assert!(
|
||||
msg.l1_inclusion_witness.is_none(),
|
||||
"l1_inclusion_witness must be None in v1"
|
||||
@ -123,3 +150,47 @@ fn main() {
|
||||
.with_chained_calls(chained_calls)
|
||||
.write();
|
||||
}
|
||||
|
||||
/// Writes the inbox config (peer + target allowlists) into the config PDA exactly
|
||||
/// once at genesis.
|
||||
fn init_config(
|
||||
self_program_id: ProgramId,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_words: Vec<u32>,
|
||||
config: &InboxConfig,
|
||||
) {
|
||||
// pre_states: [config PDA].
|
||||
let [config_meta] = <[AccountWithMetadata; 1]>::try_from(pre_states)
|
||||
.expect("InitConfig requires the config account");
|
||||
assert_eq!(
|
||||
config_meta.account_id,
|
||||
inbox_config_account_id(self_program_id),
|
||||
"account must be the inbox config PDA"
|
||||
);
|
||||
// Init-once: a default pre-state cannot be re-run to overwrite the allowlists,
|
||||
// and `new_claimed_if_default` alone would not stop the owning program from
|
||||
// rewriting its own config data on a later call.
|
||||
assert_eq!(
|
||||
config_meta.account,
|
||||
Account::default(),
|
||||
"inbox config already initialized"
|
||||
);
|
||||
|
||||
let mut config_account = config_meta.account.clone();
|
||||
config_account.data = config
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("inbox config fits in account data");
|
||||
let config_post =
|
||||
AccountPostState::new_claimed_if_default(config_account, Claim::Pda(inbox_config_seed()));
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![config_meta],
|
||||
vec![config_post],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
|
||||
@ -19,6 +19,12 @@ pub enum Instruction {
|
||||
/// Required accounts (2): the wrapped-token config PDA, then the recipient's
|
||||
/// holding PDA.
|
||||
Mint { recipient: [u8; 32], amount: u128 },
|
||||
/// Pins `minter` (the cross-zone inbox) as the authorized minter, written once
|
||||
/// into a default config PDA at genesis. The guest refuses a non-default
|
||||
/// pre-state, so it cannot be re-run to hijack the minter.
|
||||
///
|
||||
/// Required accounts (1): the wrapped-token config PDA.
|
||||
InitConfig { minter: ProgramId },
|
||||
}
|
||||
|
||||
/// PDA holding the authorized minter program id (the cross-zone inbox), seeded at
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use lee_core::{
|
||||
account::AccountWithMetadata,
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
};
|
||||
use wrapped_token_core::{
|
||||
Instruction, balance_bytes, config_account_id, holding_account_id, holding_seed, read_balance,
|
||||
read_minter,
|
||||
Instruction, balance_bytes, config_account_id, config_seed, holding_account_id, holding_seed,
|
||||
minter_bytes, read_balance, read_minter,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@ -18,8 +18,33 @@ fn main() {
|
||||
instruction_words,
|
||||
) = read_lee_inputs::<Instruction>();
|
||||
|
||||
let Instruction::Mint { recipient, amount } = instruction;
|
||||
match instruction {
|
||||
Instruction::Mint { recipient, amount } => mint(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_words,
|
||||
recipient,
|
||||
amount,
|
||||
),
|
||||
Instruction::InitConfig { minter } => init_config(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_words,
|
||||
minter,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn mint(
|
||||
self_program_id: lee_core::program::ProgramId,
|
||||
caller_program_id: Option<lee_core::program::ProgramId>,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_words: Vec<u32>,
|
||||
recipient: [u8; 32],
|
||||
amount: u128,
|
||||
) {
|
||||
// pre_states: [config PDA, recipient holding PDA].
|
||||
let [config, holding] = <[AccountWithMetadata; 2]>::try_from(pre_states)
|
||||
.expect("Mint requires the config and recipient holding accounts");
|
||||
@ -68,3 +93,51 @@ fn main() {
|
||||
)
|
||||
.write();
|
||||
}
|
||||
|
||||
/// Writes the authorized minter into the config PDA exactly once at genesis.
|
||||
fn init_config(
|
||||
self_program_id: lee_core::program::ProgramId,
|
||||
caller_program_id: Option<lee_core::program::ProgramId>,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_words: Vec<u32>,
|
||||
minter: lee_core::program::ProgramId,
|
||||
) {
|
||||
assert!(
|
||||
caller_program_id.is_none(),
|
||||
"InitConfig is a top-level genesis transaction"
|
||||
);
|
||||
|
||||
// pre_states: [config PDA].
|
||||
let [config] = <[AccountWithMetadata; 1]>::try_from(pre_states)
|
||||
.expect("InitConfig requires the config account");
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
config_account_id(self_program_id),
|
||||
"account must be the wrapped-token config PDA"
|
||||
);
|
||||
// Init-once: a default pre-state cannot be re-run to overwrite the minter, and
|
||||
// `new_claimed_if_default` alone would not stop the owning program from
|
||||
// rewriting its own config data on a later call.
|
||||
assert_eq!(
|
||||
config.account,
|
||||
Account::default(),
|
||||
"wrapped-token config already initialized"
|
||||
);
|
||||
|
||||
let mut config_account = config.account.clone();
|
||||
config_account.data = minter_bytes(minter)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("minter id fits in account data");
|
||||
let config_post =
|
||||
AccountPostState::new_claimed_if_default(config_account, Claim::Pda(config_seed()));
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![config],
|
||||
vec![config_post],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
|
||||
@ -637,19 +637,12 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec<LeeTrans
|
||||
#[cfg(feature = "testnet")]
|
||||
let base = testnet_initial_state::initial_state_testnet();
|
||||
|
||||
// Cross-zone programs are base builtins, so nothing extra is registered here.
|
||||
let mut state = base;
|
||||
// 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));
|
||||
}
|
||||
let mut state = base.with_public_accounts(holdings);
|
||||
|
||||
// Fingerprint the directly-seeded state, before genesis txs, so it matches the indexer's.
|
||||
info!(
|
||||
@ -657,22 +650,37 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec<LeeTrans
|
||||
hex::encode(state.genesis_fingerprint())
|
||||
);
|
||||
|
||||
let genesis_txs = config
|
||||
.genesis
|
||||
.iter()
|
||||
.filter_map(|genesis_tx| match genesis_tx {
|
||||
GenesisAction::SupplyAccount {
|
||||
account_id,
|
||||
balance,
|
||||
} => Some(build_supply_account_genesis_transaction(
|
||||
account_id, *balance,
|
||||
)),
|
||||
GenesisAction::SupplyBridgeAccount { balance } => {
|
||||
Some(build_supply_bridge_account_genesis_transaction(*balance))
|
||||
}
|
||||
// Seeded directly (accounts via `cross_zone::genesis_accounts`), not by a genesis tx.
|
||||
GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
})
|
||||
// Config txs seed the config accounts by transaction, so every node
|
||||
// reconstructs them by replaying the genesis block. The wrapped-token minter is
|
||||
// initialized on every zone (wrapped_token is a builtin), since its InitConfig
|
||||
// is user-callable and a config PDA left default would be claimable by anyone as
|
||||
// the first initializer (a minter hijack). The inbox allowlist is initialized
|
||||
// only on receiving zones; the inbox is sequencer-only, so its default config
|
||||
// PDA is not user-claimable, merely unused until the zone receives.
|
||||
let wrapped_token_config_tx = std::iter::once(cross_zone::build_wrapped_token_init_config_tx());
|
||||
let inbox_config_tx = config.cross_zone.as_ref().map(|cross_zone| {
|
||||
let self_zone = *config.bedrock_config.channel_id.as_ref();
|
||||
cross_zone::build_inbox_init_config_tx(self_zone, cross_zone)
|
||||
});
|
||||
let supply_txs = config.genesis.iter().filter_map(|action| match action {
|
||||
GenesisAction::SupplyAccount {
|
||||
account_id,
|
||||
balance,
|
||||
} => Some(build_supply_account_genesis_transaction(
|
||||
account_id, *balance,
|
||||
)),
|
||||
GenesisAction::SupplyBridgeAccount { balance } => {
|
||||
Some(build_supply_bridge_account_genesis_transaction(*balance))
|
||||
}
|
||||
// Seeded directly (holdings via `build_holding_account`), not by a genesis tx.
|
||||
GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
});
|
||||
|
||||
// Applied in order with the mandatory clock last, then committed to the genesis
|
||||
// block so external observers can replay them. Every genesis tx is public.
|
||||
let genesis_txs = wrapped_token_config_tx
|
||||
.chain(inbox_config_tx)
|
||||
.chain(supply_txs)
|
||||
.chain(std::iter::once(clock_invocation(0)))
|
||||
.inspect(|tx| {
|
||||
state
|
||||
|
||||
@ -289,24 +289,14 @@ 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, 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 state = Arc::new(AppState {
|
||||
zone_a: ZoneRuntime {
|
||||
@ -446,7 +436,9 @@ async fn poll_finality(state: Arc<AppState>) {
|
||||
fn decode_inbox_text(instruction_data: &[u32]) -> Option<String> {
|
||||
let instruction: Instruction =
|
||||
risc0_zkvm::serde::from_slice::<Instruction, u32>(instruction_data).ok()?;
|
||||
let Instruction::Dispatch(message) = instruction;
|
||||
let Instruction::Dispatch(message) = instruction else {
|
||||
return None;
|
||||
};
|
||||
decode_payload(&message.payload)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user