Skip to content
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
caea86a
Create router contract
ManuelBilbao Oct 9, 2025
2284147
Add router feature to contracts
ManuelBilbao Oct 10, 2025
d3d9985
Add router to deployer
ManuelBilbao Oct 10, 2025
a023188
Update contracts
ManuelBilbao Oct 14, 2025
109e234
Send messages across L2s
ManuelBilbao Oct 14, 2025
508438a
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 14, 2025
f104f5c
Fix merge
ManuelBilbao Oct 14, 2025
018a63d
Burn gas in src L2
ManuelBilbao Oct 15, 2025
dfdb8ea
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 15, 2025
a57fead
Change contracts flow
ManuelBilbao Oct 16, 2025
21ad3aa
Add contracts docs
ManuelBilbao Oct 16, 2025
1901b1d
Improve documentation
ManuelBilbao Oct 16, 2025
1a99f05
Revert changes
ManuelBilbao Oct 16, 2025
c220c3e
Add docs
ManuelBilbao Oct 16, 2025
d937327
Move router contract to main path
ManuelBilbao Oct 17, 2025
19df3e8
Refactor deployer
ManuelBilbao Oct 17, 2025
81fbdd9
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 17, 2025
78011eb
Fix event signature
ManuelBilbao Oct 17, 2025
0bc4f04
Update genesis L2
ManuelBilbao Oct 17, 2025
b102114
Add L2toL2Messages to store
ManuelBilbao Oct 17, 2025
f127bdc
Implement L2toL2Messages in block fetcher
ManuelBilbao Oct 17, 2025
d55eed5
Implement get L2->L2 messages for SQL store
ManuelBilbao Oct 17, 2025
12bc29c
Remove unwraps
ManuelBilbao Oct 17, 2025
29cac02
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 17, 2025
b5b3a76
Fix sql
ManuelBilbao Oct 17, 2025
f3f083a
Fix wording
ManuelBilbao Oct 17, 2025
41b8adc
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 20, 2025
7c826ed
Add preliminar shared bridge documentation
ilitteri Oct 21, 2025
af2558d
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 22, 2025
0b35959
Apply format suggestion
ManuelBilbao Oct 22, 2025
17e3f1f
Apply format suggestion
ManuelBilbao Oct 22, 2025
02d20de
Rename shared bridge variable
ManuelBilbao Oct 22, 2025
54010cf
Rename message_hashes field
ManuelBilbao Oct 22, 2025
36463e3
Fix missing rename
ManuelBilbao Oct 22, 2025
c806c25
Fix tests
ManuelBilbao Oct 22, 2025
8f95d80
Unlink router and OCP
ManuelBilbao Oct 22, 2025
089e3df
Fix deposit when no calldata
ManuelBilbao Oct 23, 2025
486e5aa
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 23, 2025
df9c9d8
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 23, 2025
425dbd7
Merge branch 'main' into shared_bridge_router
ManuelBilbao Oct 24, 2025
9be647d
Fix: store L2->L2 txs when sealing batch
ManuelBilbao Oct 24, 2025
5329629
Fix based commitment transaction
ManuelBilbao Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/ethrex/build_l2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ pub fn download_script() {
&Path::new("../../crates/l2/contracts/src/l1/CommonBridge.sol"),
"CommonBridge",
),
(
&Path::new("../../crates/l2/contracts/src/l1/Router.sol"),
"Router",
),
];
for (path, name) in l1_contracts {
compile_contract_to_bytecode(
Expand Down Expand Up @@ -171,6 +175,7 @@ fn write_empty_bytecode_files(output_contracts_path: &Path) {
"SP1Verifier",
"OnChainProposer",
"CommonBridge",
"Router",
"CommonBridgeL2",
"L2ToL1Messenger",
"UpgradeableSystemContract",
Expand Down
10 changes: 6 additions & 4 deletions cmd/ethrex/l2/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ pub enum Command {
#[arg(
long,
value_parser = parse_private_key,
env = "SEQUENCER_PRIVATE_KEY",
help = "The private key of the sequencer",
env = "SEQUENCER_PRIVATE_KEY",
help = "The private key of the sequencer",
help_heading = "Sequencer account options",
group = "sequencer_signing",
)]
Expand Down Expand Up @@ -452,7 +452,7 @@ impl Command {
.await?;

// Get withdrawal hashes
let message_hashes = state_diff
let l1_message_hashes = state_diff
.l1_messages
.iter()
.map(get_l1_message_hash)
Expand Down Expand Up @@ -491,7 +491,9 @@ impl Command {
last_block: new_block.number,
state_root: new_block.state_root,
privileged_transactions_hash: H256::zero(),
message_hashes,
l1_message_hashes,
// TODO: Check if this is restorable
l2_to_l2_messages: vec![],
blobs_bundle: BlobsBundle::empty(),
commit_tx: None,
verify_tx: None,
Expand Down
146 changes: 132 additions & 14 deletions cmd/ethrex/l2/deployer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,23 @@ pub struct DeployerOptions {
help = "The L1 address of the L2 native token (e.g., USDC, USDT, DAI, etc. Use address(0) for ETH)"
)]
pub native_token_l1_address: Address,
#[arg(
long = "router.deploy",
default_value = "false",
env = "ETHREX_SHARED_BRIDGE_DEPLOY_ROUTER",
help_heading = "Deployer options",
help = "If set, the deployer will deploy the shared bridge router contract. Default to false",
conflicts_with = "router"
)]
pub deploy_router: bool,
#[arg(
long = "router.address",
value_name = "ADDRESS",
env = "ETHREX_SHARED_BRIDGE_ROUTER_ADDRESS",
help_heading = "Deployer options",
help = "The address of the shared bridge router"
)]
pub router: Option<Address>,
}

