Files
logos-execution-zone/integration_tests/tests/cross_zone_ping.rs
T
Marvin Jones b9152f58c0 refactor(lee): migrate ChainedCall/Message program reference from ProgramId to AccountId
Chained-call and public-transaction dispatch now address the target
program directly by AccountId instead of routing through ProgramId and
converting internally, closing the gap that blocked PDA-addressed
program invocation. The field is named program_account_id (not
account_id) to stay unambiguous next to the account_ids list it sits
beside in the same structs.

Execution/PDA-derivation logic that fundamentally needs the RISC0 image
id (self_program_id, caller_program_id, env::verify, PDA seed
derivation) stays ProgramId-typed, recovering it from the dispatched
AccountId via the existing bijection where needed.
2026-08-22 16:58:27 -04:00

168 lines
5.9 KiB
Rust

#![expect(
clippy::tests_outside_test_module,
reason = "top-level test functions are conventional for integration tests"
)]
//! End-to-end cross-zone round trip: a ping submitted on zone A is delivered by
//! zone B's watcher to `ping_receiver` on zone B, which records the payload.
//!
//! Two sequencers share one Bedrock node (no indexers): zone A publishes the
//! ping to Bedrock, zone B's watcher reads zone A's finalized blocks, injects the
//! inbox dispatch, and zone B's sequencer delivers it. This is the M3 milestone,
//! sequencer-trusted, with no indexer re-derivation (that is M4).
use std::time::Duration;
use anyhow::{Context as _, Result};
use common::transaction::LeeTransaction;
use cross_zone_outbox_core::outbox_pda;
use integration_tests::config::{self, SequencerPartialConfig};
use lee::{AccountId, PublicTransaction, public_transaction::Message};
use lee_core::program::ProgramId;
use ping_core::{
ReceiverInstruction, SenderInstruction, ping_record_pda, receiver_config_account_id,
sender_config_account_id,
};
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(480);
const PING_PAYLOAD: &[u8] = b"hello-cross-zone";
#[test]
async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
let partial = SequencerPartialConfig::default();
let channel_a = config::bedrock_channel_id();
let channel_b = config::bedrock_channel_id_b();
let zone_a: [u8; 32] = *channel_a.as_ref();
let zone_b: [u8; 32] = *channel_b.as_ref();
let receiver_id = programs::ping_receiver().id();
// Zone B watches zone A and allows delivery only to ping_receiver.
let cross_zone = CrossZoneConfig {
peers: vec![CrossZonePeer {
channel_id: zone_a,
allowed_routes: vec![CrossZoneRoute {
src_program_id: programs::ping_sender().id(),
target_program_id: receiver_id,
}],
expected_block_signing_pubkeys: Vec::new(),
}],
source_authority: None,
source_governance: None,
};
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_a,
})
.disable_wallet()
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_b,
})
.disable_wallet()
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_genesis(vec![])
.with_cross_zone(Some(cross_zone)),
)
.build()
.await?;
// Submit the ping on zone A, addressed to ping_receiver on zone B.
let ping = build_ping_tx(zone_b, receiver_id);
let seq_client_a = &ctx
.zone_default_sequencer_component(channel_a)
.sequencer_client;
let seq_client_b = &ctx
.zone_default_sequencer_component(channel_b)
.sequencer_client;
seq_client_a
.send_transaction(ping)
.await
.context("Failed to submit ping on zone A")?;
// Wait until zone B's sequencer records the delivered payload.
let record_id = ping_record_pda(receiver_id);
let delivered = wait_for_delivery(seq_client_b.clone(), record_id).await?;
assert_eq!(
delivered, PING_PAYLOAD,
"Zone B must record the payload delivered from zone A"
);
Ok(())
}
/// Builds a top-level `ping_sender` transaction that chains into the outbox to emit
/// a message carrying a `ping_receiver::Record` instruction for the target zone.
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
let outbox_id = programs::cross_zone_outbox().id();
let ordinal = 0;
// The payload is the ping_receiver instruction, serialized as risc0 words in
// little-endian bytes (the contract the inbox reverses when forwarding).
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
payload: PING_PAYLOAD.to_vec(),
})
.expect("serialize ping instruction");
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
let send = SenderInstruction::Send {
target_zone,
target_program_id: receiver_id,
target_accounts: vec![
receiver_config_account_id(receiver_id).into_value(),
ping_record_pda(receiver_id).into_value(),
],
payload,
ordinal,
};
let sender_id = programs::ping_sender().id();
let outbox_account = outbox_pda(outbox_id, sender_id, &target_zone, ordinal);
let message = Message::try_new(
sender_id.into(),
vec![sender_config_account_id(sender_id), outbox_account],
vec![],
send,
)
.expect("build ping message");
LeeTransaction::Public(PublicTransaction::new(
message,
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
))
}
/// Polls zone B's sequencer until the ping record PDA holds a payload.
async fn wait_for_delivery(client: SequencerClient, record_id: AccountId) -> Result<Vec<u8>> {
let wait = async {
loop {
let account = client.get_account(record_id).await?;
let data = account.data.into_inner();
if !data.is_empty() {
return Ok::<Vec<u8>, anyhow::Error>(data);
}
tokio::time::sleep(Duration::from_secs(3)).await;
}
};
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
.await
.context("Zone B did not record the cross-zone payload in time")?
}