From d78497f460a2bd6ba62ccb44a5cccb6c7d1786cb Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Wed, 9 Oct 2024 16:30:50 -0600 Subject: [PATCH 1/9] use PV1 in deployer.rs --- utils/src/deployer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/src/deployer.rs b/utils/src/deployer.rs index 4f5be76a7..7eeb42c0d 100644 --- a/utils/src/deployer.rs +++ b/utils/src/deployer.rs @@ -9,7 +9,7 @@ use contract_bindings::{ light_client_mock::LIGHTCLIENTMOCK_ABI, light_client_state_update_vk::LightClientStateUpdateVK, light_client_state_update_vk_mock::LightClientStateUpdateVKMock, - plonk_verifier_2::PlonkVerifier2, + plonk_verifier::PlonkVerifier, }; use derive_more::Display; use ethers::{ @@ -191,7 +191,7 @@ pub async fn deploy_light_client_contract( let plonk_verifier = contracts .deploy_tx( Contract::PlonkVerifier, - PlonkVerifier2::deploy(l1.clone(), ())?, + PlonkVerifier::deploy(l1.clone(), ())?, ) .await?; let vk = contracts @@ -254,7 +254,7 @@ pub async fn deploy_mock_light_client_contract( let plonk_verifier = contracts .deploy_tx( Contract::PlonkVerifier, - PlonkVerifier2::deploy(l1.clone(), ())?, + PlonkVerifier::deploy(l1.clone(), ())?, ) .await?; let vk = contracts From 1775ae2d08de3d2a3a899e1afd1bf2b3ab505518 Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Wed, 9 Oct 2024 21:31:56 -0600 Subject: [PATCH 2/9] choose lower priority fee for lightclient update --- Cargo.lock | 1 + hotshot-state-prover/Cargo.toml | 1 + hotshot-state-prover/src/service.rs | 37 +++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5e51e920..fea8a7dda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4447,6 +4447,7 @@ dependencies = [ "reqwest 0.12.8", "sequencer-utils", "serde", + "serde_json", "surf-disco", "tide-disco", "time 0.3.36", diff --git a/hotshot-state-prover/Cargo.toml b/hotshot-state-prover/Cargo.toml index 38172dea0..2b9619880 100644 --- a/hotshot-state-prover/Cargo.toml +++ b/hotshot-state-prover/Cargo.toml @@ -34,6 +34,7 @@ jf-utils = { workspace = true } reqwest = { workspace = true } sequencer-utils = { path = "../utils" } serde = { workspace = true } +serde_json = { workspace = true } surf-disco = { workspace = true } tide-disco = { workspace = true } time = { workspace = true } diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index 99bb410b7..9d5f6c623 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -18,7 +18,8 @@ use ethers::{ middleware::SignerMiddleware, providers::{Http, Middleware, Provider, ProviderError}, signers::{LocalWallet, Signer, Wallet}, - types::{Address, U256}, + types::{transaction::eip2718::TypedTransaction, Address, U256}, + utils::parse_units, }; use futures::FutureExt; use hotshot_contract_adapter::{ @@ -315,7 +316,31 @@ pub async fn submit_state_and_proof( // 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()); + + // frugal gas price: set to SafeGasPrice based on live price oracle + // safe to be included with low priority position in a block + let gas_info = reqwest::get("https://api.etherscan.io/api?module=gastracker&action=gasoracle") + .await? + .json::() + .await?; + let safe_gas: U256 = parse_units( + gas_info["result"]["SafeGasPrice"] + .as_str() + .expect("fail to parse SafeGasPrice"), + "gwei", + ) + .unwrap() // safe unwrap, etherscan will return right value + .into(); + + let mut tx = contract.new_finalized_state(new_state.into(), proof.into()); + if let TypedTransaction::Eip1559(inner) = &mut tx.tx { + inner.max_fee_per_gas = Some(safe_gas); + tracing::info!( + "Gas oracle info: {}, setting maxFeePerGas to: {}", + gas_info, + safe_gas, + ); + } // send the tx let (receipt, included_block) = sequencer_utils::contract_send::<_, _, LightClientErrors>(&tx) @@ -521,6 +546,8 @@ pub enum ProverError { PlonkError(PlonkError), /// Internal error Internal(String), + /// General network issue: {0} + NetworkError(anyhow::Error), } impl From for ProverError { @@ -547,6 +574,12 @@ impl From for ProverError { } } +impl From for ProverError { + fn from(err: reqwest::Error) -> Self { + Self::NetworkError(anyhow!("{}", err)) + } +} + impl std::error::Error for ProverError {} #[cfg(test)] From 4ba458edb76d651fe2df060afa7c453542cebc6a Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Thu, 10 Oct 2024 14:11:31 -0600 Subject: [PATCH 3/9] fix missed lines --- hotshot-state-prover/Cargo.toml | 3 +++ utils/src/deployer.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/hotshot-state-prover/Cargo.toml b/hotshot-state-prover/Cargo.toml index 2b9619880..b0e908e98 100644 --- a/hotshot-state-prover/Cargo.toml +++ b/hotshot-state-prover/Cargo.toml @@ -43,6 +43,9 @@ tracing = { workspace = true } url = { workspace = true } vbs = { workspace = true } +[dev-dependencies] +sequencer-utils = { path = "../utils", features = ["testing"] } + [features] default = ["parallel"] std = ["ark-std/std", "ark-ff/std"] diff --git a/utils/src/deployer.rs b/utils/src/deployer.rs index 7eeb42c0d..8a15ee972 100644 --- a/utils/src/deployer.rs +++ b/utils/src/deployer.rs @@ -490,7 +490,7 @@ pub mod test_helpers { fee_contract::{FeeContract, FEECONTRACT_ABI, FEECONTRACT_BYTECODE}, light_client::{LightClient, LIGHTCLIENT_ABI}, light_client_state_update_vk::LightClientStateUpdateVK, - plonk_verifier_2::PlonkVerifier2, + plonk_verifier::PlonkVerifier, }; use ethers::{prelude::*, solc::artifacts::BytecodeObject}; use hotshot_contract_adapter::light_client::LightClientConstructorArgs; @@ -507,7 +507,7 @@ pub mod test_helpers { let plonk_verifier = contracts .deploy_tx( Contract::PlonkVerifier, - PlonkVerifier2::deploy(l1.clone(), ())?, + PlonkVerifier::deploy(l1.clone(), ())?, ) .await?; let vk = contracts From 1ad78b9f7bda72b08440b17199c1e6810096add1 Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Thu, 10 Oct 2024 14:15:32 -0600 Subject: [PATCH 4/9] fix clippy --- tests/common/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index cec5a6092..dcb01f068 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -82,7 +82,7 @@ impl TestConfig { .unwrap(); // Varies between v0 and v3. - let load_generator_url = if sequencer_version >= 03 { + let load_generator_url = if sequencer_version >= 3 { url_from_port(dotenvy::var( "ESPRESSO_SUBMIT_TRANSACTIONS_PRIVATE_RESERVE_PORT", )?)? @@ -91,7 +91,7 @@ impl TestConfig { }; // TODO test both builders (probably requires some refactoring). - let builder_url = if sequencer_version >= 03 { + let builder_url = if sequencer_version >= 3 { let url = url_from_port(dotenvy::var("ESPRESSO_RESERVE_BUILDER_SERVER_PORT")?)?; Url::from_str(&url)? @@ -108,7 +108,7 @@ impl TestConfig { let l1_provider_url = url_from_port(dotenvy::var("ESPRESSO_SEQUENCER_L1_PORT")?)?; let sequencer_api_url = url_from_port(dotenvy::var("ESPRESSO_SEQUENCER1_API_PORT")?)?; - let sequencer_clients = vec![ + let sequencer_clients = [ dotenvy::var("ESPRESSO_SEQUENCER_API_PORT")?, dotenvy::var("ESPRESSO_SEQUENCER1_API_PORT")?, dotenvy::var("ESPRESSO_SEQUENCER2_API_PORT")?, From f0cf7aa08b69693e3fdd11d5bc17f4b8dbf7be26 Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Thu, 10 Oct 2024 16:32:30 -0600 Subject: [PATCH 5/9] ignore gas oracle when etherscan fail to return --- hotshot-state-prover/src/service.rs | 46 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index 9d5f6c623..8684e3504 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -317,29 +317,31 @@ pub async fn submit_state_and_proof( let proof: ParsedPlonkProof = proof.into(); let new_state: ParsedLightClientState = public_input.into(); - // frugal gas price: set to SafeGasPrice based on live price oracle - // safe to be included with low priority position in a block - let gas_info = reqwest::get("https://api.etherscan.io/api?module=gastracker&action=gasoracle") - .await? - .json::() - .await?; - let safe_gas: U256 = parse_units( - gas_info["result"]["SafeGasPrice"] - .as_str() - .expect("fail to parse SafeGasPrice"), - "gwei", - ) - .unwrap() // safe unwrap, etherscan will return right value - .into(); - let mut tx = contract.new_finalized_state(new_state.into(), proof.into()); - if let TypedTransaction::Eip1559(inner) = &mut tx.tx { - inner.max_fee_per_gas = Some(safe_gas); - tracing::info!( - "Gas oracle info: {}, setting maxFeePerGas to: {}", - gas_info, - safe_gas, - ); + // frugal gas price: set to SafeGasPrice based on live price oracle + // safe to be included with low priority position in a block. + // ignore this if etherscan fail to return + if let Ok(res) = + reqwest::get("https://api.etherscan.io/api?module=gastracker&action=gasoracle").await + { + let gas_info = res.json::().await?; + let safe_gas: U256 = parse_units( + gas_info["result"]["SafeGasPrice"] + .as_str() + .expect("fail to parse SafeGasPrice"), + "gwei", + ) + .unwrap() // safe unwrap, etherscan will return right value + .into(); + + if let TypedTransaction::Eip1559(inner) = &mut tx.tx { + inner.max_fee_per_gas = Some(safe_gas); + tracing::info!( + "Gas oracle info: {}, setting maxFeePerGas to: {}", + gas_info, + safe_gas, + ); + } } // send the tx From c835231830979279594e789707cf0a04d3239c59 Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Fri, 11 Oct 2024 12:13:11 -0600 Subject: [PATCH 6/9] gracefully fallback when etherscan api response changes --- Cargo.lock | 1 - hotshot-state-prover/Cargo.toml | 1 - hotshot-state-prover/src/service.rs | 64 +++++++++++++++++++++-------- marketplace-builder/Cargo.toml | 3 ++ 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fea8a7dda..b5e51e920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4447,7 +4447,6 @@ dependencies = [ "reqwest 0.12.8", "sequencer-utils", "serde", - "serde_json", "surf-disco", "tide-disco", "time 0.3.36", diff --git a/hotshot-state-prover/Cargo.toml b/hotshot-state-prover/Cargo.toml index b0e908e98..1d74d2549 100644 --- a/hotshot-state-prover/Cargo.toml +++ b/hotshot-state-prover/Cargo.toml @@ -34,7 +34,6 @@ jf-utils = { workspace = true } reqwest = { workspace = true } sequencer-utils = { path = "../utils" } serde = { workspace = true } -serde_json = { workspace = true } surf-disco = { workspace = true } tide-disco = { workspace = true } time = { workspace = true } diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index 8684e3504..3faebe3c6 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -303,6 +303,30 @@ pub async fn read_contract_state( Ok((state.into(), st_state.into())) } +/// Relevant fields in the etherscan price oracle feed, +/// used for parsing their JSON responses. +/// See +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct EtherscanPriceOracleResponse { + status: String, + message: String, + result: EtherscanPriceOracleResult, +} +/// The `result` field in `EtherscanPriceOracleResponse` +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct EtherscanPriceOracleResult { + #[serde(rename = "suggestBaseFee")] + base_fee: String, + #[serde(rename = "SafeGasPrice")] + safe_max_fee: String, + #[serde(rename = "ProposeGasPrice")] + propose_max_fee: String, + #[serde(rename = "FastGasPrice")] + fast_max_fee: String, +} + /// submit the latest finalized state along with a proof to the L1 LightClient contract pub async fn submit_state_and_proof( proof: Proof, @@ -318,29 +342,35 @@ pub async fn submit_state_and_proof( let new_state: ParsedLightClientState = public_input.into(); let mut tx = contract.new_finalized_state(new_state.into(), proof.into()); + + // TODO: (alex) centralize this to a dedicated ethtxmanager with gas control // frugal gas price: set to SafeGasPrice based on live price oracle // safe to be included with low priority position in a block. // ignore this if etherscan fail to return if let Ok(res) = reqwest::get("https://api.etherscan.io/api?module=gastracker&action=gasoracle").await { - let gas_info = res.json::().await?; - let safe_gas: U256 = parse_units( - gas_info["result"]["SafeGasPrice"] - .as_str() - .expect("fail to parse SafeGasPrice"), - "gwei", - ) - .unwrap() // safe unwrap, etherscan will return right value - .into(); - - if let TypedTransaction::Eip1559(inner) = &mut tx.tx { - inner.max_fee_per_gas = Some(safe_gas); - tracing::info!( - "Gas oracle info: {}, setting maxFeePerGas to: {}", - gas_info, - safe_gas, - ); + // parse etherscan response, + // if etherscan API changes, we log the warning and fallback to default (by falling through) + if let Ok(res) = res.json::().await { + if let TypedTransaction::Eip1559(inner) = &mut tx.tx { + // both safe unwraps below assuming correct etherscan stream + let safe_max_fee: U256 = parse_units(res.result.safe_max_fee.clone(), "gwei") + .unwrap() + .into(); + let base_fee: U256 = parse_units(res.result.base_fee.clone(), "gwei") + .unwrap() + .into(); + inner.max_fee_per_gas = Some(safe_max_fee); + inner.max_priority_fee_per_gas = Some(safe_max_fee - base_fee); + tracing::info!( + "Gas oracle info: {:?}, setting maxFeePerGas to: {}", + res.result, + safe_max_fee, + ); + } + } else { + tracing::warn!("!! Etherscan Price Oracle API changed !!") } } diff --git a/marketplace-builder/Cargo.toml b/marketplace-builder/Cargo.toml index ef4268bea..9fd7f193c 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 } From 6106914b3c282978b1e5a4036c548a51c0d117fa Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Fri, 11 Oct 2024 15:38:52 -0600 Subject: [PATCH 7/9] use TryFrom for etherscan feed sanitization --- hotshot-state-prover/src/service.rs | 56 +++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index 3faebe3c6..2a298356a 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -327,6 +327,34 @@ struct EtherscanPriceOracleResult { fast_max_fee: String, } +/// The actual gas info needed for tx submission, parsed from `EtherscanPriceOracleResult` +#[derive(Debug, Clone, Copy)] +struct GasInfo { + base_fee: U256, + safe_max_fee: U256, +} +impl TryFrom for GasInfo { + type Error = anyhow::Error; + fn try_from(res: EtherscanPriceOracleResult) -> Result { + // cases when etherscan change denomination + let safe_max_fee: U256 = parse_units(res.safe_max_fee.clone(), "gwei") + .map_err(|e| anyhow!("{}", e))? + .into(); + let base_fee: U256 = parse_units(res.base_fee.clone(), "gwei") + .map_err(|e| anyhow!("{}", e))? + .into(); + if safe_max_fee < base_fee { + // cases when etherscan completely misconfigure price feed + Err(anyhow!("!! Etherscan Price Oracle: wrong price feed!")) + } else { + Ok(Self { + base_fee, + safe_max_fee, + }) + } + } +} + /// submit the latest finalized state along with a proof to the L1 LightClient contract pub async fn submit_state_and_proof( proof: Proof, @@ -353,21 +381,19 @@ pub async fn submit_state_and_proof( // parse etherscan response, // if etherscan API changes, we log the warning and fallback to default (by falling through) if let Ok(res) = res.json::().await { - if let TypedTransaction::Eip1559(inner) = &mut tx.tx { - // both safe unwraps below assuming correct etherscan stream - let safe_max_fee: U256 = parse_units(res.result.safe_max_fee.clone(), "gwei") - .unwrap() - .into(); - let base_fee: U256 = parse_units(res.result.base_fee.clone(), "gwei") - .unwrap() - .into(); - inner.max_fee_per_gas = Some(safe_max_fee); - inner.max_priority_fee_per_gas = Some(safe_max_fee - base_fee); - tracing::info!( - "Gas oracle info: {:?}, setting maxFeePerGas to: {}", - res.result, - safe_max_fee, - ); + tracing::info!("Etherscan gas oracle: {:?}", res.result); + match TryInto::::try_into(res.result) { + Err(e) => { + tracing::warn!("{}", e) + } + Ok(gas_info) => { + if let TypedTransaction::Eip1559(inner) = &mut tx.tx { + inner.max_fee_per_gas = Some(gas_info.safe_max_fee); + inner.max_priority_fee_per_gas = + Some(gas_info.safe_max_fee - gas_info.base_fee); + tracing::info!("Setting maxFeePerGas to: {}", gas_info.safe_max_fee); + } + } } } else { tracing::warn!("!! Etherscan Price Oracle API changed !!") From 384fc7f9c80eb0221d0057e4efee95d83b5fb831 Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Wed, 16 Oct 2024 11:41:33 +0800 Subject: [PATCH 8/9] use (updated) blocknative oracle from ethers-rs --- Cargo.lock | 2 + hotshot-state-prover/src/service.rs | 134 +++++++------------------ utils/Cargo.toml | 3 + utils/src/blocknative.rs | 150 ++++++++++++++++++++++++++++ utils/src/lib.rs | 1 + 5 files changed, 191 insertions(+), 99 deletions(-) create mode 100644 utils/src/blocknative.rs diff --git a/Cargo.lock b/Cargo.lock index 3bf345f56..be62041bd 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 2a298356a..27a411ed5 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -13,13 +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, + middleware::{gas_oracle::GasOracleError, SignerMiddleware}, providers::{Http, Middleware, Provider, ProviderError}, signers::{LocalWallet, Signer, Wallet}, types::{transaction::eip2718::TypedTransaction, Address, U256}, - utils::parse_units, }; use futures::FutureExt; use hotshot_contract_adapter::{ @@ -43,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; @@ -277,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) => { @@ -303,100 +304,30 @@ pub async fn read_contract_state( Ok((state.into(), st_state.into())) } -/// Relevant fields in the etherscan price oracle feed, -/// used for parsing their JSON responses. -/// See -#[allow(dead_code)] -#[derive(Deserialize, Debug, Clone)] -struct EtherscanPriceOracleResponse { - status: String, - message: String, - result: EtherscanPriceOracleResult, -} -/// The `result` field in `EtherscanPriceOracleResponse` -#[allow(dead_code)] -#[derive(Deserialize, Debug, Clone)] -struct EtherscanPriceOracleResult { - #[serde(rename = "suggestBaseFee")] - base_fee: String, - #[serde(rename = "SafeGasPrice")] - safe_max_fee: String, - #[serde(rename = "ProposeGasPrice")] - propose_max_fee: String, - #[serde(rename = "FastGasPrice")] - fast_max_fee: String, -} - -/// The actual gas info needed for tx submission, parsed from `EtherscanPriceOracleResult` -#[derive(Debug, Clone, Copy)] -struct GasInfo { - base_fee: U256, - safe_max_fee: U256, -} -impl TryFrom for GasInfo { - type Error = anyhow::Error; - fn try_from(res: EtherscanPriceOracleResult) -> Result { - // cases when etherscan change denomination - let safe_max_fee: U256 = parse_units(res.safe_max_fee.clone(), "gwei") - .map_err(|e| anyhow!("{}", e))? - .into(); - let base_fee: U256 = parse_units(res.base_fee.clone(), "gwei") - .map_err(|e| anyhow!("{}", e))? - .into(); - if safe_max_fee < base_fee { - // cases when etherscan completely misconfigure price feed - Err(anyhow!("!! Etherscan Price Oracle: wrong price feed!")) - } else { - Ok(Self { - base_fee, - safe_max_fee, - }) - } - } -} - /// submit the latest finalized state along with a proof to the L1 LightClient contract 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 mut tx = contract.new_finalized_state(new_state.into(), proof.into()); - // TODO: (alex) centralize this to a dedicated ethtxmanager with gas control - // frugal gas price: set to SafeGasPrice based on live price oracle - // safe to be included with low priority position in a block. - // ignore this if etherscan fail to return - if let Ok(res) = - reqwest::get("https://api.etherscan.io/api?module=gastracker&action=gasoracle").await - { - // parse etherscan response, - // if etherscan API changes, we log the warning and fallback to default (by falling through) - if let Ok(res) = res.json::().await { - tracing::info!("Etherscan gas oracle: {:?}", res.result); - match TryInto::::try_into(res.result) { - Err(e) => { - tracing::warn!("{}", e) - } - Ok(gas_info) => { - if let TypedTransaction::Eip1559(inner) = &mut tx.tx { - inner.max_fee_per_gas = Some(gas_info.safe_max_fee); - inner.max_priority_fee_per_gas = - Some(gas_info.safe_max_fee - gas_info.base_fee); - tracing::info!("Setting maxFeePerGas to: {}", gas_info.safe_max_fee); - } - } - } - } else { - tracing::warn!("!! Etherscan Price Oracle API changed !!") + // 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); + let (max_fee, priority_fee) = gas_oracle.estimate_eip1559_fees().await?; + 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 + ); } } @@ -432,8 +363,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 @@ -491,7 +422,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(()) @@ -631,10 +562,15 @@ impl From for ProverError { Self::ContractError(anyhow!("{}", err)) } } +impl From, LocalWallet>> for ProverError { + fn from(err: SignerMiddlewareError, LocalWallet>) -> Self { + Self::ContractError(anyhow!("{}", err)) + } +} -impl From for ProverError { - fn from(err: reqwest::Error) -> Self { - Self::NetworkError(anyhow!("{}", err)) +impl From for ProverError { + fn from(err: GasOracleError) -> Self { + Self::NetworkError(anyhow!("GasOracle Error: {}", err)) } } @@ -934,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()); @@ -970,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/utils/Cargo.toml b/utils/Cargo.toml index 89a4771e5..39889d8b9 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 000000000..bfdc60aed --- /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 d4fcea03d..e79486df1 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; From db9ab26f48ec7b4f2f37faa0cc815d7f20c5ae6d Mon Sep 17 00:00:00 2001 From: Alex Xiong Date: Wed, 16 Oct 2024 11:50:54 +0800 Subject: [PATCH 9/9] log err if price oracle failed --- hotshot-state-prover/src/service.rs | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/hotshot-state-prover/src/service.rs b/hotshot-state-prover/src/service.rs index 27a411ed5..a674398f3 100644 --- a/hotshot-state-prover/src/service.rs +++ b/hotshot-state-prover/src/service.rs @@ -19,7 +19,7 @@ use ethers::middleware::{ }; use ethers::{ core::k256::ecdsa::SigningKey, - middleware::{gas_oracle::GasOracleError, SignerMiddleware}, + middleware::SignerMiddleware, providers::{Http, Middleware, Provider, ProviderError}, signers::{LocalWallet, Signer, Wallet}, types::{transaction::eip2718::TypedTransaction, Address, U256}, @@ -319,15 +319,21 @@ pub async fn submit_state_and_proof( // 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); - let (max_fee, priority_fee) = gas_oracle.estimate_eip1559_fees().await?; - 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 - ); + 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); + } } } @@ -568,12 +574,6 @@ impl From, LocalWallet>> for ProverError { } } -impl From for ProverError { - fn from(err: GasOracleError) -> Self { - Self::NetworkError(anyhow!("GasOracle Error: {}", err)) - } -} - impl std::error::Error for ProverError {} #[cfg(test)]