impl Default for DeployerOptions {
Expand Down Expand Up @@ -416,6 +433,8 @@ impl Default for DeployerOptions {
inclusion_max_wait: 3000,
use_compiled_genesis: true,
native_token_l1_address: H160::zero(),
router: None,
deploy_router: false,
}
}
}
Expand Down Expand Up @@ -459,6 +478,13 @@ pub enum DeployerError {
Genesis,
}

/// Bytecode of the Router contract.
/// This is generated by the [build script](./build.rs).
const ROUTER_BYTECODE: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/contracts/solc_out/Router.bytecode"
));

/// Bytecode of the OnChainProposer contract.
/// This is generated by the [build script](./build.rs).
const ON_CHAIN_PROPOSER_BYTECODE: &[u8] = include_bytes!(concat!(
Expand Down Expand Up @@ -500,7 +526,9 @@ const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,ad
const INITIALIZE_BRIDGE_ADDRESS_SIGNATURE: &str = "initializeBridgeAddress(address)";
const TRANSFER_OWNERSHIP_SIGNATURE: &str = "transferOwnership(address)";
const ACCEPT_OWNERSHIP_SIGNATURE: &str = "acceptOwnership()";
const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,address)";
const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,address,address)";
const ROUTER_INITIALIZER_SIGNATURE: &str = "initialize(address)";
const ROUTER_REGISTER_SIGNATURE: &str = "register(uint256,address)";

// deposit(uint256 _amount, address _l2Recipient)
const NATIVE_TOKEN_DEPOSIT_SIGNATURE: &str = "deposit(uint256,address)";
Expand All @@ -517,6 +545,7 @@ pub struct ContractAddresses {
pub tdx_verifier_address: Address,
pub sequencer_registry_address: Address,
pub aligned_aggregator_address: Address,
pub router: Option<Address>,
}

