From 6d18061df4a18803d3c6377305ef711ce60317e1 Mon Sep 17 00:00:00 2001 From: Ivan Schasny <31857042+ischasny@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:03:59 +0100 Subject: [PATCH] feat: conditional cbt l1 updates (#2748) For operational efficiency, this PR enables conditional L1 updates for chains with custom base token. It adds a new configuration `BASE_TOKEN_ADJUSTER_L1_UPDATE_DEVIATION` that defines how much the token price needs to fluctuate in order for the update to be propagated to L1. Equal to 10% by default. --------- Co-authored-by: Roman Brodetski --- Cargo.lock | 1 + .../config/src/configs/base_token_adjuster.rs | 12 + core/lib/config/src/testonly.rs | 1 + core/lib/contracts/src/lib.rs | 8 + .../lib/env_config/src/base_token_adjuster.rs | 4 + .../src/base_token_adjuster.rs | 4 + .../proto/config/base_token_adjuster.proto | 1 + core/node/base_token_adjuster/Cargo.toml | 1 + .../src/base_token_l1_behaviour.rs | 331 ++++++++++++++++++ .../src/base_token_ratio_persister.rs | 218 +----------- core/node/base_token_adjuster/src/lib.rs | 4 +- .../base_token/base_token_ratio_persister.rs | 48 +-- 12 files changed, 404 insertions(+), 229 deletions(-) create mode 100644 core/node/base_token_adjuster/src/base_token_l1_behaviour.rs diff --git a/Cargo.lock b/Cargo.lock index 413f76e68e3..0350028da7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8068,6 +8068,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bigdecimal", "chrono", "hex", "rand 0.8.5", diff --git a/core/lib/config/src/configs/base_token_adjuster.rs b/core/lib/config/src/configs/base_token_adjuster.rs index c8a0fe6312e..d49a3853ff1 100644 --- a/core/lib/config/src/configs/base_token_adjuster.rs +++ b/core/lib/config/src/configs/base_token_adjuster.rs @@ -35,6 +35,9 @@ const DEFAULT_PRICE_FETCHING_SLEEP_MS: u64 = 5_000; /// Default number of milliseconds to sleep between transaction sending attempts const DEFAULT_L1_TX_SENDING_SLEEP_MS: u64 = 30_000; +/// Default number of percent that the quote should change in order for update to be propagated to L1 +const DEFAULT_L1_UPDATE_DEVIATION_PERCENTAGE: u32 = 10; + /// Default maximum acceptable priority fee in gwei to prevent sending transaction with extremely high priority fee. const DEFAULT_MAX_ACCEPTABLE_PRIORITY_FEE_IN_GWEI: u64 = 100_000_000_000; @@ -79,6 +82,11 @@ pub struct BaseTokenAdjusterConfig { #[serde(default = "BaseTokenAdjusterConfig::default_l1_tx_sending_sleep_ms")] pub l1_tx_sending_sleep_ms: u64, + /// How many percent a quote needs to change in order for update to be propagated to L1. + /// Exists to save on gas. + #[serde(default = "BaseTokenAdjusterConfig::default_l1_update_deviation_percentage")] + pub l1_update_deviation_percentage: u32, + /// Maximum number of attempts to fetch quote from a remote API before failing over #[serde(default = "BaseTokenAdjusterConfig::default_price_fetching_max_attempts")] pub price_fetching_max_attempts: u32, @@ -107,6 +115,7 @@ impl Default for BaseTokenAdjusterConfig { l1_receipt_checking_sleep_ms: Self::default_l1_receipt_checking_sleep_ms(), l1_tx_sending_max_attempts: Self::default_l1_tx_sending_max_attempts(), l1_tx_sending_sleep_ms: Self::default_l1_tx_sending_sleep_ms(), + l1_update_deviation_percentage: Self::default_l1_update_deviation_percentage(), price_fetching_sleep_ms: Self::default_price_fetching_sleep_ms(), price_fetching_max_attempts: Self::default_price_fetching_max_attempts(), halt_on_error: Self::default_halt_on_error(), @@ -170,6 +179,9 @@ impl BaseTokenAdjusterConfig { pub fn default_l1_tx_sending_sleep_ms() -> u64 { DEFAULT_L1_TX_SENDING_SLEEP_MS } + pub fn default_l1_update_deviation_percentage() -> u32 { + DEFAULT_L1_UPDATE_DEVIATION_PERCENTAGE + } pub fn default_price_fetching_sleep_ms() -> u64 { DEFAULT_PRICE_FETCHING_SLEEP_MS diff --git a/core/lib/config/src/testonly.rs b/core/lib/config/src/testonly.rs index 2ec91f5bec7..8c713319a5e 100644 --- a/core/lib/config/src/testonly.rs +++ b/core/lib/config/src/testonly.rs @@ -1046,6 +1046,7 @@ impl Distribution for Enc l1_receipt_checking_sleep_ms: self.sample(rng), l1_tx_sending_max_attempts: self.sample(rng), l1_tx_sending_sleep_ms: self.sample(rng), + l1_update_deviation_percentage: self.sample(rng), price_fetching_max_attempts: self.sample(rng), price_fetching_sleep_ms: self.sample(rng), halt_on_error: self.sample(rng), diff --git a/core/lib/contracts/src/lib.rs b/core/lib/contracts/src/lib.rs index a7ef0e5b26c..f10e557a642 100644 --- a/core/lib/contracts/src/lib.rs +++ b/core/lib/contracts/src/lib.rs @@ -48,6 +48,10 @@ const DIAMOND_INIT_CONTRACT_FILE: (&str, &str) = ( ); const GOVERNANCE_CONTRACT_FILE: (&str, &str) = ("governance", "IGovernance.sol/IGovernance.json"); const CHAIN_ADMIN_CONTRACT_FILE: (&str, &str) = ("governance", "IChainAdmin.sol/IChainAdmin.json"); +const GETTERS_FACET_CONTRACT_FILE: (&str, &str) = ( + "state-transition/chain-deps/facets", + "Getters.sol/GettersFacet.json", +); const MULTICALL3_CONTRACT_FILE: (&str, &str) = ("dev-contracts", "Multicall3.sol/Multicall3.json"); const VERIFIER_CONTRACT_FILE: (&str, &str) = ("state-transition", "Verifier.sol/Verifier.json"); @@ -134,6 +138,10 @@ pub fn chain_admin_contract() -> Contract { load_contract_for_both_compilers(CHAIN_ADMIN_CONTRACT_FILE) } +pub fn getters_facet_contract() -> Contract { + load_contract_for_both_compilers(GETTERS_FACET_CONTRACT_FILE) +} + pub fn state_transition_manager_contract() -> Contract { load_contract_for_both_compilers(STATE_TRANSITION_CONTRACT_FILE) } diff --git a/core/lib/env_config/src/base_token_adjuster.rs b/core/lib/env_config/src/base_token_adjuster.rs index f94e9c8f92a..5003d5ea587 100644 --- a/core/lib/env_config/src/base_token_adjuster.rs +++ b/core/lib/env_config/src/base_token_adjuster.rs @@ -28,6 +28,7 @@ mod tests { l1_tx_sending_sleep_ms: 30_000, price_fetching_max_attempts: 20, price_fetching_sleep_ms: 10_000, + l1_update_deviation_percentage: 20, halt_on_error: true, } } @@ -45,6 +46,7 @@ mod tests { l1_tx_sending_sleep_ms: 30_000, price_fetching_max_attempts: 3, price_fetching_sleep_ms: 5_000, + l1_update_deviation_percentage: 10, halt_on_error: false, } } @@ -62,6 +64,7 @@ mod tests { BASE_TOKEN_ADJUSTER_L1_RECEIPT_CHECKING_SLEEP_MS=20000 BASE_TOKEN_ADJUSTER_L1_TX_SENDING_MAX_ATTEMPTS=10 BASE_TOKEN_ADJUSTER_L1_TX_SENDING_SLEEP_MS=30000 + BASE_TOKEN_ADJUSTER_L1_UPDATE_DEVIATION_PERCENTAGE=20 BASE_TOKEN_ADJUSTER_PRICE_FETCHING_MAX_ATTEMPTS=20 BASE_TOKEN_ADJUSTER_PRICE_FETCHING_SLEEP_MS=10000 BASE_TOKEN_ADJUSTER_HALT_ON_ERROR=true @@ -85,6 +88,7 @@ mod tests { "BASE_TOKEN_ADJUSTER_L1_RECEIPT_CHECKING_SLEEP_MS", "BASE_TOKEN_ADJUSTER_L1_TX_SENDING_MAX_ATTEMPTS", "BASE_TOKEN_ADJUSTER_L1_TX_SENDING_SLEEP_MS", + "BASE_TOKEN_ADJUSTER_L1_UPDATE_DEVIATION_PERCENTAGE", "BASE_TOKEN_ADJUSTER_PRICE_FETCHING_MAX_ATTEMPTS", "BASE_TOKEN_ADJUSTER_PRICE_FETCHING_SLEEP_MS", "BASE_TOKEN_ADJUSTER_HALT_ON_ERROR", diff --git a/core/lib/protobuf_config/src/base_token_adjuster.rs b/core/lib/protobuf_config/src/base_token_adjuster.rs index 951feac1653..93c2fcea55b 100644 --- a/core/lib/protobuf_config/src/base_token_adjuster.rs +++ b/core/lib/protobuf_config/src/base_token_adjuster.rs @@ -42,6 +42,9 @@ impl ProtoRepr for proto::BaseTokenAdjuster { l1_tx_sending_sleep_ms: self .l1_tx_sending_sleep_ms .unwrap_or(Self::Type::default_l1_tx_sending_sleep_ms()), + l1_update_deviation_percentage: self + .l1_update_deviation_percentage + .unwrap_or(Self::Type::default_l1_update_deviation_percentage()), }) } @@ -53,6 +56,7 @@ impl ProtoRepr for proto::BaseTokenAdjuster { l1_receipt_checking_max_attempts: Some(this.l1_receipt_checking_max_attempts), l1_tx_sending_max_attempts: Some(this.l1_tx_sending_max_attempts), l1_tx_sending_sleep_ms: Some(this.l1_tx_sending_sleep_ms), + l1_update_deviation_percentage: Some(this.l1_update_deviation_percentage), price_fetching_max_attempts: Some(this.price_fetching_max_attempts), price_fetching_sleep_ms: Some(this.price_fetching_sleep_ms), max_tx_gas: Some(this.max_tx_gas), diff --git a/core/lib/protobuf_config/src/proto/config/base_token_adjuster.proto b/core/lib/protobuf_config/src/proto/config/base_token_adjuster.proto index 396bd400c04..6ec81baf51a 100644 --- a/core/lib/protobuf_config/src/proto/config/base_token_adjuster.proto +++ b/core/lib/protobuf_config/src/proto/config/base_token_adjuster.proto @@ -15,4 +15,5 @@ message BaseTokenAdjuster { optional bool halt_on_error = 10; optional uint32 price_fetching_max_attempts = 11; optional uint64 price_fetching_sleep_ms = 12; + optional uint32 l1_update_deviation_percentage = 13; } diff --git a/core/node/base_token_adjuster/Cargo.toml b/core/node/base_token_adjuster/Cargo.toml index 3a0beb2ea13..9dcf5d79653 100644 --- a/core/node/base_token_adjuster/Cargo.toml +++ b/core/node/base_token_adjuster/Cargo.toml @@ -21,6 +21,7 @@ zksync_eth_client.workspace = true zksync_node_fee_model.workspace = true zksync_utils.workspace = true vise.workspace = true +bigdecimal.workspace = true tokio = { workspace = true, features = ["time"] } anyhow.workspace = true diff --git a/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs b/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs new file mode 100644 index 00000000000..0199b06ebd6 --- /dev/null +++ b/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs @@ -0,0 +1,331 @@ +use std::{ + cmp::max, + ops::{Div, Mul}, + sync::Arc, + time::Instant, +}; + +use anyhow::Context; +use bigdecimal::{num_bigint::ToBigInt, BigDecimal, Zero}; +use zksync_config::BaseTokenAdjusterConfig; +use zksync_eth_client::{BoundEthInterface, CallFunctionArgs, Options}; +use zksync_node_fee_model::l1_gas_price::TxParamsProvider; +use zksync_types::{ + base_token_ratio::BaseTokenAPIRatio, + ethabi::{Contract, Token}, + web3::{contract::Tokenize, BlockNumber}, + Address, U256, +}; + +use crate::metrics::{OperationResult, OperationResultLabels, METRICS}; + +#[derive(Debug, Clone)] +pub struct UpdateOnL1Params { + pub eth_client: Box, + pub gas_adjuster: Arc, + pub token_multiplier_setter_account_address: Address, + pub chain_admin_contract: Contract, + pub getters_facet_contract: Contract, + pub diamond_proxy_contract_address: Address, + pub chain_admin_contract_address: Option
, + pub config: BaseTokenAdjusterConfig, +} + +#[derive(Debug, Clone)] +pub enum BaseTokenL1Behaviour { + UpdateOnL1 { + params: UpdateOnL1Params, + last_persisted_l1_ratio: Option, + }, + NoOp, +} + +impl BaseTokenL1Behaviour { + pub async fn update_l1(&mut self, new_ratio: BaseTokenAPIRatio) -> anyhow::Result<()> { + let (l1_params, last_persisted_l1_ratio) = match self { + BaseTokenL1Behaviour::UpdateOnL1 { + ref params, + ref last_persisted_l1_ratio, + } => (¶ms.clone(), last_persisted_l1_ratio), + BaseTokenL1Behaviour::NoOp => return Ok(()), + }; + + let prev_ratio = if let Some(prev_ratio) = last_persisted_l1_ratio { + prev_ratio.clone() + } else { + let prev_ratio = self.get_current_ratio_from_l1(l1_params).await?; + self.update_last_persisted_l1_ratio(prev_ratio.clone()); + tracing::info!( + "Fetched current base token ratio from the L1: {}", + prev_ratio.to_bigint().unwrap() + ); + prev_ratio + }; + + let current_ratio = BigDecimal::from(new_ratio.numerator.get()) + .div(BigDecimal::from(new_ratio.denominator.get())); + let deviation = Self::compute_deviation(prev_ratio.clone(), current_ratio.clone()); + + if deviation < BigDecimal::from(l1_params.config.l1_update_deviation_percentage) { + tracing::debug!( + "Skipping L1 update. current_ratio {}, previous_ratio {}, deviation {}", + current_ratio, + prev_ratio, + deviation.to_bigint().unwrap() + ); + return Ok(()); + } + + let max_attempts = l1_params.config.l1_tx_sending_max_attempts; + let sleep_duration = l1_params.config.l1_tx_sending_sleep_duration(); + let mut prev_base_fee_per_gas: Option = None; + let mut prev_priority_fee_per_gas: Option = None; + let mut last_error = None; + for attempt in 0..max_attempts { + let (base_fee_per_gas, priority_fee_per_gas) = + self.get_eth_fees(l1_params, prev_base_fee_per_gas, prev_priority_fee_per_gas); + + let start_time = Instant::now(); + let result = self + .do_update_l1(l1_params, new_ratio, base_fee_per_gas, priority_fee_per_gas) + .await; + + match result { + Ok(x) => { + tracing::info!( + "Updated base token multiplier on L1: numerator {}, denominator {}, base_fee_per_gas {}, priority_fee_per_gas {}, deviation {}", + new_ratio.numerator.get(), + new_ratio.denominator.get(), + base_fee_per_gas, + priority_fee_per_gas, + deviation.to_bigint().unwrap() + ); + METRICS + .l1_gas_used + .set(x.unwrap_or(U256::zero()).low_u128() as u64); + METRICS.l1_update_latency[&OperationResultLabels { + result: OperationResult::Success, + }] + .observe(start_time.elapsed()); + self.update_last_persisted_l1_ratio( + BigDecimal::from(new_ratio.numerator.get()) + .div(BigDecimal::from(new_ratio.denominator.get())), + ); + + return Ok(()); + } + Err(err) => { + tracing::info!( + "Failed to update base token multiplier on L1, attempt {}, base_fee_per_gas {}, priority_fee_per_gas {}: {}", + attempt, + base_fee_per_gas, + priority_fee_per_gas, + err + ); + METRICS.l1_update_latency[&OperationResultLabels { + result: OperationResult::Failure, + }] + .observe(start_time.elapsed()); + + tokio::time::sleep(sleep_duration).await; + prev_base_fee_per_gas = Some(base_fee_per_gas); + prev_priority_fee_per_gas = Some(priority_fee_per_gas); + last_error = Some(err) + } + } + } + + let error_message = "Failed to update base token multiplier on L1"; + Err(last_error + .map(|x| x.context(error_message)) + .unwrap_or_else(|| anyhow::anyhow!(error_message))) + } + + fn update_last_persisted_l1_ratio(&mut self, new_ratio: BigDecimal) { + match self { + BaseTokenL1Behaviour::UpdateOnL1 { + params: _, + ref mut last_persisted_l1_ratio, + } => *last_persisted_l1_ratio = Some(new_ratio), + BaseTokenL1Behaviour::NoOp => {} + }; + } + + async fn do_update_l1( + &self, + l1_params: &UpdateOnL1Params, + api_ratio: BaseTokenAPIRatio, + base_fee_per_gas: u64, + priority_fee_per_gas: u64, + ) -> anyhow::Result> { + let fn_set_token_multiplier = l1_params + .chain_admin_contract + .function("setTokenMultiplier") + .context("`setTokenMultiplier` function must be present in the ChainAdmin contract")?; + + let calldata = fn_set_token_multiplier + .encode_input( + &( + Token::Address(l1_params.diamond_proxy_contract_address), + Token::Uint(api_ratio.numerator.get().into()), + Token::Uint(api_ratio.denominator.get().into()), + ) + .into_tokens(), + ) + .context("failed encoding `setTokenMultiplier` input")?; + + let nonce = (*l1_params.eth_client) + .as_ref() + .nonce_at_for_account( + l1_params.token_multiplier_setter_account_address, + BlockNumber::Latest, + ) + .await + .with_context(|| "failed getting transaction count")? + .as_u64(); + + let options = Options { + gas: Some(U256::from(l1_params.config.max_tx_gas)), + nonce: Some(U256::from(nonce)), + max_fee_per_gas: Some(U256::from(base_fee_per_gas + priority_fee_per_gas)), + max_priority_fee_per_gas: Some(U256::from(priority_fee_per_gas)), + ..Default::default() + }; + + let signed_tx = l1_params + .eth_client + .sign_prepared_tx_for_addr( + calldata, + l1_params.chain_admin_contract_address.unwrap(), + options, + ) + .await + .context("cannot sign a `setTokenMultiplier` transaction")?; + + let hash = (*l1_params.eth_client) + .as_ref() + .send_raw_tx(signed_tx.raw_tx) + .await + .context("failed sending `setTokenMultiplier` transaction")?; + + let max_attempts = l1_params.config.l1_receipt_checking_max_attempts; + let sleep_duration = l1_params.config.l1_receipt_checking_sleep_duration(); + for _i in 0..max_attempts { + let maybe_receipt = (*l1_params.eth_client) + .as_ref() + .tx_receipt(hash) + .await + .context("failed getting receipt for `setTokenMultiplier` transaction")?; + if let Some(receipt) = maybe_receipt { + if receipt.status == Some(1.into()) { + return Ok(receipt.gas_used); + } + return Err(anyhow::Error::msg(format!( + "`setTokenMultiplier` transaction {:?} failed with status {:?}", + hex::encode(hash), + receipt.status + ))); + } else { + tokio::time::sleep(sleep_duration).await; + } + } + + Err(anyhow::Error::msg(format!( + "Unable to retrieve `setTokenMultiplier` transaction status in {} attempts", + max_attempts + ))) + } + + async fn get_current_ratio_from_l1( + &self, + l1_params: &UpdateOnL1Params, + ) -> anyhow::Result { + let numerator: U256 = CallFunctionArgs::new("baseTokenGasPriceMultiplierNominator", ()) + .for_contract( + l1_params.diamond_proxy_contract_address, + &l1_params.getters_facet_contract, + ) + .call((*l1_params.eth_client).as_ref()) + .await?; + let denominator: U256 = CallFunctionArgs::new("baseTokenGasPriceMultiplierDenominator", ()) + .for_contract( + l1_params.diamond_proxy_contract_address, + &l1_params.getters_facet_contract, + ) + .call((*l1_params.eth_client).as_ref()) + .await?; + Ok(BigDecimal::from(numerator.as_u128()).div(BigDecimal::from(denominator.as_u128()))) + } + + fn get_eth_fees( + &self, + l1_params: &UpdateOnL1Params, + prev_base_fee_per_gas: Option, + prev_priority_fee_per_gas: Option, + ) -> (u64, u64) { + // Use get_blob_tx_base_fee here instead of get_base_fee to optimise for fast inclusion. + // get_base_fee might cause the transaction to be stuck in the mempool for 10+ minutes. + let mut base_fee_per_gas = l1_params.gas_adjuster.as_ref().get_blob_tx_base_fee(); + let mut priority_fee_per_gas = l1_params.gas_adjuster.as_ref().get_priority_fee(); + if let Some(x) = prev_priority_fee_per_gas { + // Increase `priority_fee_per_gas` by at least 20% to prevent "replacement transaction under-priced" error. + priority_fee_per_gas = max(priority_fee_per_gas, (x * 6) / 5 + 1); + } + + if let Some(x) = prev_base_fee_per_gas { + // same for base_fee_per_gas but 10% + base_fee_per_gas = max(base_fee_per_gas, x + (x / 10) + 1); + } + + // Extra check to prevent sending transaction with extremely high priority fee. + if priority_fee_per_gas > l1_params.config.max_acceptable_priority_fee_in_gwei { + panic!( + "Extremely high value of priority_fee_per_gas is suggested: {}, while max acceptable is {}", + priority_fee_per_gas, + l1_params.config.max_acceptable_priority_fee_in_gwei + ); + } + + (base_fee_per_gas, priority_fee_per_gas) + } + + fn compute_deviation(prev: BigDecimal, next: BigDecimal) -> BigDecimal { + if prev.eq(&BigDecimal::zero()) { + return BigDecimal::from(100); + } + + (prev.clone() - next.clone()) + .abs() + .div(prev.clone()) + .mul(BigDecimal::from(100)) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Div; + + use bigdecimal::{BigDecimal, Zero}; + + use crate::base_token_l1_behaviour::BaseTokenL1Behaviour; + + #[test] + fn test_compute_deviation() { + let prev_ratio = BigDecimal::from(4); + let current_ratio = BigDecimal::from(5); + let deviation = + BaseTokenL1Behaviour::compute_deviation(prev_ratio.clone(), current_ratio.clone()); + assert_eq!(deviation, BigDecimal::from(25)); + + let deviation = BaseTokenL1Behaviour::compute_deviation(current_ratio, prev_ratio); + assert_eq!(deviation, BigDecimal::from(20)); + } + + #[test] + fn test_compute_deviation_when_prev_is_zero() { + let prev_ratio = BigDecimal::zero(); + let current_ratio = BigDecimal::from(1).div(BigDecimal::from(2)); + let deviation = BaseTokenL1Behaviour::compute_deviation(prev_ratio, current_ratio); + assert_eq!(deviation, BigDecimal::from(100)); + } +} diff --git a/core/node/base_token_adjuster/src/base_token_ratio_persister.rs b/core/node/base_token_adjuster/src/base_token_ratio_persister.rs index 12cd6233efb..220f100e5dc 100644 --- a/core/node/base_token_adjuster/src/base_token_ratio_persister.rs +++ b/core/node/base_token_adjuster/src/base_token_ratio_persister.rs @@ -1,30 +1,16 @@ -use std::{cmp::max, fmt::Debug, sync::Arc, time::Instant}; +use std::{fmt::Debug, sync::Arc, time::Instant}; use anyhow::Context as _; use tokio::{sync::watch, time::sleep}; use zksync_config::configs::base_token_adjuster::BaseTokenAdjusterConfig; use zksync_dal::{ConnectionPool, Core, CoreDal}; -use zksync_eth_client::{BoundEthInterface, Options}; use zksync_external_price_api::PriceAPIClient; -use zksync_node_fee_model::l1_gas_price::TxParamsProvider; -use zksync_types::{ - base_token_ratio::BaseTokenAPIRatio, - ethabi::{Contract, Token}, - web3::{contract::Tokenize, BlockNumber}, - Address, U256, -}; - -use crate::metrics::{OperationResult, OperationResultLabels, METRICS}; +use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address}; -#[derive(Debug, Clone)] -pub struct BaseTokenRatioPersisterL1Params { - pub eth_client: Box, - pub gas_adjuster: Arc, - pub token_multiplier_setter_account_address: Address, - pub chain_admin_contract: Contract, - pub diamond_proxy_contract_address: Address, - pub chain_admin_contract_address: Option
, -} +use crate::{ + base_token_l1_behaviour::BaseTokenL1Behaviour, + metrics::{OperationResult, OperationResultLabels, METRICS}, +}; #[derive(Debug, Clone)] pub struct BaseTokenRatioPersister { @@ -32,7 +18,7 @@ pub struct BaseTokenRatioPersister { config: BaseTokenAdjusterConfig, base_token_address: Address, price_api_client: Arc, - l1_params: Option, + l1_behaviour: BaseTokenL1Behaviour, } impl BaseTokenRatioPersister { @@ -42,14 +28,14 @@ impl BaseTokenRatioPersister { config: BaseTokenAdjusterConfig, base_token_address: Address, price_api_client: Arc, - l1_params: Option, + l1_behaviour: BaseTokenL1Behaviour, ) -> Self { Self { pool, config, base_token_address, price_api_client, - l1_params, + l1_behaviour, } } @@ -80,108 +66,11 @@ impl BaseTokenRatioPersister { Ok(()) } - async fn loop_iteration(&self) -> anyhow::Result<()> { + async fn loop_iteration(&mut self) -> anyhow::Result<()> { // TODO(PE-148): Consider shifting retry upon adding external API redundancy. let new_ratio = self.retry_fetch_ratio().await?; self.persist_ratio(new_ratio).await?; - self.retry_update_ratio_on_l1(new_ratio).await - } - - fn get_eth_fees( - &self, - l1_params: &BaseTokenRatioPersisterL1Params, - prev_base_fee_per_gas: Option, - prev_priority_fee_per_gas: Option, - ) -> (u64, u64) { - // Use get_blob_tx_base_fee here instead of get_base_fee to optimise for fast inclusion. - // get_base_fee might cause the transaction to be stuck in the mempool for 10+ minutes. - let mut base_fee_per_gas = l1_params.gas_adjuster.as_ref().get_blob_tx_base_fee(); - let mut priority_fee_per_gas = l1_params.gas_adjuster.as_ref().get_priority_fee(); - if let Some(x) = prev_priority_fee_per_gas { - // Increase `priority_fee_per_gas` by at least 20% to prevent "replacement transaction under-priced" error. - priority_fee_per_gas = max(priority_fee_per_gas, (x * 6) / 5 + 1); - } - - if let Some(x) = prev_base_fee_per_gas { - // same for base_fee_per_gas but 10% - base_fee_per_gas = max(base_fee_per_gas, x + (x / 10) + 1); - } - - // Extra check to prevent sending transaction will extremely high priority fee. - if priority_fee_per_gas > self.config.max_acceptable_priority_fee_in_gwei { - panic!( - "Extremely high value of priority_fee_per_gas is suggested: {}, while max acceptable is {}", - priority_fee_per_gas, - self.config.max_acceptable_priority_fee_in_gwei - ); - } - - (base_fee_per_gas, priority_fee_per_gas) - } - - async fn retry_update_ratio_on_l1(&self, new_ratio: BaseTokenAPIRatio) -> anyhow::Result<()> { - let Some(l1_params) = &self.l1_params else { - return Ok(()); - }; - - let max_attempts = self.config.l1_tx_sending_max_attempts; - let sleep_duration = self.config.l1_tx_sending_sleep_duration(); - let mut prev_base_fee_per_gas: Option = None; - let mut prev_priority_fee_per_gas: Option = None; - let mut last_error = None; - for attempt in 0..max_attempts { - let (base_fee_per_gas, priority_fee_per_gas) = - self.get_eth_fees(l1_params, prev_base_fee_per_gas, prev_priority_fee_per_gas); - - let start_time = Instant::now(); - let result = self - .update_ratio_on_l1(l1_params, new_ratio, base_fee_per_gas, priority_fee_per_gas) - .await; - - match result { - Ok(x) => { - tracing::info!( - "Updated base token multiplier on L1: numerator {}, denominator {}, base_fee_per_gas {}, priority_fee_per_gas {}", - new_ratio.numerator.get(), - new_ratio.denominator.get(), - base_fee_per_gas, - priority_fee_per_gas - ); - METRICS - .l1_gas_used - .set(x.unwrap_or(U256::zero()).low_u128() as u64); - METRICS.l1_update_latency[&OperationResultLabels { - result: OperationResult::Success, - }] - .observe(start_time.elapsed()); - - return Ok(()); - } - Err(err) => { - tracing::info!( - "Failed to update base token multiplier on L1, attempt {}, base_fee_per_gas {}, priority_fee_per_gas {}: {}", - attempt, - base_fee_per_gas, - priority_fee_per_gas, - err - ); - METRICS.l1_update_latency[&OperationResultLabels { - result: OperationResult::Failure, - }] - .observe(start_time.elapsed()); - - tokio::time::sleep(sleep_duration).await; - prev_base_fee_per_gas = Some(base_fee_per_gas); - prev_priority_fee_per_gas = Some(priority_fee_per_gas); - last_error = Some(err) - } - } - } - - let error_message = "Failed to update base token multiplier on L1"; - Err(last_error - .map(|x| x.context(error_message)) - .unwrap_or_else(|| anyhow::anyhow!(error_message))) + self.l1_behaviour.update_l1(new_ratio).await } async fn retry_fetch_ratio(&self) -> anyhow::Result { @@ -244,89 +133,4 @@ impl BaseTokenRatioPersister { Ok(id) } - - async fn update_ratio_on_l1( - &self, - l1_params: &BaseTokenRatioPersisterL1Params, - api_ratio: BaseTokenAPIRatio, - base_fee_per_gas: u64, - priority_fee_per_gas: u64, - ) -> anyhow::Result> { - let fn_set_token_multiplier = l1_params - .chain_admin_contract - .function("setTokenMultiplier") - .context("`setTokenMultiplier` function must be present in the ChainAdmin contract")?; - - let calldata = fn_set_token_multiplier - .encode_input( - &( - Token::Address(l1_params.diamond_proxy_contract_address), - Token::Uint(api_ratio.numerator.get().into()), - Token::Uint(api_ratio.denominator.get().into()), - ) - .into_tokens(), - ) - .context("failed encoding `setTokenMultiplier` input")?; - - let nonce = (*l1_params.eth_client) - .as_ref() - .nonce_at_for_account( - l1_params.token_multiplier_setter_account_address, - BlockNumber::Pending, - ) - .await - .with_context(|| "failed getting transaction count")? - .as_u64(); - - let options = Options { - gas: Some(U256::from(self.config.max_tx_gas)), - nonce: Some(U256::from(nonce)), - max_fee_per_gas: Some(U256::from(base_fee_per_gas + priority_fee_per_gas)), - max_priority_fee_per_gas: Some(U256::from(priority_fee_per_gas)), - ..Default::default() - }; - - let signed_tx = l1_params - .eth_client - .sign_prepared_tx_for_addr( - calldata, - l1_params.chain_admin_contract_address.unwrap(), - options, - ) - .await - .context("cannot sign a `setTokenMultiplier` transaction")?; - - let hash = (*l1_params.eth_client) - .as_ref() - .send_raw_tx(signed_tx.raw_tx) - .await - .context("failed sending `setTokenMultiplier` transaction")?; - - let max_attempts = self.config.l1_receipt_checking_max_attempts; - let sleep_duration = self.config.l1_receipt_checking_sleep_duration(); - for _i in 0..max_attempts { - let maybe_receipt = (*l1_params.eth_client) - .as_ref() - .tx_receipt(hash) - .await - .context("failed getting receipt for `setTokenMultiplier` transaction")?; - if let Some(receipt) = maybe_receipt { - if receipt.status == Some(1.into()) { - return Ok(receipt.gas_used); - } - return Err(anyhow::Error::msg(format!( - "`setTokenMultiplier` transaction {:?} failed with status {:?}", - hex::encode(hash), - receipt.status - ))); - } else { - tokio::time::sleep(sleep_duration).await; - } - } - - Err(anyhow::Error::msg(format!( - "Unable to retrieve `setTokenMultiplier` transaction status in {} attempts", - max_attempts - ))) - } } diff --git a/core/node/base_token_adjuster/src/lib.rs b/core/node/base_token_adjuster/src/lib.rs index d786b440f62..ddfad6ea8c9 100644 --- a/core/node/base_token_adjuster/src/lib.rs +++ b/core/node/base_token_adjuster/src/lib.rs @@ -1,8 +1,10 @@ pub use self::{ - base_token_ratio_persister::{BaseTokenRatioPersister, BaseTokenRatioPersisterL1Params}, + base_token_l1_behaviour::{BaseTokenL1Behaviour, UpdateOnL1Params}, + base_token_ratio_persister::BaseTokenRatioPersister, base_token_ratio_provider::{DBBaseTokenRatioProvider, NoOpRatioProvider}, }; +mod base_token_l1_behaviour; mod base_token_ratio_persister; mod base_token_ratio_provider; mod metrics; diff --git a/core/node/node_framework/src/implementations/layers/base_token/base_token_ratio_persister.rs b/core/node/node_framework/src/implementations/layers/base_token/base_token_ratio_persister.rs index 3632613379f..347d69e5536 100644 --- a/core/node/node_framework/src/implementations/layers/base_token/base_token_ratio_persister.rs +++ b/core/node/node_framework/src/implementations/layers/base_token/base_token_ratio_persister.rs @@ -1,9 +1,9 @@ -use zksync_base_token_adjuster::{BaseTokenRatioPersister, BaseTokenRatioPersisterL1Params}; +use zksync_base_token_adjuster::{BaseTokenL1Behaviour, BaseTokenRatioPersister, UpdateOnL1Params}; use zksync_config::{ configs::{base_token_adjuster::BaseTokenAdjusterConfig, wallets::Wallets}, ContractsConfig, }; -use zksync_contracts::chain_admin_contract; +use zksync_contracts::{chain_admin_contract, getters_facet_contract}; use zksync_eth_client::clients::PKSigningClient; use zksync_types::L1ChainId; @@ -83,38 +83,44 @@ impl WiringLayer for BaseTokenRatioPersisterLayer { .base_token_addr .expect("base token address is not set"); - let l1_params = - self.wallets_config - .token_multiplier_setter - .map(|token_multiplier_setter| { - let tms_private_key = token_multiplier_setter.wallet.private_key(); - let tms_address = token_multiplier_setter.wallet.address(); - let EthInterfaceResource(query_client) = input.eth_client; + let l1_behaviour = self + .wallets_config + .token_multiplier_setter + .map(|token_multiplier_setter| { + let tms_private_key = token_multiplier_setter.wallet.private_key(); + let tms_address = token_multiplier_setter.wallet.address(); + let EthInterfaceResource(query_client) = input.eth_client; - let signing_client = PKSigningClient::new_raw( - tms_private_key.clone(), - self.contracts_config.diamond_proxy_addr, - self.config.default_priority_fee_per_gas, - #[allow(clippy::useless_conversion)] - self.l1_chain_id.into(), - query_client.clone().for_component("base_token_adjuster"), - ); - BaseTokenRatioPersisterL1Params { + let signing_client = PKSigningClient::new_raw( + tms_private_key.clone(), + self.contracts_config.diamond_proxy_addr, + self.config.default_priority_fee_per_gas, + #[allow(clippy::useless_conversion)] + self.l1_chain_id.into(), + query_client.clone().for_component("base_token_adjuster"), + ); + BaseTokenL1Behaviour::UpdateOnL1 { + params: UpdateOnL1Params { eth_client: Box::new(signing_client), gas_adjuster: input.tx_params.0, token_multiplier_setter_account_address: tms_address, chain_admin_contract: chain_admin_contract(), + getters_facet_contract: getters_facet_contract(), diamond_proxy_contract_address: self.contracts_config.diamond_proxy_addr, chain_admin_contract_address: self.contracts_config.chain_admin_addr, - } - }); + config: self.config.clone(), + }, + last_persisted_l1_ratio: None, + } + }) + .unwrap_or(BaseTokenL1Behaviour::NoOp); let persister = BaseTokenRatioPersister::new( master_pool, self.config, base_token_addr, price_api_client.0, - l1_params, + l1_behaviour, ); Ok(Output { persister })