Validator automatically self-announces (#2177)

### Description

This PR adds the ability for validators to self-announce themselves, so
long as the validator key has a large enough balance to pay for the tx.

This required setting the retry provider to skip retrying reverting
transactions, since with a quorum provider and a very speedy blockchain
(like hardhat) the transaction is broadcast multiple times and the later
transactions conflict with the first one.

### Drive-by changes

None

### Related issues

- Fixes #2120 

### Backward compatibility

_Are these changes backward compatible?_

Yes

---------

Co-authored-by: Mattie Conover <git@mconover.dev>
pull/2241/head
Asa Oines 1 year ago committed by GitHub
parent 24f9642ea5
commit f6ad1e9a95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 63
      rust/agents/validator/src/submit.rs
  2. 9
      rust/agents/validator/src/validator.rs
  3. 55
      rust/chains/hyperlane-ethereum/src/mailbox.rs
  4. 3
      rust/chains/hyperlane-ethereum/src/rpc_clients/mod.rs
  5. 60
      rust/chains/hyperlane-ethereum/src/tx.rs
  6. 59
      rust/chains/hyperlane-ethereum/src/validator_announce.rs
  7. 16
      rust/hyperlane-core/src/traits/validator_announce.rs
  8. 38
      rust/utils/run-locally/src/main.rs

@ -5,16 +5,20 @@ use std::time::{Duration, Instant};
use eyre::Result;
use prometheus::IntGauge;
use tokio::{task::JoinHandle, time::sleep};
use tracing::{debug, info, info_span, instrument::Instrumented, Instrument};
use tracing::{debug, error, info, info_span, instrument::Instrumented, warn, Instrument};
use hyperlane_base::{CheckpointSyncer, CoreMetrics};
use hyperlane_core::{Announcement, HyperlaneDomain, HyperlaneSigner, HyperlaneSignerExt, Mailbox};
use hyperlane_core::{
Announcement, HyperlaneDomain, HyperlaneSigner, HyperlaneSignerExt, Mailbox, ValidatorAnnounce,
H256, U256,
};
pub(crate) struct ValidatorSubmitter {
interval: Duration,
reorg_period: Option<NonZeroU64>,
signer: Arc<dyn HyperlaneSigner>,
mailbox: Arc<dyn Mailbox>,
validator_announce: Arc<dyn ValidatorAnnounce>,
checkpoint_syncer: Arc<dyn CheckpointSyncer>,
metrics: ValidatorSubmitterMetrics,
}
@ -24,6 +28,7 @@ impl ValidatorSubmitter {
interval: Duration,
reorg_period: u64,
mailbox: Arc<dyn Mailbox>,
validator_announce: Arc<dyn ValidatorAnnounce>,
signer: Arc<dyn HyperlaneSigner>,
checkpoint_syncer: Arc<dyn CheckpointSyncer>,
metrics: ValidatorSubmitterMetrics,
@ -32,6 +37,7 @@ impl ValidatorSubmitter {
reorg_period: NonZeroU64::new(reorg_period),
interval,
mailbox,
validator_announce,
signer,
checkpoint_syncer,
metrics,
@ -43,7 +49,7 @@ impl ValidatorSubmitter {
tokio::spawn(async move { self.main_task().await }).instrument(span)
}
async fn main_task(self) -> Result<()> {
async fn announce_task(&self) -> Result<()> {
// Sign and post the validator announcement
let announcement = Announcement {
validator: self.signer.eth_address(),
@ -51,11 +57,60 @@ impl ValidatorSubmitter {
mailbox_domain: self.mailbox.domain().id(),
storage_location: self.checkpoint_syncer.announcement_location(),
};
let signed_announcement = self.signer.sign(announcement).await?;
let signed_announcement = self.signer.sign(announcement.clone()).await?;
self.checkpoint_syncer
.write_announcement(&signed_announcement)
.await?;
// Ensure that the validator has announced themselves before we enter
// the main validator submit loop. This is to avoid a situation in
// which the validator is signing checkpoints but has not announced
// their locations, which makes them functionally unusable.
let validators: [H256; 1] = [self.signer.eth_address().into()];
loop {
info!("Checking for validator announcement");
if let Some(locations) = self
.validator_announce
.get_announced_storage_locations(&validators)
.await?
.first()
{
if locations.contains(&self.checkpoint_syncer.announcement_location()) {
info!("Validator has announced signature storage location");
break;
}
info!("Validator has not announced signature storage location");
let balance_delta = self
.validator_announce
.announce_tokens_needed(signed_announcement.clone())
.await?;
if balance_delta > U256::zero() {
warn!(
tokens_needed=%balance_delta,
validator_address=?announcement.validator,
"Please send tokens to the validator address to announce",
);
} else {
let outcome = self
.validator_announce
.announce(signed_announcement.clone(), None)
.await?;
if !outcome.executed {
error!(
hash=?outcome.txid,
"Transaction attempting to announce validator reverted"
);
}
}
}
sleep(self.interval).await;
}
Ok(())
}
async fn main_task(self) -> Result<()> {
self.announce_task().await?;
// Ensure that the mailbox has > 0 messages before we enter the main
// validator submit loop. This is to avoid an underflow / reverted
// call when we invoke the `mailbox.latest_checkpoint()` method,

@ -7,7 +7,7 @@ use tokio::task::JoinHandle;
use tracing::instrument::Instrumented;
use hyperlane_base::{run_all, BaseAgent, CheckpointSyncer, CoreMetrics, HyperlaneAgentCore};
use hyperlane_core::{HyperlaneDomain, HyperlaneSigner, Mailbox};
use hyperlane_core::{HyperlaneDomain, HyperlaneSigner, Mailbox, ValidatorAnnounce};
use crate::{
settings::ValidatorSettings, submit::ValidatorSubmitter, submit::ValidatorSubmitterMetrics,
@ -19,6 +19,7 @@ pub struct Validator {
origin_chain: HyperlaneDomain,
core: HyperlaneAgentCore,
mailbox: Arc<dyn Mailbox>,
validator_announce: Arc<dyn ValidatorAnnounce>,
signer: Arc<dyn HyperlaneSigner>,
reorg_period: u64,
interval: Duration,
@ -55,10 +56,15 @@ impl BaseAgent for Validator {
.await?
.into();
let validator_announce = settings
.build_validator_announce(&settings.origin_chain, &metrics)
.await?;
Ok(Self {
origin_chain: settings.origin_chain,
core,
mailbox,
validator_announce,
signer,
reorg_period: settings.reorg_period,
interval: settings.interval,
@ -72,6 +78,7 @@ impl BaseAgent for Validator {
self.interval,
self.reorg_period,
self.mailbox.clone(),
self.validator_announce.clone(),
self.signer.clone(),
self.checkpoint_syncer.clone(),
ValidatorSubmitterMetrics::new(&self.core.metrics, &self.origin_chain),

@ -8,27 +8,22 @@ use std::sync::Arc;
use async_trait::async_trait;
use ethers::abi::AbiEncode;
use ethers::prelude::Middleware;
use ethers::types::Eip1559TransactionRequest;
use ethers_contract::builders::ContractCall;
use hyperlane_core::{KnownHyperlaneDomain, H160};
use tracing::instrument;
use hyperlane_core::{
utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator,
HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProtocolError, HyperlaneProvider, Indexer, LogMeta, Mailbox, MailboxIndexer,
RawHyperlaneMessage, TxCostEstimate, TxOutcome, H256, U256,
RawHyperlaneMessage, TxCostEstimate, TxOutcome, H160, H256, U256,
};
use crate::contracts::arbitrum_node_interface::ArbitrumNodeInterface;
use crate::contracts::i_mailbox::{IMailbox as EthereumMailboxInternal, ProcessCall, IMAILBOX_ABI};
use crate::trait_builder::BuildableWithProvider;
use crate::tx::report_tx;
use crate::tx::{fill_tx_gas_params, report_tx};
use crate::EthereumProvider;
/// An amount of gas to add to the estimated gas
const GAS_ESTIMATE_BUFFER: u32 = 50000;
impl<M> std::fmt::Display for EthereumMailboxInternal<M>
where
M: Middleware,
@ -218,46 +213,7 @@ where
metadata.to_vec().into(),
RawHyperlaneMessage::from(message).to_vec().into(),
);
let gas_limit = if let Some(gas_limit) = tx_gas_limit {
gas_limit
} else {
tx.estimate_gas()
.await?
.saturating_add(U256::from(GAS_ESTIMATE_BUFFER))
};
let Ok((max_fee, max_priority_fee)) = self.provider.estimate_eip1559_fees(None).await else {
// Is not EIP 1559 chain
return Ok(tx.gas(gas_limit))
};
let max_priority_fee = if matches!(
KnownHyperlaneDomain::try_from(message.destination),
Ok(KnownHyperlaneDomain::Polygon)
) {
// Polygon needs a max priority fee >= 30 gwei
let min_polygon_fee = U256::from(30_000_000_000u64);
max_priority_fee.max(min_polygon_fee)
} else {
max_priority_fee
};
// Is EIP 1559 chain
let mut request = Eip1559TransactionRequest::new();
if let Some(from) = tx.tx.from() {
request = request.from(*from);
}
if let Some(to) = tx.tx.to() {
request = request.to(to.clone());
}
if let Some(data) = tx.tx.data() {
request = request.data(data.clone());
}
if let Some(value) = tx.tx.value() {
request = request.value(*value);
}
request = request.max_fee_per_gas(max_fee);
request = request.max_priority_fee_per_gas(max_priority_fee);
let mut eip_1559_tx = tx.clone();
eip_1559_tx.tx = ethers::types::transaction::eip2718::TypedTransaction::Eip1559(request);
Ok(eip_1559_tx.gas(gas_limit))
fill_tx_gas_params(tx, tx_gas_limit, self.provider.clone(), message.destination).await
}
}
@ -447,7 +403,10 @@ mod test {
TxCostEstimate, H160, H256, U256,
};
use crate::{mailbox::GAS_ESTIMATE_BUFFER, EthereumMailbox};
use crate::EthereumMailbox;
/// An amount of gas to add to the estimated gas
const GAS_ESTIMATE_BUFFER: u32 = 50000;
#[tokio::test]
async fn test_process_estimate_costs_sets_l2_gas_limit_for_arbitrum() {

@ -19,7 +19,8 @@ enum CategorizedResponse<R> {
const METHODS_TO_NOT_RETRY: &[&str] = &["eth_estimateGas"];
const METHOD_TO_NOT_RETRY_WHEN_NOT_SUPPORTED: &[&str] = &["eth_feeHistory"];
const METHODS_TO_NOT_RETRY_ON_REVERT: &[&str] = &["eth_call"];
const METHODS_TO_NOT_RETRY_ON_REVERT: &[&str] =
&["eth_call", "eth_sendTransaction", "eth_sendRawTransaction"];
const METHODS_TO_NOT_RETRY_ON_NONCE_ERROR: &[&str] =
&["eth_sendRawTransaction", "eth_sendTransaction"];
const METHODS_TO_NOT_RETRY_ON_ALREADY_KNOWN: &[&str] =

@ -1,15 +1,20 @@
use std::sync::Arc;
use std::time::Duration;
use ethers::abi::Detokenize;
use ethers::prelude::{NameOrAddress, TransactionReceipt};
use ethers::types::Eip1559TransactionRequest;
use ethers_contract::builders::ContractCall;
use tracing::{error, info};
use hyperlane_core::utils::fmt_bytes;
use hyperlane_core::{ChainCommunicationError, ChainResult, H256};
use hyperlane_core::{ChainCommunicationError, ChainResult, KnownHyperlaneDomain, H256, U256};
use crate::Middleware;
/// An amount of gas to add to the estimated gas
const GAS_ESTIMATE_BUFFER: u32 = 50000;
/// Dispatches a transaction, logs the tx id, and returns the result
pub(crate) async fn report_tx<M, D>(tx: ContractCall<M, D>) -> ChainResult<TransactionReceipt>
where
@ -58,3 +63,56 @@ where
}
}
}
/// Populates the gas limit and price for a transaction
pub(crate) async fn fill_tx_gas_params<M, D>(
tx: ContractCall<M, D>,
tx_gas_limit: Option<U256>,
provider: Arc<M>,
domain: u32,
) -> ChainResult<ContractCall<M, D>>
where
M: Middleware + 'static,
D: Detokenize,
{
let gas_limit = if let Some(gas_limit) = tx_gas_limit {
gas_limit
} else {
tx.estimate_gas()
.await?
.saturating_add(U256::from(GAS_ESTIMATE_BUFFER))
};
let Ok((max_fee, max_priority_fee)) = provider.estimate_eip1559_fees(None).await else {
// Is not EIP 1559 chain
return Ok(tx.gas(gas_limit))
};
let max_priority_fee = if matches!(
KnownHyperlaneDomain::try_from(domain),
Ok(KnownHyperlaneDomain::Polygon)
) {
// Polygon needs a max priority fee >= 30 gwei
let min_polygon_fee = U256::from(30_000_000_000u64);
max_priority_fee.max(min_polygon_fee)
} else {
max_priority_fee
};
// Is EIP 1559 chain
let mut request = Eip1559TransactionRequest::new();
if let Some(from) = tx.tx.from() {
request = request.from(*from);
}
if let Some(to) = tx.tx.to() {
request = request.to(to.clone());
}
if let Some(data) = tx.tx.data() {
request = request.data(data.clone());
}
if let Some(value) = tx.tx.value() {
request = request.value(*value);
}
request = request.max_fee_per_gas(max_fee);
request = request.max_priority_fee_per_gas(max_priority_fee);
let mut eip_1559_tx = tx;
eip_1559_tx.tx = ethers::types::transaction::eip2718::TypedTransaction::Eip1559(request);
Ok(eip_1559_tx.gas(gas_limit))
}

@ -5,17 +5,20 @@ use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use ethers::providers::Middleware;
use ethers::providers::{Middleware, ProviderError};
use ethers_contract::builders::ContractCall;
use hyperlane_core::{
ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain,
HyperlaneProvider, ValidatorAnnounce, H160, H256,
Announcement, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract,
HyperlaneDomain, HyperlaneProvider, SignedType, TxOutcome, ValidatorAnnounce, H160, H256, U256,
};
use tracing::instrument;
use crate::contracts::i_validator_announce::{
IValidatorAnnounce as EthereumValidatorAnnounceInternal, IVALIDATORANNOUNCE_ABI,
};
use crate::trait_builder::BuildableWithProvider;
use crate::tx::{fill_tx_gas_params, report_tx};
use crate::EthereumProvider;
impl<M> std::fmt::Display for EthereumValidatorAnnounceInternal<M>
@ -50,6 +53,7 @@ where
{
contract: Arc<EthereumValidatorAnnounceInternal<M>>,
domain: HyperlaneDomain,
provider: Arc<M>,
}
impl<M> EthereumValidatorAnnounce<M>
@ -62,11 +66,28 @@ where
Self {
contract: Arc::new(EthereumValidatorAnnounceInternal::new(
locator.address,
provider,
provider.clone(),
)),
domain: locator.domain.clone(),
provider,
}
}
/// Returns a ContractCall that processes the provided message.
/// If the provided tx_gas_limit is None, gas estimation occurs.
async fn announce_contract_call(
&self,
announcement: SignedType<Announcement>,
tx_gas_limit: Option<U256>,
) -> ChainResult<ContractCall<M, bool>> {
let serialized_signature: [u8; 65] = announcement.signature.into();
let tx = self.contract.announce(
announcement.value.validator,
announcement.value.storage_location,
serialized_signature.into(),
);
fill_tx_gas_params(tx, tx_gas_limit, self.provider.clone(), self.domain.id()).await
}
}
impl<M> HyperlaneChain for EthereumValidatorAnnounce<M>
@ -110,6 +131,36 @@ where
.await?;
Ok(storage_locations)
}
async fn announce_tokens_needed(
&self,
announcement: SignedType<Announcement>,
) -> ChainResult<U256> {
let validator = announcement.value.validator;
let contract_call = self.announce_contract_call(announcement, None).await?;
if let Ok(balance) = self.provider.get_balance(validator, None).await {
if let Some(cost) = contract_call.tx.max_cost() {
Ok(cost.saturating_sub(balance))
} else {
Err(ProviderError::CustomError("Unable to get announce max cost".into()).into())
}
} else {
Err(ProviderError::CustomError("Unable to query balance".into()).into())
}
}
#[instrument(err, ret, skip(self))]
async fn announce(
&self,
announcement: SignedType<Announcement>,
tx_gas_limit: Option<U256>,
) -> ChainResult<TxOutcome> {
let contract_call = self
.announce_contract_call(announcement, tx_gas_limit)
.await?;
let receipt = report_tx(contract_call).await?;
Ok(receipt.into())
}
}
pub struct EthereumValidatorAnnounceAbi;

@ -3,7 +3,7 @@ use std::fmt::Debug;
use async_trait::async_trait;
use auto_impl::auto_impl;
use crate::{ChainResult, HyperlaneContract, H256};
use crate::{Announcement, ChainResult, HyperlaneContract, SignedType, TxOutcome, H256, U256};
/// Interface for the ValidatorAnnounce chain contract. Allows abstraction over
/// different chains
@ -15,4 +15,18 @@ pub trait ValidatorAnnounce: HyperlaneContract + Send + Sync + Debug {
&self,
validators: &[H256],
) -> ChainResult<Vec<Vec<String>>>;
/// Announce a storage location for a validator
async fn announce(
&self,
announcement: SignedType<Announcement>,
tx_gas_limit: Option<U256>,
) -> ChainResult<TxOutcome>;
/// Returns the number of additional tokens needed to pay for the announce
/// transaction.
async fn announce_tokens_needed(
&self,
announcement: SignedType<Announcement>,
) -> ChainResult<U256>;
}

@ -279,6 +279,15 @@ fn main() -> ExitCode {
.status()
.expect("Failed to run prettier from top level dir");
// Rebuild the SDK to pick up the deployed contracts
println!("Rebuilding sdk...");
build_cmd(
&["yarn", "build"],
&build_log,
log_all,
Some("../typescript/sdk"),
);
println!("Spawning relayer...");
let mut relayer = Command::new("target/debug/relayer")
.stdout(Stdio::piped())
@ -336,35 +345,6 @@ fn main() -> ExitCode {
}));
state.validator = Some(validator);
// Rebuild the SDK to pick up the deployed contracts
println!("Rebuilding sdk...");
build_cmd(
&["yarn", "build"],
&build_log,
log_all,
Some("../typescript/sdk"),
);
// Register the validator announcement
println!("Announcing validator...");
let mut announce = Command::new("yarn");
let location = format!("file://{}", checkpoints_dir.path().to_str().unwrap());
announce.arg("ts-node");
announce.args([
"scripts/announce-validators.ts",
"--environment",
"test",
"--location",
&location,
"--chain",
"test1",
]);
announce
.current_dir("../typescript/infra")
.stdout(Stdio::piped())
.spawn()
.expect("Failed to announce validator");
println!("Setup complete! Agents running in background...");
println!("Ctrl+C to end execution...");

Loading…
Cancel
Save