Skip to content

Commit 92037cb

Browse files
authored
feat: fallback to single tx submission if batching fails (#7247)
1 parent 211e245 commit 92037cb

File tree

12 files changed

+918
-542
lines changed

12 files changed

+918
-542
lines changed

rust/main/chains/hyperlane-ethereum/src/contracts/multicall.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use itertools::{Either, Itertools};
77
use tokio::sync::Mutex;
88
use tracing::warn;
99

10-
use hyperlane_core::{ChainResult, HyperlaneDomain, HyperlaneProvider, H256, U256};
10+
use hyperlane_core::{
11+
ChainCommunicationError, ChainResult, HyperlaneDomain, HyperlaneProvider, H256, U256,
12+
};
1113

1214
use crate::{decode_revert_reason, EthereumProvider};
1315

@@ -34,7 +36,7 @@ pub async fn build_multicall<M: Middleware + 'static>(
3436
domain: HyperlaneDomain,
3537
cache: Arc<Mutex<BatchCache>>,
3638
multicall_contract_address: H256,
37-
) -> eyre::Result<Multicall<M>> {
39+
) -> ChainResult<Multicall<M>> {
3840
let is_contract_cache = {
3941
let cache = cache.lock().await;
4042
cache.is_contract.get(&multicall_contract_address).cloned()
@@ -56,16 +58,19 @@ pub async fn build_multicall<M: Middleware + 'static>(
5658
};
5759

5860
if !is_contract {
59-
return Err(eyre::eyre!("Multicall contract not found at address"));
61+
let err =
62+
ChainCommunicationError::ContractNotFound(hex::encode(multicall_contract_address));
63+
return Err(err);
6064
}
6165
let multicall =
6266
match Multicall::new(provider.clone(), Some(multicall_contract_address.into())).await {
6367
Ok(multicall) => multicall.version(MulticallVersion::Multicall3),
6468
Err(err) => {
65-
return Err(eyre::eyre!(
66-
"Unable to build multicall contract: {}",
67-
err.to_string()
68-
))
69+
tracing::error!(?err, "Unable to build multicall contract");
70+
let err = ChainCommunicationError::CustomError(format!(
71+
"Unable to build multicall contract: {err}",
72+
));
73+
return Err(err);
6974
}
7075
};
7176

rust/main/chains/hyperlane-ethereum/src/rpc_clients/provider.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ where
337337
&self,
338338
cache: Arc<Mutex<BatchCache>>,
339339
batch_contract_address: H256,
340-
) -> eyre::Result<Multicall<M>> {
340+
) -> ChainResult<Multicall<M>> {
341341
multicall::build_multicall(
342342
self.provider.clone(),
343343
self.domain.clone(),

rust/main/hyperlane-core/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ pub enum ChainCommunicationError {
7272
/// An error with a contract call
7373
#[error(transparent)]
7474
ContractError(HyperlaneCustomErrorWrapper),
75+
/// When a transaction is not found
76+
#[error("Address is not a contract {0}")]
77+
ContractNotFound(String),
7578
/// A transaction was dropped from the mempool
7679
#[error("Transaction dropped from mempool {0:?}")]
7780
TransactionDropped(H256),

rust/main/lander/src/adapter/chains/ethereum/adapter.rs

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ethers_core::abi::Function;
1111
use ethers_core::types::Eip1559TransactionRequest;
1212
use eyre::eyre;
1313
use futures_util::future;
14+
use hyperlane_core::ChainResult;
1415
use tokio::sync::Mutex;
1516
use tokio::try_join;
1617
use tracing::{debug, error, info, instrument, warn};
@@ -33,6 +34,9 @@ use hyperlane_ethereum::{
3334
multicall, EthereumReorgPeriod, EvmProviderForLander, LanderProviderBuilder,
3435
};
3536

37+
use crate::adapter::chains::ethereum::metrics::{
38+
LABEL_BATCHED_TRANSACTION_FAILED, LABEL_BATCHED_TRANSACTION_SUCCESS,
39+
};
3640
use crate::{
3741
adapter::{core::TxBuildingResult, AdaptsChain, GasLimit},
3842
dispatcher::{PayloadDb, PostInclusionMetricsSource, TransactionDb},
@@ -66,6 +70,7 @@ pub struct EthereumAdapter {
6670
pub payload_db: Arc<dyn PayloadDb>,
6771
pub signer: H160,
6872
pub minimum_time_between_resubmissions: Duration,
73+
pub metrics: EthereumAdapterMetrics,
6974
}
7075

7176
impl EthereumAdapter {
@@ -97,14 +102,16 @@ impl EthereumAdapter {
97102
.ok_or_else(|| eyre!("No signer found in provider for domain {}", domain))?;
98103

99104
let metrics = EthereumAdapterMetrics::new(
105+
conf.domain.clone(),
106+
dispatcher_metrics.get_batched_transactions(),
100107
dispatcher_metrics.get_finalized_nonce(domain, &signer.to_string()),
101108
dispatcher_metrics.get_upper_nonce(domain, &signer.to_string()),
102109
);
103110

104111
let payload_db = db.clone() as Arc<dyn PayloadDb>;
105112

106113
let reorg_period = EthereumReorgPeriod::try_from(&conf.reorg_period)?;
107-
let nonce_manager = NonceManager::new(&conf, db, provider.clone(), metrics).await?;
114+
let nonce_manager = NonceManager::new(&conf, db, provider.clone(), metrics.clone()).await?;
108115

109116
let adapter = Self {
110117
estimated_block_time: conf.estimated_block_time,
@@ -119,6 +126,7 @@ impl EthereumAdapter {
119126
payload_db,
120127
signer,
121128
minimum_time_between_resubmissions: DEFAULT_MINIMUM_TIME_BETWEEN_RESUBMISSIONS,
129+
metrics,
122130
};
123131

124132
Ok(adapter)
@@ -338,7 +346,7 @@ impl EthereumAdapter {
338346
&self,
339347
precursors: Vec<(TypedTransaction, Function)>,
340348
payload_details: Vec<PayloadDetails>,
341-
) -> Vec<TxBuildingResult> {
349+
) -> ChainResult<Vec<TxBuildingResult>> {
342350
use super::transaction::TransactionFactory;
343351

344352
let multi_precursor = self
@@ -350,24 +358,19 @@ impl EthereumAdapter {
350358
self.signer,
351359
)
352360
.await
353-
.map(|(tx, f)| EthereumTxPrecursor::new(tx, f));
354-
355-
let multi_precursor = match multi_precursor {
356-
Ok(precursor) => precursor,
357-
Err(e) => {
358-
error!(error = ?e, "Failed to batch payloads");
359-
return vec![];
360-
}
361-
};
361+
.map(|(tx, f)| EthereumTxPrecursor::new(tx, f))?;
362362

363363
let transaction = TransactionFactory::build(multi_precursor, payload_details.clone());
364364

365365
let tx_building_result = TxBuildingResult {
366366
payloads: payload_details,
367367
maybe_tx: Some(transaction),
368368
};
369+
Ok(vec![tx_building_result])
370+
}
369371

370-
vec![tx_building_result]
372+
pub fn metrics(&self) -> &EthereumAdapterMetrics {
373+
&self.metrics
371374
}
372375
}
373376

@@ -403,14 +406,16 @@ impl AdaptsChain for EthereumAdapter {
403406
elapsed > *ready_time
404407
}
405408

406-
/// Builds a transaction for the given payloads.
409+
/// Builds transactions for the given payloads.
407410
///
408411
/// If there is only one payload, it builds a transaction without batching.
409412
/// If there are multiple payloads, it batches them into a single transaction.
410413
/// The order of individual calls in the batched transaction is determined
411414
/// by the order of payloads.
412415
/// The order should not change since the simulation and estimation of the batched transaction
413416
/// depend on the order of payloads.
417+
/// If batching fails, it will fallback to building multiple transactions with a
418+
/// single payload for each of them.
414419
async fn build_transactions(&self, payloads: &[FullPayload]) -> Vec<TxBuildingResult> {
415420
use super::transaction::TransactionFactory;
416421

@@ -445,12 +450,48 @@ impl AdaptsChain for EthereumAdapter {
445450
return results;
446451
}
447452

448-
// Batched transaction
449-
let results = self
450-
.build_batched_transaction(precursors, payload_details)
451-
.await;
453+
match self
454+
.build_batched_transaction(precursors.clone(), payload_details.clone())
455+
.await
456+
{
457+
Ok(results) => {
458+
self.metrics().increment_batched_transactions(
459+
LABEL_BATCHED_TRANSACTION_SUCCESS,
460+
payloads.len() as u64,
461+
);
462+
info!(
463+
?payloads,
464+
?results,
465+
"built batched transaction for payloads"
466+
);
467+
// Batched transaction
468+
return results;
469+
}
470+
Err(err) => {
471+
self.metrics().increment_batched_transactions(
472+
LABEL_BATCHED_TRANSACTION_FAILED,
473+
payloads.len() as u64,
474+
);
475+
warn!(
476+
domain = self.domain.name(),
477+
?err,
478+
"Failed to build batch transaction. Fallback to single tx submission"
479+
);
480+
}
481+
}
452482

453-
info!(?payloads, ?results, "built transaction for payloads");
483+
let results: Vec<_> = payloads
484+
.iter()
485+
.map(|payload| {
486+
let precursor = EthereumTxPrecursor::from_payload(payload, self.signer);
487+
self.build_single_transaction(precursor, vec![payload.details.clone()])
488+
})
489+
.collect();
490+
info!(
491+
?payloads,
492+
?results,
493+
"built multiple transactions for multiple payloads"
494+
);
454495
results
455496
}
456497

0 commit comments

Comments
 (0)