diff --git a/Cargo.toml b/Cargo.toml index d1b5453e..ae968a63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,13 @@ axum = "0.6.20" axum-middleware = { path = "crates/axum-middleware" } clap = { version = "4.4.7", features = ["derive"] } common = { path = "crates/common" } -ethers = { version = "2.0.10", features = ["abigen", "ws", "ipc", "rustls", "openssl"] } +ethers = { version = "2.0.10", features = [ + "abigen", + "ws", + "ipc", + "rustls", + "openssl", +] } ethers-throttle = { path = "crates/ethers-throttle" } eyre = "0.6.8" futures = "0.3.28" diff --git a/bin/configs/state_bridge.toml b/bin/configs/state_bridge.toml new file mode 100644 index 00000000..2a122da7 --- /dev/null +++ b/bin/configs/state_bridge.toml @@ -0,0 +1,33 @@ +# Address of the WorldIDIdentityManager contract on L1 +l1_world_id = "0x78eC127A3716D447F4575E9c834d452E397EE9E1" +l1_rpc_endpoint = "" +# Request per second limit for rpc endpoint +throttle = 5 +# Number of block confirmations before considering a `propagateRoot()` tx finalized +block_confirmations = 3 +# Optional max gas price to use when sending `propagateRoot()` txs +# max_gas_price = 3000000000 + + +# Optimism +[[state_bridge]] +# Address of the state bridge contract on L1 +l1_state_bridge = "0x39CcB3b670651a14da8b3835f42924f49C2C5986" +# Address of the WorldID contract on L2 +l2_world_id = "0x715161DB36C260e4bE7094773785A111D11bC5F9" +# RPC endpoint for L2 +l2_rpc_endpoint = "" +# Request per second limit for rpc endpoint +throttle = 5 +# Minimum time between `propagateRoot()` transactions +relaying_period_seconds = 1800 + + + +# Polygon +[[state_bridge]] +l1_state_bridge = "0x9e4984bdE7F17aB422Cc4254C08074b3c42160d1" +l2_world_id = "0xCDfDF72065493bDDb2131478c89C1D5482BD1dF6" +l2_rpc_endpoint = "" +throttle = 5 +relaying_period_seconds = 1800 diff --git a/bin/state_bridge_service.rs b/bin/state_bridge_service.rs index e62334da..8b898d58 100644 --- a/bin/state_bridge_service.rs +++ b/bin/state_bridge_service.rs @@ -1,29 +1,34 @@ use std::fs; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use clap::Parser; use common::tracing::{init_datadog_subscriber, init_subscriber}; -use ethers::abi::Address; +use ethers::middleware::gas_escalator::{ + Frequency, GasEscalatorMiddleware, GeometricGasPrice, +}; use ethers::prelude::{ - Http, LocalWallet, NonceManagerMiddleware, Provider, Signer, - SignerMiddleware, H160, + Http, LocalWallet, NonceManagerMiddleware, Provider, Signer, H160, }; use ethers::providers::Middleware; +use ethers::types::U256; +use ethers_throttle::ThrottledProvider; use futures::stream::FuturesUnordered; use futures::StreamExt; +use governor::Jitter; use opentelemetry::global::shutdown_tracer_provider; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize}; use tracing::Level; -use world_tree::abi::{IBridgedWorldID, IStateBridge}; +use url::Url; use world_tree::state_bridge::service::StateBridgeService; use world_tree::state_bridge::StateBridge; #[derive(Parser, Debug)] #[clap( name = "State Bridge Service", - about = "The state bridge service listens to root changes from the WorldIdIdentityManager and propagates them to each of the corresponding Layer 2s specified in the configuration file." + about = "The state bridge service listens to root changes from the `WorldIdIdentityManager` and propagates them to each of the corresponding Layer 2s specified in the configuration file." )] struct Opts { #[clap( @@ -32,52 +37,38 @@ struct Opts { help = "Path to the TOML state bridge service config file" )] config: PathBuf, - - #[clap(long, help = "Enable datadog backend for instrumentation")] + #[clap( + short, + long, + help = "Private key for account used to send `propagateRoot()` txs" + )] + private_key: String, + #[clap(short, long, help = "Enable datadog backend for instrumentation")] datadog: bool, } -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -struct BridgeConfig { - name: String, //TODO: do we need this where do we use it? - state_bridge_address: Address, - bridged_world_id_address: Address, - bridged_rpc_url: String, +#[derive(Deserialize, Serialize, Debug)] +struct Config { + l1_rpc_endpoint: String, + #[serde(deserialize_with = "deserialize_h160")] + l1_world_id: H160, + block_confirmations: usize, + state_bridge: Vec, + throttle: u32, + #[serde(deserialize_with = "deserialize_opt_u256", default)] + max_gas_price: Option, } -//TODO: lets update this to be a yaml file and then we can do something like the following: -// rpc_url: "" -// private_key: "" -// world_id_address: "" -// block_confirmations: "" -// state_bridges: -// optimism: -// state_bridge_address: "" -// bridged_world_id_address: "" -// bridged_rpc_url: "" -// relaying_period_seconds: 5 -// -// arbitrum: -// state_bridge_address: "" -// bridged_world_id_address: "" -// bridged_rpc_url: "" -// relaying_period_seconds: 5 - #[derive(Deserialize, Serialize, Debug, Clone)] -struct Config { - // RPC URL for the HTTP provider (World ID IdentityManager) - rpc_url: String, - // Private key to use for the middleware signer - private_key: String, - // `WorldIDIdentityManager` contract address - world_id_address: H160, - // List of `StateBridge` and `BridgedWorldID` pair addresses - bridge_configs: Vec, - // `propagateRoot()` call period time in seconds +struct StateBridgeConfig { + #[serde(deserialize_with = "deserialize_h160")] + l1_state_bridge: H160, + #[serde(deserialize_with = "deserialize_h160")] + l2_world_id: H160, + l2_rpc_endpoint: String, + #[serde(deserialize_with = "deserialize_duration_from_seconds")] relaying_period_seconds: Duration, - // Number of block confirmations required for the `propagateRoot` call on the `StateBridge` - // contract - block_confirmations: Option, + throttle: u32, } const SERVICE_NAME: &str = "state-bridge-service"; @@ -94,73 +85,37 @@ async fn main() -> eyre::Result<()> { init_subscriber(Level::INFO); } - spawn_state_bridge_service( - config.rpc_url, - config.private_key, - config.world_id_address, - config.bridge_configs, - config.relaying_period_seconds, - config.block_confirmations.unwrap_or(0), + let mut wallet = opts.private_key.parse::()?; + let l1_middleware = initialize_l1_middleware( + &config.l1_rpc_endpoint, + config.throttle, + wallet.address(), + config.max_gas_price, ) .await?; - shutdown_tracer_provider(); - - Ok(()) -} - -async fn spawn_state_bridge_service( - rpc_url: String, - private_key: String, - world_id_address: H160, - bridge_configs: Vec, - relaying_period: Duration, - block_confirmations: usize, -) -> eyre::Result<()> { - let provider = Provider::::try_from(rpc_url) - .expect("failed to initialize Http provider"); - - let chain_id = provider.get_chainid().await?.as_u64(); - - let wallet = private_key.parse::()?.with_chain_id(chain_id); - let wallet_address = wallet.address(); - - let signer_middleware = SignerMiddleware::new(provider, wallet); - let nonce_manager_middleware = - NonceManagerMiddleware::new(signer_middleware, wallet_address); - let middleware = Arc::new(nonce_manager_middleware); + let chain_id = l1_middleware.get_chainid().await?.as_u64(); + wallet = wallet.with_chain_id(chain_id); let mut state_bridge_service = - StateBridgeService::new(world_id_address, middleware).await?; - - let wallet = private_key - .parse::() - .expect("couldn't instantiate wallet from private key"); - - for bridge_config in bridge_configs { - let BridgeConfig { - state_bridge_address, - bridged_world_id_address, - bridged_rpc_url, - .. - } = bridge_config; - - let l2_middleware = - initialize_l2_middleware(&bridged_rpc_url, wallet.clone()).await?; - - let state_bridge_interface = - IStateBridge::new(state_bridge_address, l2_middleware.clone()); - - let bridged_world_id_interface = IBridgedWorldID::new( - bridged_world_id_address, - l2_middleware.clone(), - ); - - let state_bridge = StateBridge::new( - state_bridge_interface, - bridged_world_id_interface, - relaying_period, - block_confirmations, + StateBridgeService::new(config.l1_world_id, l1_middleware.clone()) + .await?; + + for bridge_config in config.state_bridge { + let l2_middleware = initialize_l2_middleware( + &bridge_config.l2_rpc_endpoint, + bridge_config.throttle, + ) + .await?; + + let state_bridge = StateBridge::new_from_parts( + bridge_config.l1_state_bridge, + wallet.clone(), + l1_middleware.clone(), + bridge_config.l2_world_id, + l2_middleware, + bridge_config.relaying_period_seconds, + config.block_confirmations, )?; state_bridge_service.add_state_bridge(state_bridge); @@ -176,27 +131,94 @@ async fn spawn_state_bridge_service( result??; } + shutdown_tracer_provider(); + Ok(()) } -pub async fn initialize_l2_middleware( - l2_rpc_endpoint: &str, - wallet: LocalWallet, +pub async fn initialize_l1_middleware( + rpc_endpoint: &str, + throttle: u32, + wallet_address: H160, + max_gas_price: Option, ) -> eyre::Result< - Arc, LocalWallet>>>, + Arc< + GasEscalatorMiddleware< + NonceManagerMiddleware>>, + >, + >, > { - let l2_provider = Provider::::try_from(l2_rpc_endpoint)?; - let chain_id = l2_provider.get_chainid().await?.as_u64(); + let provider = initialize_throttled_provider(rpc_endpoint, throttle)?; + let nonce_manager_middleware = + NonceManagerMiddleware::new(provider, wallet_address); - let wallet = wallet.with_chain_id(chain_id); - let wallet_address = wallet.address(); + let geometric_escalator = + GeometricGasPrice::new(1.125, 60_u64, max_gas_price); + let gas_escalator_middleware = GasEscalatorMiddleware::new( + nonce_manager_middleware, + geometric_escalator, + Frequency::PerBlock, + ); - let signer_middleware = SignerMiddleware::new(l2_provider, wallet); + Ok(Arc::new(gas_escalator_middleware)) +} - let nonce_manager_middleware = - NonceManagerMiddleware::new(signer_middleware, wallet_address); +pub async fn initialize_l2_middleware( + l2_rpc_endpoint: &str, + throttle: u32, +) -> eyre::Result>>> { + Ok(Arc::new(initialize_throttled_provider( + l2_rpc_endpoint, + throttle, + )?)) +} - let l2_middleware = Arc::new(nonce_manager_middleware); +pub fn initialize_throttled_provider( + rpc_endpoint: &str, + throttle: u32, +) -> eyre::Result>> { + let http_provider = Http::new(Url::parse(rpc_endpoint)?); + let throttled_http_provider = ThrottledProvider::new( + http_provider, + throttle, + Some(Jitter::new( + Duration::from_millis(10), + Duration::from_millis(100), + )), + ); + + Ok(Provider::new(throttled_http_provider)) +} - Ok(l2_middleware) +fn deserialize_h160<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + H160::from_str(&s).map_err(de::Error::custom) +} + +fn deserialize_duration_from_seconds<'de, D>( + deserializer: D, +) -> Result +where + D: Deserializer<'de>, +{ + let secs = u64::deserialize(deserializer)?; + Ok(Duration::from_secs(secs)) +} + +fn deserialize_opt_u256<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = Option::::deserialize(deserializer)?; + match s { + Some(value) => U256::from_dec_str(&value) + .map(Some) + .map_err(serde::de::Error::custom), + None => Ok(None), + } } diff --git a/crates/common/src/metrics.rs b/crates/common/src/metrics.rs index 34149d8d..4f27f515 100644 --- a/crates/common/src/metrics.rs +++ b/crates/common/src/metrics.rs @@ -8,5 +8,6 @@ pub fn init_statsd_exporter(host: &str, port: u16) { .build(None) .expect("Could not create StatsdRecorder"); - metrics::set_boxed_recorder(Box::new(recorder)).expect("TODO:"); + metrics::set_boxed_recorder(Box::new(recorder)) + .expect("TODO: Handle this error"); } diff --git a/crates/common/src/test_utilities/chain_mock.rs b/crates/common/src/test_utilities/chain_mock.rs index dd863207..f738f569 100644 --- a/crates/common/src/test_utilities/chain_mock.rs +++ b/crates/common/src/test_utilities/chain_mock.rs @@ -32,6 +32,8 @@ pub struct MockChain { pub mock_bridged_world_id: MockBridgedWorldID, /// Middleware provider pub middleware: Arc, + /// Wallet used to send transactions to the mock chain + pub wallet: LocalWallet, } /// Spawns an anvil local chain with all World ID contracts deployed on it @@ -48,7 +50,7 @@ pub async fn spawn_mock_chain() -> eyre::Result> { LocalWallet::from(chain.keys()[0].clone()).with_chain_id(chain_id); let wallet_address = wallet.address(); - let client = SignerMiddleware::new(provider, wallet); + let client = SignerMiddleware::new(provider, wallet.clone()); let client = NonceManagerMiddleware::new(client, wallet_address); let client = Arc::new(client); @@ -97,5 +99,6 @@ pub async fn spawn_mock_chain() -> eyre::Result> { mock_bridged_world_id, mock_world_id, middleware: client, + wallet: wallet, }) } diff --git a/src/state_bridge/error.rs b/src/state_bridge/error.rs index a701c9bb..95f8e588 100644 --- a/src/state_bridge/error.rs +++ b/src/state_bridge/error.rs @@ -1,20 +1,27 @@ use ethers::prelude::{AbiError, ContractError}; use ethers::providers::{Middleware, ProviderError}; +use ethers::signers::WalletError; +use ethers::types::H256; use thiserror::Error; use crate::tree::Hash; #[derive(Error, Debug)] -pub enum StateBridgeError +pub enum StateBridgeError where - M: Middleware, + L1M: Middleware, + L2M: Middleware, { - #[error("Middleware error")] - MiddlewareError(::Error), + #[error("L1 middleware error")] + L1MiddlewareError(::Error), + #[error("L2 middleware error")] + L2MiddlewareError(::Error), #[error("Provider error")] ProviderError(#[from] ProviderError), - #[error("Contract error")] - ContractError(#[from] ContractError), + #[error("L1 contract error")] + L1ContractError(ContractError), + #[error("L2 contract error")] + L2ContractError(ContractError), #[error("ABI Codec error")] ABICodecError(#[from] AbiError), #[error("Eth ABI error")] @@ -25,4 +32,23 @@ where RecvError(#[from] tokio::sync::broadcast::error::RecvError), #[error("No state bridge was added to WorldTreeRoot")] BridgesNotInitialized, + #[error("Transaction error")] + TransactionError(#[from] TransactionError), +} + +#[derive(Error, Debug)] +pub enum TransactionError +where + M: Middleware, +{ + #[error("Middleware error")] + MiddlewareError(::Error), + #[error("Provider error")] + ProviderError(#[from] ethers::providers::ProviderError), + #[error("Wallet error")] + WalletError(#[from] WalletError), + #[error("Wallet has insufficient funds")] + InsufficientWalletFunds, + #[error("Tx receipt not found")] + TxReceiptNotFound(H256), } diff --git a/src/state_bridge/mod.rs b/src/state_bridge/mod.rs index a9027946..e4db1ff5 100644 --- a/src/state_bridge/mod.rs +++ b/src/state_bridge/mod.rs @@ -1,9 +1,12 @@ pub mod error; pub mod service; +pub mod transaction; +use std::ops::Sub; use std::sync::Arc; use ethers::providers::Middleware; +use ethers::signers::{LocalWallet, Signer}; use ethers::types::H160; use ruint::Uint; use tokio::select; @@ -12,37 +15,47 @@ use tokio::time::{Duration, Instant}; use tracing::instrument; use self::error::StateBridgeError; -use crate::abi::{IBridgedWorldID, IStateBridge}; +use crate::abi::{self, IBridgedWorldID}; use crate::tree::Hash; /// The `StateBridge` is responsible for monitoring root changes from the `WorldRoot`, propagating the root to the corresponding Layer 2. -pub struct StateBridge { - /// Interface for the `StateBridge` contract - pub state_bridge: IStateBridge, +pub struct StateBridge { + // Address for the state bridge contract on layer 1 + l1_state_bridge: H160, + // Wallet responsible for sending `propagateRoot` transactions + wallet: LocalWallet, + // Middleware to interact with layer 1 + l1_middleware: Arc, /// Interface for the `BridgedWorldID` contract - pub bridged_world_id: IBridgedWorldID, + pub l2_world_id: IBridgedWorldID, /// Time delay between `propagateRoot()` transactions pub relaying_period: Duration, /// The number of block confirmations before a `propagateRoot()` transaction is considered finalized pub block_confirmations: usize, } -impl StateBridge { +impl StateBridge { /// # Arguments /// - /// * state_bridge - Interface to the StateBridge smart contract. - /// * bridged_world_id - Interface to the BridgedWorldID smart contract. + /// * l1_state_bridge - Address for the state bridge contract on layer 1. + /// * wallet - Wallet responsible for sending `propagateRoot` transactions. + /// * l1_middleware - Middleware to interact with layer 1. + /// * l2_world_id - Interface to the BridgedWorldID smart contract. /// * relaying_period - Duration between successive propagateRoot() invocations. /// * block_confirmations - Number of block confirmations required to consider a propagateRoot() transaction as finalized. pub fn new( - state_bridge: IStateBridge, - bridged_world_id: IBridgedWorldID, + l1_state_bridge: H160, + wallet: LocalWallet, + l1_middleware: Arc, + l2_world_id: IBridgedWorldID, relaying_period: Duration, block_confirmations: usize, - ) -> Result> { + ) -> Result> { Ok(Self { - state_bridge, - bridged_world_id, + l1_state_bridge, + wallet, + l1_middleware, + l2_world_id, relaying_period, block_confirmations, }) @@ -50,31 +63,28 @@ impl StateBridge { /// # Arguments /// - /// * `bridge_address` - Address of the StateBridge contract. - /// * `canonical_middleware` - Middleware for interacting with the chain where StateBridge is deployed. - /// * `bridged_world_id_address` - Address of the BridgedWorldID contract. - /// * `derived_middleware` - Middleware for interacting with the chain where BridgedWorldID is deployed. + /// * l1_state_bridge - Address for the state bridge contract on layer 1. + /// * l1_middleware - Middleware to interact with layer 1. + /// * `l2_world_id` - Address of the BridgedWorldID contract. + /// * `l2_middleware` - Middleware to interact with layer 2. /// * `relaying_period` - Duration between `propagateRoot()` transactions. /// * `block_confirmations` - Number of block confirmations before a`propagateRoot()` transaction is considered finalized. pub fn new_from_parts( - bridge_address: H160, - canonical_middleware: Arc, - bridged_world_id_address: H160, - derived_middleware: Arc, + l1_state_bridge: H160, + wallet: LocalWallet, + l1_middleware: Arc, + l2_world_id: H160, + l2_middleware: Arc, relaying_period: Duration, block_confirmations: usize, - ) -> Result> { - let state_bridge = - IStateBridge::new(bridge_address, canonical_middleware); - - let bridged_world_id = IBridgedWorldID::new( - bridged_world_id_address, - derived_middleware.clone(), - ); + ) -> Result> { + let l2_world_id = IBridgedWorldID::new(l2_world_id, l2_middleware); Ok(Self { - state_bridge, - bridged_world_id, + l1_state_bridge, + wallet, + l1_middleware, + l2_world_id, relaying_period, block_confirmations, }) @@ -89,72 +99,117 @@ impl StateBridge { pub fn spawn( &self, mut root_rx: tokio::sync::broadcast::Receiver, - ) -> JoinHandle>> { - let bridged_world_id = self.bridged_world_id.clone(); - let state_bridge = self.state_bridge.clone(); + ) -> JoinHandle>> { + let l2_world_id = self.l2_world_id.clone(); + let l1_state_bridge = self.l1_state_bridge; let relaying_period = self.relaying_period; let block_confirmations = self.block_confirmations; + let wallet = self.wallet.clone(); + let l2_world_id_address = l2_world_id.address(); + let l1_middleware = self.l1_middleware.clone(); - let bridged_world_id_address = bridged_world_id.address(); - let state_bridge_address = state_bridge.address(); tracing::info!( - ?bridged_world_id_address, - ?state_bridge_address, + ?l2_world_id_address, + ?l1_state_bridge, ?relaying_period, ?block_confirmations, "Spawning bridge" ); tokio::spawn(async move { - let mut latest_root = Hash::ZERO; - let mut last_propagation = Instant::now(); + let mut latest_root = Uint::from_limbs( + l2_world_id + .latest_root() + .call() + .await + .map_err(StateBridgeError::L2ContractError)? + .0, + ); - loop { - // will either be positive or zero if difference is negative - let sleep_time = relaying_period - .saturating_sub(Instant::now() - last_propagation); + let mut last_propagation = Instant::now().sub(relaying_period); + loop { select! { root = root_rx.recv() => { - tracing::info!(?root, "Root received from rx"); + tracing::info!(?l1_state_bridge, ?root, "Root received from rx"); latest_root = root?; } - _ = tokio::time::sleep(sleep_time) => { - tracing::info!("Sleep time elapsed"); + _ = tokio::time::sleep(relaying_period) => { + tracing::info!(?l1_state_bridge, "Sleep time elapsed"); } } let time_since_last_propagation = Instant::now() - last_propagation; - if time_since_last_propagation > relaying_period { - tracing::info!("Relaying period elapsed"); + if time_since_last_propagation >= relaying_period { + tracing::info!(?l1_state_bridge, "Relaying period elapsed"); let latest_bridged_root = Uint::from_limbs( - bridged_world_id.latest_root().call().await?.0, + l2_world_id + .latest_root() + .call() + .await + .map_err(StateBridgeError::L2ContractError)? + .0, ); if latest_root != latest_bridged_root { tracing::info!( + ?l1_state_bridge, ?latest_root, ?latest_bridged_root, "Propagating root" ); - state_bridge - .propagate_root() - .send() - .await? - .confirmations(block_confirmations) - .await?; + Self::propagate_root( + l1_state_bridge, + &wallet, + block_confirmations, + l1_middleware.clone(), + ) + .await?; last_propagation = Instant::now(); } else { - tracing::info!("Root already propagated"); + tracing::info!( + ?l1_state_bridge, + "Root already propagated" + ); } } } }) } + + pub async fn propagate_root( + l1_state_bridge: H160, + wallet: &LocalWallet, + block_confirmations: usize, + l1_middleware: Arc, + ) -> Result<(), StateBridgeError> { + let calldata = abi::ISTATEBRIDGE_ABI + .function("propagateRoot")? + .encode_input(&[])?; + + let tx = transaction::fill_and_simulate_eip1559_transaction( + calldata.into(), + l1_state_bridge, + wallet.address(), + wallet.chain_id(), + l1_middleware.clone(), + ) + .await?; + + transaction::sign_and_send_transaction( + tx, + wallet, + block_confirmations, + l1_middleware, + ) + .await?; + + Ok(()) + } } diff --git a/src/state_bridge/service.rs b/src/state_bridge/service.rs index 5f2d797d..ec7872cb 100644 --- a/src/state_bridge/service.rs +++ b/src/state_bridge/service.rs @@ -12,21 +12,25 @@ use crate::abi::{IWorldIDIdentityManager, TreeChangedFilter}; use crate::tree::Hash; /// Monitors the world tree root for changes and propagates new roots to target Layer 2s -pub struct StateBridgeService { +pub struct StateBridgeService< + L1M: Middleware + 'static, + L2M: Middleware + 'static, +> { /// Monitors `TreeChanged` events from `WorldIDIdentityManager` and broadcasts new roots to through the `root_tx`. - pub world_id_identity_manager: IWorldIDIdentityManager, + pub world_id_identity_manager: IWorldIDIdentityManager, /// Vec of `StateBridge`, responsible for root propagation to target Layer 2s. - pub state_bridges: Vec>, + pub state_bridges: Vec>, } -impl StateBridgeService +impl StateBridgeService where - M: Middleware, + L1M: Middleware, + L2M: Middleware, { pub async fn new( world_tree_address: H160, - middleware: Arc, - ) -> Result> { + middleware: Arc, + ) -> Result> { let world_id_identity_manager = IWorldIDIdentityManager::new( world_tree_address, middleware.clone(), @@ -38,7 +42,7 @@ where } /// Adds a `StateBridge` to orchestrate root propagation to a target Layer 2. - pub fn add_state_bridge(&mut self, state_bridge: StateBridge) { + pub fn add_state_bridge(&mut self, state_bridge: StateBridge) { self.state_bridges.push(state_bridge); } @@ -48,7 +52,7 @@ where pub fn listen_for_new_roots( &self, root_tx: tokio::sync::broadcast::Sender, - ) -> JoinHandle>> { + ) -> JoinHandle>> { let world_id_identity_manager = self.world_id_identity_manager.clone(); let world_id_identity_manager_address = @@ -57,16 +61,29 @@ where let root_tx_clone = root_tx.clone(); tokio::spawn(async move { - // Event emitted when insertions or deletions are made to the tree + // Get the latest root from the WorldIdIdentityManager and send it through the channel + let latest_root = world_id_identity_manager + .latest_root() + .call() + .await + .map_err(StateBridgeError::L1ContractError)?; + + root_tx_clone.send(Uint::from_limbs(latest_root.0))?; + + // Initialize filter to listen for tree changed events let filter = world_id_identity_manager.event::(); - let mut event_stream = filter.stream().await?.with_meta(); + let mut event_stream = filter + .stream() + .await + .map_err(StateBridgeError::L1ContractError)? + .with_meta(); - // Listen to a stream of events, when a new event is received, update the root and block number + // When a new event is received, send the new root through the channel while let Some(Ok((event, _))) = event_stream.next().await { let new_root = event.post_root.0; tracing::info!(?new_root, "New root from chain"); - root_tx_clone.send(Uint::from_limbs(event.post_root.0))?; + root_tx_clone.send(Uint::from_limbs(new_root))?; } Ok(()) @@ -77,8 +94,8 @@ where pub fn spawn( &mut self, ) -> Result< - Vec>>>, - StateBridgeError, + Vec>>>, + StateBridgeError, > { if self.state_bridges.is_empty() { return Err(StateBridgeError::BridgesNotInitialized); diff --git a/src/state_bridge/transaction.rs b/src/state_bridge/transaction.rs new file mode 100644 index 00000000..8abd6e78 --- /dev/null +++ b/src/state_bridge/transaction.rs @@ -0,0 +1,121 @@ +use std::sync::Arc; + +use ethers::providers::{JsonRpcClient, Middleware, PendingTransaction}; +use ethers::signers::{LocalWallet, WalletError}; +use ethers::types::transaction::eip2718::TypedTransaction; +use ethers::types::{ + Bytes, Eip1559TransactionRequest, TransactionReceipt, H160, +}; +use tracing::instrument; + +use super::error::TransactionError; + +//Signs and sends transaction, bumps gas if necessary +#[instrument(skip(wallet_key, block_confirmations, middleware))] +pub async fn sign_and_send_transaction( + tx: TypedTransaction, + wallet_key: &LocalWallet, + block_confirmations: usize, + middleware: Arc, +) -> Result> { + tracing::info!("Signing tx"); + let signed_tx = raw_signed_transaction(tx.clone(), wallet_key)?; + tracing::info!("Sending tx"); + match middleware.send_raw_transaction(signed_tx.clone()).await { + Ok(pending_tx) => { + let tx_hash = pending_tx.tx_hash(); + tracing::info!(?tx_hash, "Pending tx"); + + return wait_for_tx_receipt(pending_tx, block_confirmations).await; + } + Err(err) => { + let error_string = err.to_string(); + if error_string.contains("insufficient funds") { + tracing::error!("Insufficient funds"); + return Err(TransactionError::InsufficientWalletFunds); + } else { + return Err(TransactionError::MiddlewareError(err)); + } + } + } +} + +#[instrument(skip(middleware))] +pub async fn fill_and_simulate_eip1559_transaction( + calldata: Bytes, + to: H160, + from: H160, + chain_id: u64, + middleware: Arc, +) -> Result> { + let (max_fee_per_gas, max_priority_fee_per_gas) = middleware + .estimate_eip1559_fees(None) + .await + .map_err(TransactionError::MiddlewareError)?; + + tracing::info!( + ?max_fee_per_gas, + ?max_priority_fee_per_gas, + "Estimated gas fees" + ); + + let mut tx: TypedTransaction = Eip1559TransactionRequest::new() + .data(calldata.clone()) + .to(to) + .from(from) + .chain_id(chain_id) + .max_priority_fee_per_gas(max_priority_fee_per_gas) + .max_fee_per_gas(max_fee_per_gas) + .into(); + + middleware + .fill_transaction(&mut tx, None) + .await + .map_err(TransactionError::MiddlewareError)?; + + tx.set_gas(tx.gas().unwrap() * 150 / 100); + + let tx_gas = tx.gas().expect("Could not get tx gas"); + tracing::info!(?tx_gas, "Gas limit set"); + + middleware + .call(&tx, None) + .await + .map_err(TransactionError::MiddlewareError)?; + + tracing::info!("Successfully simulated tx"); + + Ok(tx) +} + +#[instrument] +pub async fn wait_for_tx_receipt<'a, M: Middleware, P: JsonRpcClient>( + pending_tx: PendingTransaction<'a, P>, + block_confirmations: usize, +) -> Result> { + let pending_tx = pending_tx.confirmations(block_confirmations); + let tx_hash = pending_tx.tx_hash(); + + tracing::info!( + ?tx_hash, + ?block_confirmations, + "Waiting for block confirmations" + ); + + if let Some(tx_receipt) = + pending_tx.await.map_err(TransactionError::ProviderError)? + { + tracing::info!(?tx_receipt, "Tx receipt received"); + + return Ok(tx_receipt); + } else { + return Err(TransactionError::TxReceiptNotFound(tx_hash)); + } +} + +pub fn raw_signed_transaction( + tx: TypedTransaction, + wallet_key: &LocalWallet, +) -> Result { + Ok(tx.rlp_signed(&wallet_key.sign_transaction_sync(&tx)?)) +} diff --git a/src/tree/block_scanner.rs b/src/tree/block_scanner.rs index a2d2e100..da324177 100644 --- a/src/tree/block_scanner.rs +++ b/src/tree/block_scanner.rs @@ -11,7 +11,7 @@ pub struct BlockScanner { pub last_synced_block: AtomicU64, /// The maximum block range to parse window_size: u64, - //TODO: + /// Filter specifying the address and topics to match on when scanning filter: Filter, } @@ -53,7 +53,6 @@ where .from_block(BlockNumber::Number(from_block.into())) .to_block(BlockNumber::Number(to_block.into())); - //TODO: can probably also use futures ordered here to get all of the logs quickly logs.extend(self.middleware.get_logs(&filter).await?); last_synced_block = to_block; diff --git a/tests/bridge_service.rs b/tests/bridge_service.rs index 20123d25..f35f3822 100644 --- a/tests/bridge_service.rs +++ b/tests/bridge_service.rs @@ -19,8 +19,7 @@ pub use serde_json::json; pub use tokio::spawn; pub use tokio::task::JoinHandle; pub use tracing::{error, info, instrument}; -use world_tree::abi::{IBridgedWorldID, IStateBridge}; -use world_tree::state_bridge::error::StateBridgeError; +use world_tree::abi::IBridgedWorldID; use world_tree::state_bridge::service::StateBridgeService; use world_tree::state_bridge::StateBridge; @@ -36,6 +35,7 @@ pub async fn test_relay_root() -> eyre::Result<()> { mock_world_id, middleware, anvil, + wallet, } = spawn_mock_chain().await?; let relaying_period = std::time::Duration::from_secs(5); @@ -51,16 +51,15 @@ pub async fn test_relay_root() -> eyre::Result<()> { .await .expect("couldn't create StateBridgeService"); - let state_bridge = - IStateBridge::new(state_bridge_address, middleware.clone()); - let bridged_world_id = IBridgedWorldID::new(bridged_world_id_address, middleware.clone()); let block_confirmations: usize = 6usize; let state_bridge = StateBridge::new( - state_bridge, + state_bridge_address, + wallet, + middleware, bridged_world_id, relaying_period, block_confirmations, @@ -103,29 +102,3 @@ pub async fn test_relay_root() -> eyre::Result<()> { Ok(()) } - -#[tokio::test] -pub async fn test_no_state_bridge_relay_fails() -> eyre::Result<()> { - // we need anvil to be in scope in order for the middleware provider to not be dropped - #[allow(unused_variables)] - let MockChain { - mock_world_id, - middleware, - anvil, - .. - } = spawn_mock_chain().await?; - - let mut state_bridge_service = - StateBridgeService::new(mock_world_id.address(), middleware.clone()) - .await - .expect("couldn't create StateBridgeService"); - - let error = state_bridge_service.spawn().unwrap_err(); - - assert!( - matches!(error, StateBridgeError::BridgesNotInitialized), - "Didn't error out as expected" - ); - - Ok(()) -}