diff --git a/Cargo.lock b/Cargo.lock index f388c1cb..07d65f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,6 +1584,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom", + "instant", + "pin-project-lite", + "rand", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -2486,6 +2500,7 @@ version = "0.1.1" dependencies = [ "alloy", "async-trait", + "backoff", "eigen-logging", "eigen-signer", "eigen-testing-utils", diff --git a/Cargo.toml b/Cargo.toml index 28e08347..7a7d6a64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ ark-serialize = "0.5.0" async-trait = "0.1.83" aws-config = "1.5.9" aws-sdk-kms = "1.49.0" +backoff = { version = "0.4.0", features = ["futures", "tokio"] } clap = { version = "4.5.20", features = ["derive"] } eigen-chainio-txmanager = { path = "crates/chainio/txmanager/" } eigen-client-avsregistry = { path = "crates/chainio/clients/avsregistry" } diff --git a/crates/chainio/clients/avsregistry/src/reader.rs b/crates/chainio/clients/avsregistry/src/reader.rs index 19471547..bc0fc048 100644 --- a/crates/chainio/clients/avsregistry/src/reader.rs +++ b/crates/chainio/clients/avsregistry/src/reader.rs @@ -399,6 +399,43 @@ impl AvsRegistryChainReader { Ok(quorum_stakes) } + /// Query registration detail + /// + /// # Arguments + /// + /// * `operator_address` - The operator address. + /// + /// # Returns + /// + /// A vector of booleans, where each boolean represents if the operator + /// is registered for a quorum. + /// + /// # Errors + /// + /// Returns an error if the operator id cannot be fetched or if the quorum bitmap + pub async fn query_registration_detail( + &self, + operator_address: Address, + ) -> Result<[bool; 64], AvsRegistryError> { + let operator_id = self.get_operator_id(operator_address).await?; + + let provider = get_provider(&self.provider); + let registry_coordinator = + RegistryCoordinator::new(self.registry_coordinator_addr, &provider); + let quorum_bitmap = registry_coordinator + .getCurrentQuorumBitmap(operator_id) + .call() + .await?; + + let inner_value = quorum_bitmap._0.into_limbs()[0]; + let mut quorums: [bool; 64] = [false; 64]; + for i in 0..64_u64 { + let other = inner_value & (1 << i) != 0; + quorums[i as usize] = other; + } + Ok(quorums) + } + /// Get operator id /// /// # Arguments @@ -597,23 +634,18 @@ impl AvsRegistryChainReader { mod tests { use super::*; use eigen_logging::get_test_logger; + use eigen_testing_utils::m2_holesky_constants::{ + HOLESKY_RPC_PROVIDER, OPERATOR_STATE_RETRIEVER, REGISTRY_COORDINATOR, + }; use hex::FromHex; use std::str::FromStr; - const HOLESKY_REGISTRY_COORDINATOR: &str = "0x53012C69A189cfA2D9d29eb6F19B32e0A2EA3490"; - const HOLESKY_OPERATOR_STATE_RETRIEVER: &str = "0xB4baAfee917fb4449f5ec64804217bccE9f46C67"; async fn build_avs_registry_chain_reader() -> AvsRegistryChainReader { - let holesky_registry_coordinator = - Address::from_str(HOLESKY_REGISTRY_COORDINATOR).expect("failed to parse address"); - let holesky_operator_state_retriever = - Address::from_str(HOLESKY_OPERATOR_STATE_RETRIEVER).expect("failed to parse address"); - - let holesky_provider = "https://ethereum-holesky.blockpi.network/v1/rpc/public"; AvsRegistryChainReader::new( get_test_logger(), - holesky_registry_coordinator, - holesky_operator_state_retriever, - holesky_provider.to_string(), + REGISTRY_COORDINATOR, + OPERATOR_STATE_RETRIEVER, + HOLESKY_RPC_PROVIDER.to_string(), ) .await .unwrap() @@ -697,9 +729,11 @@ mod tests { #[tokio::test] async fn test_is_operator_registered() { let avs_reader = build_avs_registry_chain_reader().await; - let address = Address::from_str(HOLESKY_REGISTRY_COORDINATOR).unwrap(); - let is_registered = avs_reader.is_operator_registered(address).await.unwrap(); + let is_registered = avs_reader + .is_operator_registered(REGISTRY_COORDINATOR) + .await + .unwrap(); assert!(!is_registered); } @@ -727,4 +761,40 @@ mod tests { .await .unwrap(); } + + #[tokio::test] + async fn test_query_registration_detail() { + let avs_reader = build_avs_registry_chain_reader().await; + + let operator_id = U256::from_str( + "35344093966194310405039483339636912150346494903629410125452342281826147822033", + ) + .unwrap(); + + let operator_id_b256: B256 = operator_id.into(); + + let operator_address = avs_reader + .get_operator_from_id(operator_id_b256.into()) + .await + .unwrap(); + + let ret_query_registration_detail = avs_reader + .query_registration_detail(operator_address) + .await + .unwrap(); + + println!("{:?}", ret_query_registration_detail); + + // all the value are false + for ret_value in ret_query_registration_detail.iter() { + assert!(!ret_value); + } + + // register an operator + let is_registered = avs_reader + .is_operator_registered(operator_address) + .await + .unwrap(); + assert!(!is_registered); + } } diff --git a/crates/chainio/txmanager/Cargo.toml b/crates/chainio/txmanager/Cargo.toml index 5525bbfa..afec9039 100644 --- a/crates/chainio/txmanager/Cargo.toml +++ b/crates/chainio/txmanager/Cargo.toml @@ -15,6 +15,7 @@ eigen-signer.workspace = true reqwest.workspace = true thiserror.workspace = true tokio.workspace = true +backoff.workspace = true [dev-dependencies] eigen-testing-utils.workspace = true diff --git a/crates/chainio/txmanager/src/simple_tx_manager.rs b/crates/chainio/txmanager/src/simple_tx_manager.rs index e52c31c9..889be616 100644 --- a/crates/chainio/txmanager/src/simple_tx_manager.rs +++ b/crates/chainio/txmanager/src/simple_tx_manager.rs @@ -4,9 +4,11 @@ use alloy::primitives::U256; use alloy::providers::{PendingTransactionBuilder, Provider, ProviderBuilder, RootProvider}; use alloy::rpc::types::eth::{TransactionInput, TransactionReceipt, TransactionRequest}; use alloy::signers::local::PrivateKeySigner; +use backoff::{future::retry, ExponentialBackoffBuilder}; use eigen_logging::logger::SharedLogger; use eigen_signer::signer::Config; use reqwest::Url; +use std::time::Duration; use thiserror::Error; static FALLBACK_GAS_TIP_CAP: u128 = 5_000_000_000; @@ -14,7 +16,7 @@ static FALLBACK_GAS_TIP_CAP: u128 = 5_000_000_000; pub type Transport = alloy::transports::http::Http; /// Possible errors raised in Tx Manager -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum TxManagerError { #[error("signer error")] SignerError, @@ -156,6 +158,48 @@ impl SimpleTxManager { SimpleTxManager::wait_for_receipt(self, pending_tx).await } + /// Send a transaction to the Ethereum node. It takes an unsigned/signed transaction, + /// sends it to the Ethereum node and waits for the receipt. + /// If you pass in a signed transaction it will ignore the signature + /// and re-sign the transaction after adding the nonce and gas limit. + /// If the transaction fails, it will retry sending the transaction until it gets a receipt, + /// using an **exponential backoff** strategy. + /// If no receipt is received after `max_elapsed_time`, it will return an error. + /// + /// # Arguments + /// + /// * `tx`: The transaction to be sent. + /// * `initial_interval`: The initial interval duration for the backoff. + /// * `max_elapsed_time`: The maximum elapsed time for retrying. + /// * `multiplier`: The multiplier used to compute the exponential backoff. + /// + /// # Returns + /// + /// * `TransactionReceipt` The transaction receipt. + /// + /// # Errors + /// + /// * `TxManagerError` - If the transaction cannot be sent, or there is an error + /// signing the transaction or estimating gas and nonce. + pub async fn send_tx_with_retries( + &self, + tx: &mut TransactionRequest, + initial_interval: Duration, + max_elapsed_time: Duration, + multiplier: f64, + ) -> Result { + let backoff_config = ExponentialBackoffBuilder::default() + .with_initial_interval(initial_interval) + .with_max_elapsed_time(Some(max_elapsed_time)) + .with_multiplier(multiplier) + .build(); + retry(backoff_config, || async { + let mut cloned_tx = tx.clone(); + Ok(self.send_tx(&mut cloned_tx).await?) + }) + .await + } + /// Estimates the gas and nonce for a transaction. /// /// # Arguments @@ -270,14 +314,16 @@ impl SimpleTxManager { #[cfg(test)] mod tests { - use super::SimpleTxManager; + use super::{SimpleTxManager, TxManagerError}; use alloy::consensus::TxLegacy; use alloy::network::TransactionBuilder; use alloy::primitives::{address, bytes, TxKind::Call, U256}; use alloy::rpc::types::eth::TransactionRequest; use eigen_logging::get_test_logger; use eigen_testing_utils::anvil::start_anvil_container; + use std::time::Duration; use tokio; + use tokio::time::Instant; #[tokio::test] async fn test_send_transaction_from_legacy() { @@ -339,4 +385,40 @@ mod tests { assert!(block_number > 0); assert_eq!(receipt.to, Some(to)); } + + #[tokio::test] + async fn test_send_transaction_with_retries_returns_after_timeout() { + let rpc_url = "http://fake:8545"; + let logger = get_test_logger(); + let private_key = + "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string(); + let simple_tx_manager = + SimpleTxManager::new(logger, 1.0, private_key.as_str(), rpc_url).unwrap(); + let to = address!("a0Ee7A142d267C1f36714E4a8F75612F20a79720"); + + let account_nonce = 0x69; + let mut tx = TransactionRequest::default() + .with_to(to) + .with_nonce(account_nonce) + .with_chain_id(31337) + .with_value(U256::from(100)) + .with_gas_limit(21_000) + .with_max_priority_fee_per_gas(1_000_000_000) + .with_max_fee_per_gas(20_000_000_000) + .with_gas_price(21_000_000_000); + let start = Instant::now(); + + let result = simple_tx_manager + .send_tx_with_retries( + &mut tx, + Duration::from_millis(5), + Duration::from_secs(1), + 1.0, + ) + .await; + assert_eq!(result, Err(TxManagerError::SendTxError)); + // substract one interval for asserting, because if the last try does not fit in the max_elapsed_time, it will not be executed + let elapsed = start.elapsed(); + assert!(elapsed >= Duration::from_secs(1) - Duration::from_millis(5)); + } } diff --git a/testing/testing-utils/src/m2_holesky_constants.rs b/testing/testing-utils/src/m2_holesky_constants.rs index cba586b3..bb5d58ff 100644 --- a/testing/testing-utils/src/m2_holesky_constants.rs +++ b/testing/testing-utils/src/m2_holesky_constants.rs @@ -1,5 +1,8 @@ use alloy_primitives::{address, Address}; +/// Holesky rpc provider +pub const HOLESKY_RPC_PROVIDER: &str = "https://ethereum-holesky-rpc.publicnode.com"; + /// https://holesky.etherscan.io/address/0xA44151489861Fe9e3055d95adC98FbD462B948e7 pub const DELEGATION_MANAGER_ADDRESS: Address = address!("A44151489861Fe9e3055d95adC98FbD462B948e7");