diff --git a/Cargo.lock b/Cargo.lock index 3bf345f564..be62041bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8691,6 +8691,7 @@ dependencies = [ "ark-serialize", "async-compatibility-layer", "async-std", + "async-trait", "clap", "committable", "contract-bindings", @@ -8700,6 +8701,7 @@ dependencies = [ "hotshot-contract-adapter", "log-panics", "portpicker", + "reqwest 0.11.27", "serde", "serde_json", "surf", diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index 99bb410b77..a674398f3d 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -13,12 +13,16 @@ use async_std::{ }; use contract_bindings::light_client::{LightClient, LightClientErrors}; use displaydoc::Display; +use ethers::middleware::{ + gas_oracle::{GasCategory, GasOracle}, + signer::SignerMiddlewareError, +}; use ethers::{ core::k256::ecdsa::SigningKey, middleware::SignerMiddleware, providers::{Http, Middleware, Provider, ProviderError}, signers::{LocalWallet, Signer, Wallet}, - types::{Address, U256}, + types::{transaction::eip2718::TypedTransaction, Address, U256}, }; use futures::FutureExt; use hotshot_contract_adapter::{ @@ -42,6 +46,7 @@ use jf_pcs::prelude::UnivariateUniversalParams; use jf_plonk::errors::PlonkError; use jf_relation::Circuit as _; use jf_signature::constants::CS_ID_SCHNORR; +use sequencer_utils::blocknative::BlockNative; use sequencer_utils::deployer::is_proxy_contract; use serde::Deserialize; use surf_disco::Client; @@ -276,11 +281,8 @@ async fn prepare_contract( /// get the `finalizedState` from the LightClient contract storage on L1 pub async fn read_contract_state( - provider: Url, - key: SigningKey, - light_client_address: Address, + contract: &LightClient, ) -> Result<(LightClientState, StakeTableState), ProverError> { - let contract = prepare_contract(provider, key, light_client_address).await?; let state: ParsedLightClientState = match contract.finalized_state().call().await { Ok(s) => s.into(), Err(e) => { @@ -306,16 +308,34 @@ pub async fn read_contract_state( pub async fn submit_state_and_proof( proof: Proof, public_input: PublicInput, - provider: Url, - key: SigningKey, - light_client_address: Address, + contract: &LightClient, ) -> Result<(), ProverError> { - let contract = prepare_contract(provider, key, light_client_address).await?; - // prepare the input the contract call and the tx itself let proof: ParsedPlonkProof = proof.into(); let new_state: ParsedLightClientState = public_input.into(); - let tx = contract.new_finalized_state(new_state.into(), proof.into()); + + let mut tx = contract.new_finalized_state(new_state.into(), proof.into()); + + // only use gas oracle for mainnet + if contract.client_ref().get_chainid().await?.as_u64() == 1 { + let gas_oracle = BlockNative::new(None).category(GasCategory::SafeLow); + match gas_oracle.estimate_eip1559_fees().await { + Ok((max_fee, priority_fee)) => { + if let TypedTransaction::Eip1559(inner) = &mut tx.tx { + inner.max_fee_per_gas = Some(max_fee); + inner.max_priority_fee_per_gas = Some(priority_fee); + tracing::info!( + "Setting maxFeePerGas: {}; maxPriorityFeePerGas to: {}", + max_fee, + priority_fee + ); + } + } + Err(e) => { + tracing::warn!("!! BlockNative Price Oracle failed: {}", e); + } + } + } // send the tx let (receipt, included_block) = sequencer_utils::contract_send::<_, _, LightClientErrors>(&tx) @@ -349,8 +369,8 @@ pub async fn sync_state( let bundle = fetch_latest_state(relay_server_client).await?; tracing::info!("Bundle accumulated weight: {}", bundle.accumulated_weight); tracing::info!("Latest HotShot block height: {}", bundle.state.block_height); - let (old_state, st_state) = - read_contract_state(provider.clone(), key.clone(), light_client_address).await?; + let contract = prepare_contract(provider.clone(), key.clone(), light_client_address).await?; + let (old_state, st_state) = read_contract_state(&contract).await?; tracing::info!( "Current HotShot block height on contract: {}", old_state.block_height @@ -408,7 +428,7 @@ pub async fn sync_state( let proof_gen_elapsed = Instant::now().signed_duration_since(proof_gen_start); tracing::info!("Proof generation completed. Elapsed: {proof_gen_elapsed:.3}"); - submit_state_and_proof(proof, public_input, provider, key, light_client_address).await?; + submit_state_and_proof(proof, public_input, &contract).await?; tracing::info!("Successfully synced light client state."); Ok(()) @@ -521,6 +541,8 @@ pub enum ProverError { PlonkError(PlonkError), /// Internal error Internal(String), + /// General network issue: {0} + NetworkError(anyhow::Error), } impl From for ProverError { @@ -546,6 +568,11 @@ impl From for ProverError { Self::ContractError(anyhow!("{}", err)) } } +impl From, LocalWallet>> for ProverError { + fn from(err: SignerMiddlewareError, LocalWallet>) -> Self { + Self::ContractError(anyhow!("{}", err)) + } +} impl std::error::Error for ProverError {} @@ -843,12 +870,13 @@ mod test { let mut config = StateProverConfig::default(); config.update_l1_info(&anvil, contract.address()); - let (state, st_state) = super::read_contract_state( + let contract = super::prepare_contract( config.provider, config.signing_key, config.light_client_address, ) .await?; + let (state, st_state) = super::read_contract_state(&contract).await?; assert_eq!(state, genesis.into()); assert_eq!(st_state, stake_genesis.into()); @@ -879,14 +907,13 @@ mod test { let (pi, proof) = gen_state_proof(new_state.clone(), &stake_genesis, &state_keys, &st); tracing::info!("Successfully generated proof for new state."); - super::submit_state_and_proof( - proof, - pi, + let contract = super::prepare_contract( config.provider, config.signing_key, config.light_client_address, ) .await?; + super::submit_state_and_proof(proof, pi, &contract).await?; tracing::info!("Successfully submitted new finalized state to L1."); // test if new state is updated in l1 let finalized_l1: ParsedLightClientState = contract.finalized_state().await?.into(); diff --git a/marketplace-builder/Cargo.toml b/marketplace-builder/Cargo.toml index ef4268bea7..9fd7f193c6 100644 --- a/marketplace-builder/Cargo.toml +++ b/marketplace-builder/Cargo.toml @@ -45,4 +45,7 @@ url = { workspace = true } vbs = { workspace = true } [dev-dependencies] +hotshot-query-service = { workspace = true } sequencer = { path = "../sequencer", features = ["testing"] } +sequencer-utils = { path = "../utils", features = ["testing"] } +tempfile = { workspace = true } diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 89a4771e5b..39889d8b9d 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -13,6 +13,7 @@ anyhow = { workspace = true } ark-serialize = { workspace = true, features = ["derive"] } async-compatibility-layer = { workspace = true } async-std = { workspace = true } +async-trait = { workspace = true } clap = { workspace = true } committable = "0.2" contract-bindings = { path = "../contract-bindings" } @@ -22,6 +23,8 @@ futures = { workspace = true } hotshot-contract-adapter = { workspace = true } log-panics = { workspace = true } portpicker = { workspace = true } +# for price oracle and align with ethers-rs dep +reqwest = { version = "0.11.14", default-features = false, features = ["json", "rustls-tls"] } serde = { workspace = true } serde_json = "^1.0.113" surf = "2.3.2" diff --git a/utils/src/blocknative.rs b/utils/src/blocknative.rs new file mode 100644 index 0000000000..bfdc60aed2 --- /dev/null +++ b/utils/src/blocknative.rs @@ -0,0 +1,150 @@ +//! Copy from +//! which is unmaintained and out-of-sync with the latest blocknative feed +//! +//! TDOO: revisit this or remove this when switching to `alloy-rs` +use async_trait::async_trait; +use ethers::{ + middleware::gas_oracle::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result}, + types::U256, +}; +use reqwest::{header::AUTHORIZATION, Client}; +use serde::Deserialize; +use std::collections::HashMap; +use url::Url; + +const URL: &str = "https://api.blocknative.com/gasprices/blockprices"; + +/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API +/// that implements the `GasOracle` trait. +#[derive(Clone, Debug)] +#[must_use] +pub struct BlockNative { + client: Client, + url: Url, + api_key: Option, + gas_category: GasCategory, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Response { + pub system: String, + pub network: String, + pub unit: String, + pub max_price: u64, + pub block_prices: Vec, + pub estimated_base_fees: Option>>>, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BlockPrice { + pub block_number: u64, + pub estimated_transaction_count: u64, + pub base_fee_per_gas: f64, + pub estimated_prices: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct GasEstimate { + pub confidence: u64, + pub price: f64, + pub max_priority_fee_per_gas: f64, + pub max_fee_per_gas: f64, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BaseFeeEstimate { + pub confidence: u64, + pub base_fee: f64, +} + +impl Response { + #[inline] + pub fn estimate_from_category(&self, gas_category: &GasCategory) -> Result { + let confidence = gas_category_to_confidence(gas_category); + let price = self + .block_prices + .first() + .ok_or(GasOracleError::InvalidResponse)? + .estimated_prices + .iter() + .find(|p| p.confidence == confidence) + .ok_or(GasOracleError::GasCategoryNotSupported)?; + Ok(*price) + } +} + +impl Default for BlockNative { + fn default() -> Self { + Self::new(None) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GasOracle for BlockNative { + async fn fetch(&self) -> Result { + let estimate = self + .query() + .await? + .estimate_from_category(&self.gas_category)?; + Ok(from_gwei_f64(estimate.price)) + } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> { + let estimate = self + .query() + .await? + .estimate_from_category(&self.gas_category)?; + let max = from_gwei_f64(estimate.max_fee_per_gas); + let prio = from_gwei_f64(estimate.max_priority_fee_per_gas); + Ok((max, prio)) + } +} + +impl BlockNative { + /// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle. + pub fn new(api_key: Option) -> Self { + Self::with_client(Client::new(), api_key) + } + + /// Same as [`Self::new`] but with a custom [`Client`]. + pub fn with_client(client: Client, api_key: Option) -> Self { + let url = Url::parse(URL).unwrap(); + Self { + client, + api_key, + url, + gas_category: GasCategory::Standard, + } + } + + /// Sets the gas price category to be used when fetching the gas price. + pub fn category(mut self, gas_category: GasCategory) -> Self { + self.gas_category = gas_category; + self + } + + /// Perform a request to the gas price API and deserialize the response. + pub async fn query(&self) -> Result { + let mut request = self.client.get(self.url.clone()); + if let Some(api_key) = self.api_key.as_ref() { + request = request.header(AUTHORIZATION, api_key); + } + let response = request.send().await?.error_for_status()?.json().await?; + Ok(response) + } +} + +#[inline] +fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 { + match gas_category { + GasCategory::SafeLow => 80, + GasCategory::Standard => 90, + GasCategory::Fast => 95, + GasCategory::Fastest => 99, + } +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index d4fcea03d1..e79486df1b 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -20,6 +20,7 @@ use ethers::{ use tempfile::TempDir; use url::Url; +pub mod blocknative; pub mod deployer; pub mod logging; pub mod ser;