Cross-Chain Messaging
Send L1-to-L2 and L2-to-L1 messages using aztec-ethereum.
Runnable Examples
examples/l1_to_l2_message.rs— full L1 → L2 send + consume flow.examples/l2_to_l1_message.rs— L2 → L1 emit + consume flow.examples/fee_juice_claim.rs— the canonical claim-based bridge, reusing the messaging primitives.
L1 → L2
use aztec_rs::l1_client::{self, EthClient, L1ContractAddresses};
use aztec_rs::messaging;
use aztec_rs::cross_chain::wait_for_l1_to_l2_message_ready;
// Resolve L1 portal addresses from the Aztec node.
let info = wallet.pxe().node().get_node_info().await?;
let l1 = L1ContractAddresses::from_json(&info.l1_contract_addresses)
.ok_or_else(|| aztec_rs::Error::InvalidData("missing L1 addresses".into()))?;
let eth = EthClient::new(ðereum_url);
let (secret, secret_hash) = messaging::generate_claim_secret();
let content = aztec_rs::types::Fr::random();
let sent = l1_client::send_l1_to_l2_message(
ð,
&l1.inbox,
&recipient_address,
info.rollup_version,
&content,
&secret_hash,
).await?;
// Block until the message is consumable on L2.
wait_for_l1_to_l2_message_ready(
wallet.pxe().node(),
&sent.msg_hash,
std::time::Duration::from_secs(30),
).await?;
// Now call the L2 contract function that consumes the message,
// passing `secret` + `content` as arguments.
L2 → L1
L2-emitted messages are produced inside a contract function. Consumption on L1 uses the Outbox:
- Send an L2 tx whose body emits the message.
- Wait for the block to be proven (
Wallet::wait_for_tx_proven). - On L1, call the Outbox’s consume function with the produced inclusion proof.
See examples/l2_to_l1_message.rs for the full flow; the L1-side call is handled by EthClient::send_transaction against the Outbox address from L1ContractAddresses.
Message Identity
L1Actor { address, chain_id }— the L1 sender.L2Actor { address, version }— the L2 recipient.L1ToL2Message { sender, recipient, content, secret_hash }— bound by its hash.
Tampering with any field changes the hash and breaks consumption.
Edge Cases
- Not yet ready:
is_l1_to_l2_message_readyreturnsfalseuntil the archiver has seen the L1 tx; poll rather than retryconsume. - Re-org on L1: readiness is advisory until the block reaches the archiver’s confirmation depth.
- Double-consume: the nullifier tree marks a consumed message as spent; retrying will revert at simulation.
Full Runnable Example
Source: examples/l1_to_l2_message.rs.
For the reverse direction see examples/l2_to_l1_message.rs.
//! Send an L1 to L2 message and consume it on L2.
#![allow(clippy::print_stdout, clippy::wildcard_imports)]
mod common;
use common::*;
#[tokio::main]
async fn main() -> Result<(), aztec_rs::Error> {
let Some((wallet, owner)) = setup_wallet(TEST_ACCOUNT_0).await else {
return Err(aztec_rs::Error::InvalidData(format!(
"node not reachable at {}",
node_url()
)));
};
let node_info = wallet.pxe().node().get_node_info().await?;
let rollup_version = node_info.rollup_version;
let l1_addresses = L1ContractAddresses::from_json(&node_info.l1_contract_addresses)
.ok_or_else(|| aztec_rs::Error::InvalidData("missing L1 addresses".to_owned()))?;
let eth_client = EthClient::new(ðereum_url());
let (test_address, test_artifact, _) =
deploy_contract(&wallet, load_test_contract_artifact(), vec![], owner).await?;
let (secret, secret_hash) = messaging::generate_claim_secret();
let content = Fr::random();
let sent = l1_client::send_l1_to_l2_message(
ð_client,
&l1_addresses.inbox,
&test_address,
rollup_version,
&content,
&secret_hash,
)
.await?;
let ready =
wait_for_l1_to_l2_message_ready_by_advancing(&wallet, owner, &sent.msg_hash, 30).await?;
if !ready {
return Err(aztec_rs::Error::Timeout(format!(
"L1-to-L2 message {} was not ready after advancing 30 L2 blocks",
sent.msg_hash
)));
}
let l1_sender = eth_client.get_account().await?;
let consume_hash = send_call(
&wallet,
build_call(
&test_artifact,
test_address,
"consume_message_from_arbitrary_sender_private",
vec![
AbiValue::Field(content),
AbiValue::Field(secret),
AbiValue::Field(eth_address_as_field(&parse_eth_address(&l1_sender))),
AbiValue::Field(sent.global_leaf_index),
],
),
owner,
)
.await?;
println!("Test contract: {test_address}");
println!("L1->L2 message: {}", sent.msg_hash);
println!("Leaf index: {}", sent.global_leaf_index);
println!("Consume tx hash: {consume_hash}");
Ok(())
}