pub async fn deploy_l1_contracts(
Expand All @@ -535,9 +564,32 @@ pub async fn deploy_l1_contracts(
Some(opts.maximum_allowed_max_fee_per_blob_gas),
)?;

let genesis: Genesis = if opts.use_compiled_genesis {
serde_json::from_str(LOCAL_DEVNETL2_GENESIS_CONTENTS).map_err(|_| DeployerError::Genesis)?
} else {
read_genesis_file(
opts.genesis_l2_path
.to_str()
.ok_or(DeployerError::FailedToGetStringFromPath)?,
)
};

let contract_addresses = deploy_contracts(&eth_client, &opts, &signer).await?;

initialize_contracts(contract_addresses, &eth_client, &opts, &signer).await?;
initialize_contracts(contract_addresses, &eth_client, &opts, &genesis, &signer).await?;

if contract_addresses.router.is_some() {
let _ = register_chain(
&eth_client,
contract_addresses,
genesis.config.chain_id,
&signer,
)
.await
.inspect_err(|err| {
warn!(%err, "Could not register chain in shared bridge router");
});
}

if opts.deposit_rich {
if opts.native_token_l1_address != Address::zero() {
Expand Down Expand Up @@ -573,8 +625,6 @@ async fn deploy_contracts(
) -> Result<ContractAddresses, DeployerError> {
trace!("Deploying contracts");

info!("Deploying OnChainProposer");

let salt = if opts.randomize_contract_deployment {
H256::random().as_bytes().to_vec()
} else {
Expand All @@ -584,6 +634,31 @@ async fn deploy_contracts(
.to_vec()
};

let deployed_router = if opts.deploy_router {
info!("Deploying Router");

let bytecode = ROUTER_BYTECODE.to_vec();
if bytecode.is_empty() {
return Err(DeployerError::BytecodeNotFound);
}

let router_deployment =
deploy_with_proxy_from_bytecode(deployer, eth_client, &bytecode, &salt).await?;
info!(
"Router deployed:\n Proxy -> address={:#x}, tx_hash={:#x}\n Impl -> address={:#x}, tx_hash={:#x}",
router_deployment.proxy_address,
router_deployment.proxy_tx_hash,
router_deployment.implementation_address,
router_deployment.implementation_tx_hash,
);

Some(router_deployment.proxy_address)
} else {
None
};

info!("Deploying OnChainProposer");

trace!("Attempting to deploy OnChainProposer contract");
let bytecode = if opts.deploy_based_contracts {
ON_CHAIN_PROPOSER_BASED_BYTECODE.to_vec()
Expand Down Expand Up @@ -696,6 +771,7 @@ async fn deploy_contracts(
tdx_verifier_address,
sequencer_registry_address: sequencer_registry_deployment.proxy_address,
aligned_aggregator_address: opts.aligned_aggregator_address,
router: opts.router.or(deployed_router),
})
}

Expand Down Expand Up @@ -755,22 +831,13 @@ async fn initialize_contracts(
contract_addresses: ContractAddresses,
eth_client: &EthClient,
opts: &DeployerOptions,
genesis: &Genesis,
initializer: &Signer,
) -> Result<(), DeployerError> {
trace!("Initializing contracts");

trace!(committer_l1_address = %opts.committer_l1_address, "Using committer L1 address for OnChainProposer initialization");

let genesis: Genesis = if opts.use_compiled_genesis {
serde_json::from_str(LOCAL_DEVNETL2_GENESIS_CONTENTS).map_err(|_| DeployerError::Genesis)?
} else {
read_genesis_file(
opts.genesis_l2_path
.to_str()
.ok_or(DeployerError::FailedToGetStringFromPath)?,
)
};

let sp1_vk = read_vk(&opts.sp1_vk_path);
let risc0_vk = read_vk(&opts.risc0_vk_path);

Expand Down Expand Up @@ -834,6 +901,22 @@ async fn initialize_contracts(
};
info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "SequencerRegistry initialized");
} else {
if let Some(router) = contract_addresses.router
&& opts.deploy_router
{
let calldata_values = vec![Value::Address(deployer_address)];
let router_initialization_calldata =
encode_calldata(ROUTER_INITIALIZER_SIGNATURE, &calldata_values)?;
let initialize_tx_hash = initialize_contract(
router,
router_initialization_calldata,
initializer,
eth_client,
)
.await?;
info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Router initialized");
}

// Initialize only OnChainProposer without Based config
let calldata_values = vec![
Value::Bool(opts.validium),
Expand Down Expand Up @@ -936,6 +1019,7 @@ async fn initialize_contracts(
Value::Address(contract_addresses.on_chain_proposer_address),
Value::Uint(opts.inclusion_max_wait.into()),
Value::Address(opts.native_token_l1_address),
Value::Address(contract_addresses.router.unwrap_or_default()),
];
let bridge_initialization_calldata =
encode_calldata(BRIDGE_INITIALIZER_SIGNATURE, &calldata_values)?;
Expand All @@ -954,6 +1038,35 @@ async fn initialize_contracts(
Ok(())
}

async fn register_chain(
eth_client: &EthClient,
contract_addresses: ContractAddresses,
chain_id: u64,
deployer: &Signer,
) -> Result<(), DeployerError> {
let params = vec![
Value::Uint(U256::from(chain_id)),
Value::Address(contract_addresses.bridge_address),
];

ethrex_l2_sdk::call_contract(
eth_client,
deployer,
contract_addresses
.router
.ok_or(DeployerError::InternalError(
"Router address is None. This is a bug.".to_string(),
))?,
ROUTER_REGISTER_SIGNATURE,
params,
)
.await?;

info!(chain_id, "Chain registered");

Ok(())
}

async fn make_deposits(
bridge: Address,
eth_client: &EthClient,
Expand Down Expand Up @@ -1199,6 +1312,11 @@ fn write_contract_addresses_to_env(
"ETHREX_NATIVE_TOKEN_L1_ADDRESS={:#x}",
native_token_l1_address
)?;
writeln!(
writer,
"ETHREX_SHARED_BRIDGE_ROUTER_ADDRESS={:#x}",
contract_addresses.router.unwrap_or_default()
)?;
trace!(?env_file_path, "Contract addresses written to .env");
Ok(())
}
1 change: 1 addition & 0 deletions crates/common/types/l2.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod batch;
pub mod fee_config;
pub mod l2_to_l2_message;
8 changes: 6 additions & 2 deletions crates/common/types/l2/batch.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{H256, types::BlobsBundle};
use crate::{
H256,
types::{BlobsBundle, l2_to_l2_message::L2toL2Message},
};
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize, Debug, Default)]
Expand All @@ -8,7 +11,8 @@ pub struct Batch {
pub last_block: u64,
pub state_root: H256,
pub privileged_transactions_hash: H256,
pub message_hashes: Vec<H256>,
pub l1_message_hashes: Vec<H256>,
pub l2_to_l2_messages: Vec<L2toL2Message>,
pub blobs_bundle: BlobsBundle,
pub commit_tx: Option<H256>,
pub verify_tx: Option<H256>,
Expand Down
46 changes: 46 additions & 0 deletions crates/common/types/l2/l2_to_l2_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use bytes::Bytes;
use ethereum_types::{Address, U256};
use ethrex_rlp::{decode::RLPDecode, structs::Decoder};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
/// Represents a message from the L2 to another L2
pub struct L2toL2Message {
/// Chain id of the destination chain
pub chain_id: U256,
/// Address that originated the transaction
pub from: Address,
/// Address of the recipient in the destination chain
pub to: Address,
/// Amount of ETH to send to the recipient
pub value: U256,
/// Gas limit for the transaction execution in the destination chain
pub gas_limit: U256,
/// Calldata for the transaction in the destination chain
pub data: Bytes,
}

impl RLPDecode for L2toL2Message {
fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), ethrex_rlp::error::RLPDecodeError> {
let decoder = Decoder::new(rlp)?;

let (chain_id, decoder) = decoder.decode_field("chain_id")?;
let (from, decoder) = decoder.decode_field("from")?;
let (to, decoder) = decoder.decode_field("to")?;
let (value, decoder) = decoder.decode_field("value")?;
let (gas_limit, decoder) = decoder.decode_field("gas_limit")?;
let (data, decoder) = decoder.decode_field("data")?;

Ok((
L2toL2Message {
chain_id,
from,
to,
value,
gas_limit,
data,
},
decoder.finish()?,
))
}
}
Loading