Skip to content

Enable custom account in deploying L1-L2 messaging contract #758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 26, 2025
32 changes: 30 additions & 2 deletions crates/starknet-devnet-core/src/messaging/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ impl From<WalletError> for Error {
}
}

async fn assert_address_contains_any_code(
provider: &Provider<Http>,
address: Address,
) -> DevnetResult<()> {
let messaging_contract_code = provider.get_code(address, None).await.map_err(|e| {
Error::MessagingError(MessagingError::EthersError(format!(
"Failed retrieving contract code at address {address}: {e}"
)))
})?;

if messaging_contract_code.is_empty() {
return Err(Error::MessagingError(MessagingError::EthersError(format!(
"The specified address ({address:#x}) contains no contract"
))));
}

Ok(())
}

#[derive(Clone)]
/// Ethereum related configuration and types.
pub struct EthereumMessaging {
Expand All @@ -84,9 +103,12 @@ impl EthereumMessaging {
///
/// * `rpc_url` - The L1 node RPC URL.
/// * `contract_address` - The messaging contract address deployed on L1 node.
/// * `deployer_account_private_key` - The private key of the funded account on L1 node to
/// perform the role of signer.
pub async fn new(
rpc_url: &str,
contract_address: Option<&str>,
deployer_account_private_key: Option<&str>,
) -> DevnetResult<EthereumMessaging> {
let provider = Provider::<Http>::try_from(rpc_url).map_err(|e| {
Error::MessagingError(MessagingError::EthersError(format!(
Expand All @@ -96,15 +118,18 @@ impl EthereumMessaging {

let chain_id = provider.get_chainid().await?;

let private_key = ETH_ACCOUNT_DEFAULT.private_key;
let private_key = match deployer_account_private_key {
Some(private_key) => private_key,
None => ETH_ACCOUNT_DEFAULT.private_key,
};

let wallet: LocalWallet =
private_key.parse::<LocalWallet>()?.with_chain_id(chain_id.as_u32());

let provider_signer = SignerMiddleware::new(provider.clone(), wallet);

let mut ethereum = EthereumMessaging {
provider: Arc::new(provider),
provider: Arc::new(provider.clone()),
provider_signer: Arc::new(provider_signer),
messaging_contract_address: Address::zero(),
last_fetched_block: 0,
Expand All @@ -116,6 +141,9 @@ impl EthereumMessaging {
"Address {address} can't be parsed from string: {e}",
)))
})?;

assert_address_contains_any_code(&provider, ethereum.messaging_contract_address)
.await?;
} else {
let cancellation_delay_seconds: U256 = (60 * 60 * 24).into();
ethereum.messaging_contract_address =
Expand Down
7 changes: 6 additions & 1 deletion crates/starknet-devnet-core/src/messaging/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,19 @@ impl Starknet {
///
/// * `rpc_url` - The L1 node RPC URL.
/// * `contract_address` - The messaging contract address deployed on L1 node.
/// * `deployer_account_private_key` - The private key of the funded account on L1 node to
/// perform the role of signer.
pub async fn configure_messaging(
&mut self,
rpc_url: &str,
contract_address: Option<&str>,
deployer_account_private_key: Option<&str>,
) -> DevnetResult<String> {
tracing::trace!("Configuring messaging: {}", rpc_url);

self.messaging.configure_ethereum(EthereumMessaging::new(rpc_url, contract_address).await?);
self.messaging.configure_ethereum(
EthereumMessaging::new(rpc_url, contract_address, deployer_account_private_key).await?,
);

Ok(format!("0x{:x}", self.messaging.ethereum_ref()?.messaging_contract_address()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ pub(crate) async fn postman_load_impl(
data: PostmanLoadL1MessagingContract,
) -> StrictRpcResult {
let mut starknet = api.starknet.lock().await;
let messaging_contract_address =
starknet.configure_messaging(&data.network_url, data.address.as_deref()).await?;
let messaging_contract_address = starknet
.configure_messaging(
&data.network_url,
data.messaging_contract_address.as_deref(),
data.deployer_account_private_key.as_deref(),
)
.await?;

Ok(DevnetResponse::MessagingContractAddress(MessagingLoadAddress {
messaging_contract_address,
Expand Down
4 changes: 3 additions & 1 deletion crates/starknet-devnet-server/src/api/http/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ pub struct LoadPath {
#[cfg_attr(test, derive(Debug))]
pub struct PostmanLoadL1MessagingContract {
pub network_url: String,
pub address: Option<String>,
#[serde(alias = "address")]
pub messaging_contract_address: Option<String>,
pub deployer_account_private_key: Option<String>,
}

#[derive(Serialize)]
Expand Down
33 changes: 25 additions & 8 deletions tests/integration/common/background_anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use k256::ecdsa::SigningKey;
use reqwest::StatusCode;

use super::background_server::get_acquired_port;
use super::constants::{DEFAULT_ETH_ACCOUNT_PRIVATE_KEY, HOST};
use super::constants::{DEFAULT_ANVIL_MNEMONIC_PHRASE, DEFAULT_ETH_ACCOUNT_PRIVATE_KEY, HOST};
use super::errors::TestError;
use super::safe_child::SafeChild;

Expand All @@ -31,14 +31,19 @@ mod abigen {

impl BackgroundAnvil {
pub(crate) async fn spawn() -> Result<Self, TestError> {
BackgroundAnvil::spawn_with_additional_args(&[]).await
Self::spawn_with_additional_args(&[]).await
}

/// Spawns an instance at random port. Assumes CLI args in `args` don't contain `--port`.
pub(crate) async fn spawn_with_additional_args(args: &[&str]) -> Result<Self, TestError> {
pub(crate) async fn spawn_with_additional_args_and_custom_signer(
args: &[&str],
mnemonic_phrase: &str,
private_key: &str,
) -> Result<Self, TestError> {
let process = Command::new("anvil")
.arg("--port")
.arg("0")
.arg("--mnemonic")
.arg(mnemonic_phrase)
.arg("--silent")
.args(args)
.spawn()
Expand All @@ -58,7 +63,8 @@ impl BackgroundAnvil {
assert_eq!(anvil_block_rsp.status(), StatusCode::OK);
println!("Spawned background anvil at {url}");

let (provider, provider_signer) = setup_ethereum_provider(&url).await?;
let (provider, provider_signer) =
setup_ethereum_provider(&url, private_key).await?;

return Ok(Self { process: safe_process, url, provider, provider_signer });
}
Expand All @@ -69,6 +75,17 @@ impl BackgroundAnvil {
Err(TestError::AnvilNotStartable("Not responsive for too long".into()))
}

/// Spawns an instance at random port. Assumes CLI args in `args` don't contain `--port` or
/// mnemonic parameters. Uses the mnemonic phrase defined in constants.
pub(crate) async fn spawn_with_additional_args(args: &[&str]) -> Result<Self, TestError> {
Self::spawn_with_additional_args_and_custom_signer(
args,
DEFAULT_ANVIL_MNEMONIC_PHRASE,
DEFAULT_ETH_ACCOUNT_PRIVATE_KEY,
)
.await
}

pub async fn deploy_l1l2_contract(
&self,
messaging_address: Address,
Expand Down Expand Up @@ -164,18 +181,18 @@ impl BackgroundAnvil {

async fn setup_ethereum_provider(
rpc_url: &str,
private_key: &str,
) -> Result<
(Arc<Provider<Http>>, Arc<SignerMiddleware<Provider<Http>, Wallet<SigningKey>>>),
TestError,
> {
let provider = Provider::<Http>::try_from(rpc_url)
.map_err(|e| TestError::EthersError(format!("Can't parse L1 node URL: {rpc_url} ({e})")))
.map_err(|e| TestError::EthersError(e.to_string()))?;
.map_err(|e| TestError::EthersError(format!("Can't parse L1 node URL: {rpc_url} ({e})")))?;

let chain_id =
provider.get_chainid().await.map_err(|e| TestError::EthersError(e.to_string()))?;

let wallet: LocalWallet = DEFAULT_ETH_ACCOUNT_PRIVATE_KEY
let wallet = private_key
.parse::<LocalWallet>()
.map_err(|e| TestError::EthersError(e.to_string()))?
.with_chain_id(chain_id.as_u32());
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/common/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,8 @@ pub const INTEGRATION_SAFE_BLOCK: u64 = 64718;
pub const QUERY_VERSION_OFFSET: Felt =
Felt::from_raw([576460752142434320, 18446744073709551584, 17407, 18446744073700081665]);

pub const DEFAULT_ANVIL_MNEMONIC_PHRASE: &str =
"test test test test test test test test test test test junk";
/// Account at index 0 if DEFAULT_ANVIL_MNEMONIC_PHRASE used
pub const DEFAULT_ETH_ACCOUNT_PRIVATE_KEY: &str =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
135 changes: 133 additions & 2 deletions tests/integration/test_messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ use starknet_rs_signers::LocalWallet;
use crate::common::background_anvil::BackgroundAnvil;
use crate::common::background_devnet::BackgroundDevnet;
use crate::common::constants::{
CHAIN_ID, L1_HANDLER_SELECTOR, MESSAGING_L1_CONTRACT_ADDRESS, MESSAGING_L2_CONTRACT_ADDRESS,
MESSAGING_WHITELISTED_L1_CONTRACT,
CHAIN_ID, DEFAULT_ETH_ACCOUNT_PRIVATE_KEY, L1_HANDLER_SELECTOR, MESSAGING_L1_CONTRACT_ADDRESS,
MESSAGING_L2_CONTRACT_ADDRESS, MESSAGING_WHITELISTED_L1_CONTRACT,
};
use crate::common::errors::RpcError;
use crate::common::utils::{
Expand Down Expand Up @@ -347,6 +347,137 @@ async fn can_deploy_l1_messaging_contract() {
);
}

#[tokio::test]
async fn should_fail_on_loading_l1_messaging_contract_if_not_deployed() {
let anvil = BackgroundAnvil::spawn().await.unwrap();

let devnet = BackgroundDevnet::spawn().await.unwrap();

let error = devnet
.send_custom_rpc(
"devnet_postmanLoad",
json!({ "network_url": anvil.url, "messaging_contract_address": MESSAGING_L1_ADDRESS }),
)
.await
.unwrap_err();

assert_eq!(
error,
RpcError {
code: -1,
message: format!(
"Ethers error: The specified address ({MESSAGING_L1_ADDRESS}) contains no \
contract."
)
.into(),
data: None
}
);
}

#[tokio::test]
async fn can_load_an_already_deployed_l1_messaging_contract() {
let anvil = BackgroundAnvil::spawn().await.unwrap();

let devnet = BackgroundDevnet::spawn().await.unwrap();

let body = devnet
.send_custom_rpc("devnet_postmanLoad", json!({ "network_url": anvil.url }))
.await
.expect("deploy l1 messaging contract failed");

assert_eq!(
body.get("messaging_contract_address").unwrap().as_str().unwrap(),
MESSAGING_L1_ADDRESS
);

let second_devnet = BackgroundDevnet::spawn().await.unwrap();

for address_key in [
"messaging_contract_address", // preferred key
"address", // legacy key
] {
let load_result = second_devnet
.send_custom_rpc(
"devnet_postmanLoad",
json!({ "network_url": anvil.url, address_key: MESSAGING_L1_ADDRESS }),
)
.await
.unwrap();
assert_eq!(
load_result.get("messaging_contract_address").unwrap().as_str().unwrap(),
MESSAGING_L1_ADDRESS
);
}
}

const MNEMONIC_FROM_SEED_42: &str =
"pen brief eager pepper brass detect problem vital physical tent assume damp";
const ACCOUNT_0_PRIVATE_KEY_WITH_SEED_42: &str =
"0x3c741b302cb3ea9163539c7fc72d4e40264aa03cfb638d79860d86865c3d0db4";
const EXPECTED_MESSAGING_CONTRACT_ADDRESS_WITH_SEED_42: &str =
"0xca945eebf408d4a73e5d330fc8f6b55cd1e419ff";

#[tokio::test]
async fn setup_anvil_incorrect_eth_private_key() {
let anvil = BackgroundAnvil::spawn_with_additional_args_and_custom_signer(
&[],
MNEMONIC_FROM_SEED_42,
ACCOUNT_0_PRIVATE_KEY_WITH_SEED_42,
)
.await
.unwrap();

let (devnet, _, _) = setup_devnet(&["--account-class", "cairo1"]).await;

let body = devnet
.send_custom_rpc("devnet_postmanLoad", json!({ "network_url": anvil.url }))
.await
.unwrap_err();
assert_contains(&body.message, "CallGasCostMoreThanGasLimit");

let body = devnet
.send_custom_rpc(
"devnet_postmanLoad",
json!({
"network_url": anvil.url,
"deployer_account_private_key": DEFAULT_ETH_ACCOUNT_PRIVATE_KEY
}),
)
.await
.unwrap_err();
assert_contains(&body.message, "CallGasCostMoreThanGasLimit");
}

#[tokio::test]
async fn deploy_l1_messaging_contract_with_custom_key() {
let anvil = BackgroundAnvil::spawn_with_additional_args_and_custom_signer(
&[],
MNEMONIC_FROM_SEED_42,
ACCOUNT_0_PRIVATE_KEY_WITH_SEED_42,
)
.await
.unwrap();

let (devnet, _, _) = setup_devnet(&["--account-class", "cairo1"]).await;

let body = devnet
.send_custom_rpc(
"devnet_postmanLoad",
json!({
"network_url": anvil.url,
"deployer_account_private_key": ACCOUNT_0_PRIVATE_KEY_WITH_SEED_42
}),
)
.await
.expect("deploy l1 messaging contract failed");

assert_eq!(
body.get("messaging_contract_address").unwrap().as_str().unwrap(),
EXPECTED_MESSAGING_CONTRACT_ADDRESS_WITH_SEED_42
);
}

#[tokio::test]
async fn can_consume_from_l2() {
let (devnet, account, l1l2_contract_address) =
Expand Down
Loading