diff --git a/.github/workflows/ci-core-reusable.yml b/.github/workflows/ci-core-reusable.yml index e03608a931f7..288bed7f9671 100644 --- a/.github/workflows/ci-core-reusable.yml +++ b/.github/workflows/ci-core-reusable.yml @@ -135,7 +135,7 @@ jobs: base_token: ["Eth", "Custom"] deployment_mode: ["Rollup", "Validium"] env: - SERVER_COMPONENTS: "api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,da_dispatcher${{ matrix.consensus && ',consensus' || '' }}" + SERVER_COMPONENTS: "api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,da_dispatcher,base_token_ratio_persister${{ matrix.consensus && ',consensus' || '' }}" runs-on: [matterlabs-ci-runner] steps: @@ -309,7 +309,7 @@ jobs: runs-on: [matterlabs-ci-runner] env: - SERVER_COMPONENTS: "api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,da_dispatcher${{ matrix.consensus && ',consensus' || '' }}" + SERVER_COMPONENTS: "api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,da_dispatcher,base_token_ratio_persister${{ matrix.consensus && ',consensus' || '' }}" EXT_NODE_FLAGS: "${{ matrix.consensus && '-- --enable-consensus' || '' }}" steps: diff --git a/Cargo.lock b/Cargo.lock index dd57e952ea22..d20e9086767e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7968,6 +7968,21 @@ dependencies = [ "sha3 0.10.8", ] +[[package]] +name = "zksync_base_token_adjuster" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "rand 0.8.5", + "tokio", + "tracing", + "zksync_config", + "zksync_dal", + "zksync_types", +] + [[package]] name = "zksync_basic_types" version = "0.1.0" @@ -8628,6 +8643,16 @@ dependencies = [ "zksync_web3_decl", ] +[[package]] +name = "zksync_external_price_api" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "zksync_config", + "zksync_types", +] + [[package]] name = "zksync_health_check" version = "0.1.0" @@ -8912,10 +8937,12 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bigdecimal", "test-casing", "tokio", "tracing", "vise", + "zksync_base_token_adjuster", "zksync_config", "zksync_dal", "zksync_eth_client", @@ -8939,6 +8966,7 @@ dependencies = [ "tokio", "tracing", "trybuild", + "zksync_base_token_adjuster", "zksync_block_reverter", "zksync_circuit_breaker", "zksync_commitment_generator", @@ -9326,6 +9354,7 @@ dependencies = [ "tokio", "tracing", "vise", + "zksync_base_token_adjuster", "zksync_config", "zksync_contracts", "zksync_dal", @@ -9430,6 +9459,7 @@ name = "zksync_types" version = "0.1.0" dependencies = [ "anyhow", + "bigdecimal", "bincode", "blake2 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)", "chrono", @@ -9447,6 +9477,7 @@ dependencies = [ "strum", "thiserror", "tokio", + "tracing", "zksync_basic_types", "zksync_config", "zksync_contracts", diff --git a/Cargo.toml b/Cargo.toml index b1ec4a864856..bb47387eb264 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "core/node/contract_verification_server", "core/node/api_server", "core/node/tee_verifier_input_producer", + "core/node/base_token_adjuster", # Libraries "core/lib/db_connection", "core/lib/zksync_core_leftovers", @@ -69,6 +70,7 @@ members = [ "core/lib/web3_decl", "core/lib/snapshots_applier", "core/lib/crypto_primitives", + "core/lib/external_price_api", # Test infrastructure "core/tests/test_account", "core/tests/loadnext", @@ -248,6 +250,7 @@ zksync_types = { path = "core/lib/types" } zksync_utils = { path = "core/lib/utils" } zksync_web3_decl = { path = "core/lib/web3_decl" } zksync_crypto_primitives = { path = "core/lib/crypto_primitives" } +zksync_external_price_api = { path = "core/lib/external_price_api" } # Framework and components zksync_node_framework = { path = "core/node/node_framework" } @@ -274,3 +277,4 @@ zksync_node_consensus = { path = "core/node/consensus" } zksync_contract_verification_server = { path = "core/node/contract_verification_server" } zksync_node_api_server = { path = "core/node/api_server" } zksync_tee_verifier_input_producer = { path = "core/node/tee_verifier_input_producer" } +zksync_base_token_adjuster = {path = "core/node/base_token_adjuster"} diff --git a/core/bin/zksync_server/src/main.rs b/core/bin/zksync_server/src/main.rs index dae87e016636..51fce8e2d8d3 100644 --- a/core/bin/zksync_server/src/main.rs +++ b/core/bin/zksync_server/src/main.rs @@ -16,8 +16,9 @@ use zksync_config::{ L1Secrets, ObservabilityConfig, PrometheusConfig, ProofDataHandlerConfig, ProtectiveReadsWriterConfig, Secrets, }, - ApiConfig, ContractVerifierConfig, DADispatcherConfig, DBConfig, EthConfig, EthWatchConfig, - GasAdjusterConfig, GenesisConfig, ObjectStoreConfig, PostgresConfig, SnapshotsCreatorConfig, + ApiConfig, BaseTokenAdjusterConfig, ContractVerifierConfig, DADispatcherConfig, DBConfig, + EthConfig, EthWatchConfig, GasAdjusterConfig, GenesisConfig, ObjectStoreConfig, PostgresConfig, + SnapshotsCreatorConfig, }; use zksync_core_leftovers::{ genesis_init, is_genesis_needed, @@ -47,7 +48,7 @@ struct Cli { /// Comma-separated list of components to launch. #[arg( long, - default_value = "api,tree,eth,state_keeper,housekeeper,tee_verifier_input_producer,commitment_generator,da_dispatcher" + default_value = "api,tree,eth,state_keeper,housekeeper,tee_verifier_input_producer,commitment_generator,da_dispatcher,base_token_ratio_persister" )] components: ComponentsToRun, /// Path to the yaml config. If set, it will be used instead of env vars. @@ -271,6 +272,7 @@ fn load_env_config() -> anyhow::Result { da_dispatcher_config: DADispatcherConfig::from_env().ok(), protective_reads_writer_config: ProtectiveReadsWriterConfig::from_env().ok(), core_object_store: ObjectStoreConfig::from_env().ok(), + base_token_adjuster_config: BaseTokenAdjusterConfig::from_env().ok(), commitment_generator: None, pruning: None, snapshot_recovery: None, diff --git a/core/bin/zksync_server/src/node_builder.rs b/core/bin/zksync_server/src/node_builder.rs index b7ceadaaee6d..d33abdbbf199 100644 --- a/core/bin/zksync_server/src/node_builder.rs +++ b/core/bin/zksync_server/src/node_builder.rs @@ -21,6 +21,8 @@ use zksync_node_api_server::{ }; use zksync_node_framework::{ implementations::layers::{ + base_token_ratio_persister::BaseTokenRatioPersisterLayer, + base_token_ratio_provider::BaseTokenRatioProviderLayer, circuit_breaker_checker::CircuitBreakerCheckerLayer, commitment_generator::CommitmentGeneratorLayer, consensus::{ConsensusLayer, Mode as ConsensusMode}, @@ -57,6 +59,7 @@ use zksync_node_framework::{ }, service::{ZkStackService, ZkStackServiceBuilder}, }; +use zksync_types::SHARED_BRIDGE_ETHER_TOKEN_ADDRESS; use zksync_vlog::prometheus::PrometheusExporterConfig; /// Macro that looks into a path to fetch an optional config, @@ -148,6 +151,11 @@ impl MainNodeBuilder { } fn add_sequencer_l1_gas_layer(mut self) -> anyhow::Result { + // Ensure the BaseTokenRatioProviderResource is inserted if the base token is not ETH. + if self.contracts_config.base_token_addr != Some(SHARED_BRIDGE_ETHER_TOKEN_ADDRESS) { + self.node.add_layer(BaseTokenRatioProviderLayer {}); + } + let gas_adjuster_config = try_load_config!(self.configs.eth) .gas_adjuster .context("Gas adjuster")?; @@ -495,6 +503,14 @@ impl MainNodeBuilder { Ok(self) } + fn add_base_token_ratio_persister_layer(mut self) -> anyhow::Result { + let config = try_load_config!(self.configs.base_token_adjuster); + self.node + .add_layer(BaseTokenRatioPersisterLayer::new(config)); + + Ok(self) + } + pub fn build(mut self, mut components: Vec) -> anyhow::Result { // Add "base" layers (resources and helper tasks). self = self @@ -585,6 +601,9 @@ impl MainNodeBuilder { Component::VmRunnerProtectiveReads => { self = self.add_vm_runner_protective_reads_layer()?; } + Component::BaseTokenRatioPersister => { + self = self.add_base_token_ratio_persister_layer()?; + } } } Ok(self.node.build()?) diff --git a/core/lib/config/src/configs/base_token_adjuster.rs b/core/lib/config/src/configs/base_token_adjuster.rs new file mode 100644 index 000000000000..11d669429e05 --- /dev/null +++ b/core/lib/config/src/configs/base_token_adjuster.rs @@ -0,0 +1,31 @@ +use std::time::Duration; + +use serde::Deserialize; + +/// By default the ratio persister will run every 30 seconds. +pub const DEFAULT_INTERVAL_MS: u64 = 30_000; + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct BaseTokenAdjusterConfig { + /// How often to spark a new cycle of the ratio persister to fetch external prices and persis ratios. + #[serde(default = "BaseTokenAdjusterConfig::default_interval")] + pub price_polling_interval_ms: u64, +} + +impl Default for BaseTokenAdjusterConfig { + fn default() -> Self { + Self { + price_polling_interval_ms: Self::default_interval(), + } + } +} + +impl BaseTokenAdjusterConfig { + fn default_interval() -> u64 { + DEFAULT_INTERVAL_MS + } + + pub fn price_polling_interval(&self) -> Duration { + Duration::from_millis(self.price_polling_interval_ms) + } +} diff --git a/core/lib/config/src/configs/contracts.rs b/core/lib/config/src/configs/contracts.rs index f9bfcc7696b1..1ab032869e37 100644 --- a/core/lib/config/src/configs/contracts.rs +++ b/core/lib/config/src/configs/contracts.rs @@ -37,6 +37,7 @@ pub struct ContractsConfig { pub l2_testnet_paymaster_addr: Option
, pub l1_multicall3_addr: Address, pub ecosystem_contracts: Option, + // Used by the RPC API and by the node builder in wiring the BaseTokenRatioProvider layer. pub base_token_addr: Option
, } diff --git a/core/lib/config/src/configs/eth_sender.rs b/core/lib/config/src/configs/eth_sender.rs index 92836c74b1c6..c0e14dd68a87 100644 --- a/core/lib/config/src/configs/eth_sender.rs +++ b/core/lib/config/src/configs/eth_sender.rs @@ -153,7 +153,7 @@ impl SenderConfig { } } -#[derive(Debug, Deserialize, Copy, Clone, PartialEq)] +#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Default)] pub struct GasAdjusterConfig { /// Priority Fee to be used by GasAdjuster pub default_priority_fee_per_gas: u64, diff --git a/core/lib/config/src/configs/general.rs b/core/lib/config/src/configs/general.rs index 25aaa442c950..b7b501364c65 100644 --- a/core/lib/config/src/configs/general.rs +++ b/core/lib/config/src/configs/general.rs @@ -1,5 +1,6 @@ use crate::{ configs::{ + base_token_adjuster::BaseTokenAdjusterConfig, chain::{CircuitBreakerConfig, MempoolConfig, OperationsManagerConfig, StateKeeperConfig}, da_dispatcher::DADispatcherConfig, fri_prover_group::FriProverGroupConfig, @@ -43,4 +44,5 @@ pub struct GeneralConfig { pub snapshot_recovery: Option, pub pruning: Option, pub core_object_store: Option, + pub base_token_adjuster: Option, } diff --git a/core/lib/config/src/configs/mod.rs b/core/lib/config/src/configs/mod.rs index 6bfa874d951d..0e8730ac9141 100644 --- a/core/lib/config/src/configs/mod.rs +++ b/core/lib/config/src/configs/mod.rs @@ -1,6 +1,7 @@ // Public re-exports pub use self::{ api::ApiConfig, + base_token_adjuster::BaseTokenAdjusterConfig, commitment_generator::CommitmentGeneratorConfig, contract_verifier::ContractVerifierConfig, contracts::{ContractsConfig, EcosystemContracts}, @@ -28,6 +29,7 @@ pub use self::{ }; pub mod api; +pub mod base_token_adjuster; pub mod chain; mod commitment_generator; pub mod consensus; diff --git a/core/lib/config/src/lib.rs b/core/lib/config/src/lib.rs index 1d74e51b6728..91b5c6d480e3 100644 --- a/core/lib/config/src/lib.rs +++ b/core/lib/config/src/lib.rs @@ -1,9 +1,9 @@ #![allow(clippy::upper_case_acronyms, clippy::derive_partial_eq_without_eq)] pub use crate::configs::{ - ApiConfig, ContractVerifierConfig, ContractsConfig, DADispatcherConfig, DBConfig, EthConfig, - EthWatchConfig, GasAdjusterConfig, GenesisConfig, ObjectStoreConfig, PostgresConfig, - SnapshotsCreatorConfig, + ApiConfig, BaseTokenAdjusterConfig, ContractVerifierConfig, ContractsConfig, + DADispatcherConfig, DBConfig, EthConfig, EthWatchConfig, GasAdjusterConfig, GenesisConfig, + ObjectStoreConfig, PostgresConfig, SnapshotsCreatorConfig, }; pub mod configs; diff --git a/core/lib/dal/.sqlx/query-0fc8ede1d0962938d606c6352335afce09869d43eb88ec7fdb526ce8491e35d9.json b/core/lib/dal/.sqlx/query-0fc8ede1d0962938d606c6352335afce09869d43eb88ec7fdb526ce8491e35d9.json new file mode 100644 index 000000000000..1ad92abac368 --- /dev/null +++ b/core/lib/dal/.sqlx/query-0fc8ede1d0962938d606c6352335afce09869d43eb88ec7fdb526ce8491e35d9.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n *\n FROM\n base_token_ratios\n ORDER BY\n ratio_timestamp DESC\n LIMIT\n 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "ratio_timestamp", + "type_info": "Timestamp" + }, + { + "ordinal": 4, + "name": "numerator", + "type_info": "Numeric" + }, + { + "ordinal": 5, + "name": "denominator", + "type_info": "Numeric" + }, + { + "ordinal": 6, + "name": "used_in_l1", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "0fc8ede1d0962938d606c6352335afce09869d43eb88ec7fdb526ce8491e35d9" +} diff --git a/core/lib/dal/.sqlx/query-c5aef75dbeb520c965a0996abed9713f437db492e2075ca69e11e2ef5728ccaa.json b/core/lib/dal/.sqlx/query-c5aef75dbeb520c965a0996abed9713f437db492e2075ca69e11e2ef5728ccaa.json new file mode 100644 index 000000000000..6dd2f6cc7a9f --- /dev/null +++ b/core/lib/dal/.sqlx/query-c5aef75dbeb520c965a0996abed9713f437db492e2075ca69e11e2ef5728ccaa.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO\n base_token_ratios (numerator, denominator, ratio_timestamp, created_at, updated_at)\n VALUES\n ($1, $2, $3, NOW(), NOW())\n RETURNING\n id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Numeric", + "Numeric", + "Timestamp" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c5aef75dbeb520c965a0996abed9713f437db492e2075ca69e11e2ef5728ccaa" +} diff --git a/core/lib/dal/migrations/20240611121747_add_base_token_ratio_table.down.sql b/core/lib/dal/migrations/20240611121747_add_base_token_ratio_table.down.sql new file mode 100644 index 000000000000..e64cb3c7c408 --- /dev/null +++ b/core/lib/dal/migrations/20240611121747_add_base_token_ratio_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS base_token_ratios; diff --git a/core/lib/dal/migrations/20240611121747_add_base_token_ratio_table.up.sql b/core/lib/dal/migrations/20240611121747_add_base_token_ratio_table.up.sql new file mode 100644 index 000000000000..f4853e352802 --- /dev/null +++ b/core/lib/dal/migrations/20240611121747_add_base_token_ratio_table.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE base_token_ratios ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + + ratio_timestamp TIMESTAMP NOT NULL, + numerator NUMERIC(20,0) NOT NULL, + denominator NUMERIC(20,0) NOT NULL, + + used_in_l1 BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/core/lib/dal/src/base_token_dal.rs b/core/lib/dal/src/base_token_dal.rs new file mode 100644 index 000000000000..a8bf51d0c603 --- /dev/null +++ b/core/lib/dal/src/base_token_dal.rs @@ -0,0 +1,61 @@ +use std::num::NonZeroU64; + +use bigdecimal::{BigDecimal, FromPrimitive}; +use zksync_db_connection::{connection::Connection, error::DalResult, instrument::InstrumentExt}; +use zksync_types::base_token_ratio::BaseTokenRatio; + +use crate::{models::storage_base_token_ratio::StorageBaseTokenRatio, Core}; + +#[derive(Debug)] +pub struct BaseTokenDal<'a, 'c> { + pub(crate) storage: &'a mut Connection<'c, Core>, +} + +impl BaseTokenDal<'_, '_> { + pub async fn insert_token_ratio( + &mut self, + numerator: NonZeroU64, + denominator: NonZeroU64, + ratio_timestamp: &chrono::NaiveDateTime, + ) -> DalResult { + let row = sqlx::query!( + r#" + INSERT INTO + base_token_ratios (numerator, denominator, ratio_timestamp, created_at, updated_at) + VALUES + ($1, $2, $3, NOW(), NOW()) + RETURNING + id + "#, + BigDecimal::from_u64(numerator.get()), + BigDecimal::from_u64(denominator.get()), + ratio_timestamp, + ) + .instrument("insert_token_ratio") + .fetch_one(self.storage) + .await?; + + Ok(row.id as usize) + } + + pub async fn get_latest_ratio(&mut self) -> DalResult> { + let row = sqlx::query_as!( + StorageBaseTokenRatio, + r#" + SELECT + * + FROM + base_token_ratios + ORDER BY + ratio_timestamp DESC + LIMIT + 1 + "#, + ) + .instrument("get_latest_ratio") + .fetch_optional(self.storage) + .await?; + + Ok(row.map(|r| r.into())) + } +} diff --git a/core/lib/dal/src/lib.rs b/core/lib/dal/src/lib.rs index 5f95e440d10d..0e1badb9af76 100644 --- a/core/lib/dal/src/lib.rs +++ b/core/lib/dal/src/lib.rs @@ -12,11 +12,11 @@ pub use zksync_db_connection::{ }; use crate::{ - blocks_dal::BlocksDal, blocks_web3_dal::BlocksWeb3Dal, consensus_dal::ConsensusDal, - contract_verification_dal::ContractVerificationDal, data_availability_dal::DataAvailabilityDal, - eth_sender_dal::EthSenderDal, events_dal::EventsDal, events_web3_dal::EventsWeb3Dal, - factory_deps_dal::FactoryDepsDal, proof_generation_dal::ProofGenerationDal, - protocol_versions_dal::ProtocolVersionsDal, + base_token_dal::BaseTokenDal, blocks_dal::BlocksDal, blocks_web3_dal::BlocksWeb3Dal, + consensus_dal::ConsensusDal, contract_verification_dal::ContractVerificationDal, + data_availability_dal::DataAvailabilityDal, eth_sender_dal::EthSenderDal, + events_dal::EventsDal, events_web3_dal::EventsWeb3Dal, factory_deps_dal::FactoryDepsDal, + proof_generation_dal::ProofGenerationDal, protocol_versions_dal::ProtocolVersionsDal, protocol_versions_web3_dal::ProtocolVersionsWeb3Dal, pruning_dal::PruningDal, snapshot_recovery_dal::SnapshotRecoveryDal, snapshots_creator_dal::SnapshotsCreatorDal, snapshots_dal::SnapshotsDal, storage_logs_dal::StorageLogsDal, @@ -27,6 +27,7 @@ use crate::{ transactions_web3_dal::TransactionsWeb3Dal, vm_runner_dal::VmRunnerDal, }; +pub mod base_token_dal; pub mod blocks_dal; pub mod blocks_web3_dal; pub mod consensus; @@ -129,6 +130,8 @@ where fn data_availability_dal(&mut self) -> DataAvailabilityDal<'_, 'a>; fn vm_runner_dal(&mut self) -> VmRunnerDal<'_, 'a>; + + fn base_token_dal(&mut self) -> BaseTokenDal<'_, 'a>; } #[derive(Clone, Debug)] @@ -251,4 +254,8 @@ impl<'a> CoreDal<'a> for Connection<'a, Core> { fn vm_runner_dal(&mut self) -> VmRunnerDal<'_, 'a> { VmRunnerDal { storage: self } } + + fn base_token_dal(&mut self) -> BaseTokenDal<'_, 'a> { + BaseTokenDal { storage: self } + } } diff --git a/core/lib/dal/src/models/mod.rs b/core/lib/dal/src/models/mod.rs index 34c914af59dd..1e852e3f6364 100644 --- a/core/lib/dal/src/models/mod.rs +++ b/core/lib/dal/src/models/mod.rs @@ -3,6 +3,7 @@ use anyhow::Context as _; use zksync_db_connection::error::SqlxContext; use zksync_types::{ProtocolVersionId, H160, H256}; +pub mod storage_base_token_ratio; pub(crate) mod storage_data_availability; pub mod storage_eth_tx; pub mod storage_event; diff --git a/core/lib/dal/src/models/storage_base_token_ratio.rs b/core/lib/dal/src/models/storage_base_token_ratio.rs new file mode 100644 index 000000000000..f486aefd4085 --- /dev/null +++ b/core/lib/dal/src/models/storage_base_token_ratio.rs @@ -0,0 +1,31 @@ +use std::num::NonZeroU64; + +use bigdecimal::{BigDecimal, ToPrimitive}; +use chrono::NaiveDateTime; +use zksync_types::base_token_ratio::BaseTokenRatio; + +/// Represents a row in the `base_token_ratios` table. +#[derive(Debug, Clone)] +pub struct StorageBaseTokenRatio { + pub id: i64, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub ratio_timestamp: NaiveDateTime, + pub numerator: BigDecimal, + pub denominator: BigDecimal, + pub used_in_l1: bool, +} + +impl From for BaseTokenRatio { + fn from(row: StorageBaseTokenRatio) -> BaseTokenRatio { + BaseTokenRatio { + id: row.id as u32, + ratio_timestamp: row.ratio_timestamp.and_utc(), + numerator: NonZeroU64::new(row.numerator.to_u64().expect("numerator is not u64")) + .unwrap(), + denominator: NonZeroU64::new(row.denominator.to_u64().expect("denominator is not u64")) + .unwrap(), + used_in_l1: row.used_in_l1, + } + } +} diff --git a/core/lib/env_config/src/base_token_adjuster.rs b/core/lib/env_config/src/base_token_adjuster.rs new file mode 100644 index 000000000000..5e4ef39671ca --- /dev/null +++ b/core/lib/env_config/src/base_token_adjuster.rs @@ -0,0 +1,9 @@ +use zksync_config::configs::BaseTokenAdjusterConfig; + +use crate::{envy_load, FromEnv}; + +impl FromEnv for BaseTokenAdjusterConfig { + fn from_env() -> anyhow::Result { + envy_load("base_token_adjuster", "BASE_TOKEN_ADJUSTER_") + } +} diff --git a/core/lib/env_config/src/lib.rs b/core/lib/env_config/src/lib.rs index 67078fcd4513..bd7aa035b68b 100644 --- a/core/lib/env_config/src/lib.rs +++ b/core/lib/env_config/src/lib.rs @@ -21,6 +21,7 @@ mod proof_data_handler; mod snapshots_creator; mod utils; +mod base_token_adjuster; mod da_dispatcher; mod genesis; #[cfg(test)] diff --git a/core/lib/external_price_api/Cargo.toml b/core/lib/external_price_api/Cargo.toml new file mode 100644 index 000000000000..c75ff5851d75 --- /dev/null +++ b/core/lib/external_price_api/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "zksync_external_price_api" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +async-trait.workspace = true +anyhow.workspace = true + +zksync_config.workspace = true +zksync_types.workspace = true diff --git a/core/lib/external_price_api/README.md b/core/lib/external_price_api/README.md new file mode 100644 index 000000000000..d1604bbae7e7 --- /dev/null +++ b/core/lib/external_price_api/README.md @@ -0,0 +1,7 @@ +# Price API Client + +This crate provides a simple trait to be implemented by clients interacting with external price APIs to fetch +ETH<->BaseToken ratio. + +All clients should be implemented here and used by the node framework layer, which will be agnostic to the number of +clients available. diff --git a/core/lib/external_price_api/src/lib.rs b/core/lib/external_price_api/src/lib.rs new file mode 100644 index 000000000000..4128c0f231f8 --- /dev/null +++ b/core/lib/external_price_api/src/lib.rs @@ -0,0 +1,11 @@ +use std::fmt; + +use async_trait::async_trait; +use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address}; + +/// Trait that defines the interface for a client connecting with an external API to get prices. +#[async_trait] +pub trait PriceAPIClient: Sync + Send + fmt::Debug { + /// Returns the price for the input token address in $USD. + async fn fetch_price(&self, token_address: Address) -> anyhow::Result; +} diff --git a/core/lib/protobuf_config/src/base_token_adjuster.rs b/core/lib/protobuf_config/src/base_token_adjuster.rs new file mode 100644 index 000000000000..d8dea17daec0 --- /dev/null +++ b/core/lib/protobuf_config/src/base_token_adjuster.rs @@ -0,0 +1,22 @@ +use zksync_config::configs::{self}; +use zksync_protobuf::ProtoRepr; + +use crate::proto::base_token_adjuster as proto; + +impl ProtoRepr for proto::BaseTokenAdjuster { + type Type = configs::base_token_adjuster::BaseTokenAdjusterConfig; + + fn read(&self) -> anyhow::Result { + Ok(configs::base_token_adjuster::BaseTokenAdjusterConfig { + price_polling_interval_ms: self + .price_polling_interval_ms + .expect("price_polling_interval_ms"), + }) + } + + fn build(this: &Self::Type) -> Self { + Self { + price_polling_interval_ms: Some(this.price_polling_interval_ms), + } + } +} diff --git a/core/lib/protobuf_config/src/general.rs b/core/lib/protobuf_config/src/general.rs index 9215ad5ae7d6..8993adeccb2c 100644 --- a/core/lib/protobuf_config/src/general.rs +++ b/core/lib/protobuf_config/src/general.rs @@ -43,6 +43,8 @@ impl ProtoRepr for proto::GeneralConfig { .context("protective_reads_writer")?, core_object_store: read_optional_repr(&self.core_object_store) .context("core_object_store")?, + base_token_adjuster: read_optional_repr(&self.base_token_adjuster) + .context("base_token_adjuster")?, commitment_generator: read_optional_repr(&self.commitment_generator) .context("commitment_generator")?, pruning: read_optional_repr(&self.pruning).context("pruning")?, @@ -88,6 +90,7 @@ impl ProtoRepr for proto::GeneralConfig { snapshot_recovery: this.snapshot_recovery.as_ref().map(ProtoRepr::build), pruning: this.pruning.as_ref().map(ProtoRepr::build), core_object_store: this.core_object_store.as_ref().map(ProtoRepr::build), + base_token_adjuster: this.base_token_adjuster.as_ref().map(ProtoRepr::build), } } } diff --git a/core/lib/protobuf_config/src/lib.rs b/core/lib/protobuf_config/src/lib.rs index 8b9ed28e23e2..fe260c6099b9 100644 --- a/core/lib/protobuf_config/src/lib.rs +++ b/core/lib/protobuf_config/src/lib.rs @@ -5,6 +5,7 @@ //! * protobuf json format mod api; +mod base_token_adjuster; mod chain; mod circuit_breaker; mod commitment_generator; 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 new file mode 100644 index 000000000000..67e97dd14cda --- /dev/null +++ b/core/lib/protobuf_config/src/proto/config/base_token_adjuster.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package zksync.config.base_token_adjuster; + +message BaseTokenAdjuster { + optional uint64 price_polling_interval_ms = 1; +} diff --git a/core/lib/protobuf_config/src/proto/config/general.proto b/core/lib/protobuf_config/src/proto/config/general.proto index 3931e708af87..457890158e54 100644 --- a/core/lib/protobuf_config/src/proto/config/general.proto +++ b/core/lib/protobuf_config/src/proto/config/general.proto @@ -19,6 +19,7 @@ import "zksync/config/commitment_generator.proto"; import "zksync/config/snapshot_recovery.proto"; import "zksync/config/pruning.proto"; import "zksync/config/object_store.proto"; +import "zksync/config/base_token_adjuster.proto"; message GeneralConfig { optional config.database.Postgres postgres = 1; @@ -47,4 +48,5 @@ message GeneralConfig { optional config.pruning.Pruning pruning = 36; optional config.commitment_generator.CommitmentGenerator commitment_generator = 37; optional config.da_dispatcher.DataAvailabilityDispatcher da_dispatcher = 38; + optional config.base_token_adjuster.BaseTokenAdjuster base_token_adjuster = 39; } diff --git a/core/lib/types/Cargo.toml b/core/lib/types/Cargo.toml index a562cccacbc1..673a0f35a26a 100644 --- a/core/lib/types/Cargo.toml +++ b/core/lib/types/Cargo.toml @@ -27,12 +27,14 @@ once_cell.workspace = true rlp.workspace = true serde.workspace = true serde_json.workspace = true +bigdecimal.workspace = true strum = { workspace = true, features = ["derive"] } thiserror.workspace = true num_enum.workspace = true hex.workspace = true prost.workspace = true itertools.workspace = true +tracing.workspace = true # Crypto stuff secp256k1.workspace = true diff --git a/core/lib/types/src/base_token_ratio.rs b/core/lib/types/src/base_token_ratio.rs new file mode 100644 index 000000000000..0782e67ab4b0 --- /dev/null +++ b/core/lib/types/src/base_token_ratio.rs @@ -0,0 +1,22 @@ +use std::num::NonZeroU64; + +use chrono::{DateTime, Utc}; + +/// Represents the base token to ETH conversion ratio at a given point in time. +#[derive(Debug, Clone)] +pub struct BaseTokenRatio { + pub id: u32, + pub ratio_timestamp: DateTime, + pub numerator: NonZeroU64, + pub denominator: NonZeroU64, + pub used_in_l1: bool, +} + +/// Struct to represent API response containing denominator, numerator, and timestamp. +#[derive(Debug)] +pub struct BaseTokenAPIRatio { + pub numerator: NonZeroU64, + pub denominator: NonZeroU64, + /// Either the timestamp of the quote or the timestamp of the request. + pub ratio_timestamp: DateTime, +} diff --git a/core/lib/types/src/fee_model.rs b/core/lib/types/src/fee_model.rs index 9c2cc4d2aaf8..38d785113e5f 100644 --- a/core/lib/types/src/fee_model.rs +++ b/core/lib/types/src/fee_model.rs @@ -1,3 +1,6 @@ +use std::num::NonZeroU64; + +use bigdecimal::{BigDecimal, ToPrimitive}; use serde::{Deserialize, Serialize}; use zksync_config::configs::chain::{FeeModelVersion, StateKeeperConfig}; use zksync_system_constants::L1_GAS_PER_PUBDATA_BYTE; @@ -236,9 +239,86 @@ pub struct FeeParamsV1 { #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct FeeParamsV2 { - pub config: FeeModelConfigV2, - pub l1_gas_price: u64, - pub l1_pubdata_price: u64, + config: FeeModelConfigV2, + l1_gas_price: u64, + l1_pubdata_price: u64, + conversion_ratio: BaseTokenConversionRatio, +} + +impl FeeParamsV2 { + pub fn new( + config: FeeModelConfigV2, + l1_gas_price: u64, + l1_pubdata_price: u64, + conversion_ratio: BaseTokenConversionRatio, + ) -> Self { + Self { + config, + l1_gas_price, + l1_pubdata_price, + conversion_ratio, + } + } + + /// Returns the fee model config with the minimal L2 gas price denominated in the chain's base token (WEI or equivalent). + pub fn config(&self) -> FeeModelConfigV2 { + FeeModelConfigV2 { + minimal_l2_gas_price: self.convert_to_base_token(self.config.minimal_l2_gas_price), + ..self.config + } + } + + /// Returns the l1 gas price denominated in the chain's base token (WEI or equivalent). + pub fn l1_gas_price(&self) -> u64 { + self.convert_to_base_token(self.l1_gas_price) + } + + /// Returns the l1 pubdata price denominated in the chain's base token (WEI or equivalent). + pub fn l1_pubdata_price(&self) -> u64 { + self.convert_to_base_token(self.l1_pubdata_price) + } + + /// Converts the fee param to the base token. + fn convert_to_base_token(&self, price_in_wei: u64) -> u64 { + let conversion_ratio = BigDecimal::from(self.conversion_ratio.numerator.get()) + / BigDecimal::from(self.conversion_ratio.denominator.get()); + let converted_price_bd = BigDecimal::from(price_in_wei) * conversion_ratio; + + // Match on the converted price to ensure it can be represented as a u64 + match converted_price_bd.to_u64() { + Some(converted_price) => converted_price, + None => { + if converted_price_bd > BigDecimal::from(u64::MAX) { + tracing::warn!( + "Conversion to base token price failed: converted price is too large: {}. Using u64::MAX instead.", + converted_price_bd + ); + } else { + panic!( + "Conversion to base token price failed: converted price is not a valid u64: {}", + converted_price_bd + ); + } + u64::MAX + } + } + } +} + +/// The struct that represents the BaseToken<->ETH conversion ratio. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct BaseTokenConversionRatio { + pub numerator: NonZeroU64, + pub denominator: NonZeroU64, +} + +impl Default for BaseTokenConversionRatio { + fn default() -> Self { + Self { + numerator: NonZeroU64::new(1).unwrap(), + denominator: NonZeroU64::new(1).unwrap(), + } + } } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] diff --git a/core/lib/types/src/lib.rs b/core/lib/types/src/lib.rs index 3c3a96c297d7..105d43aa6c6c 100644 --- a/core/lib/types/src/lib.rs +++ b/core/lib/types/src/lib.rs @@ -57,6 +57,7 @@ pub mod vm_trace; pub mod zk_evm_types; pub mod api; +pub mod base_token_ratio; pub mod eth_sender; pub mod helpers; pub mod proto; diff --git a/core/lib/zksync_core_leftovers/src/lib.rs b/core/lib/zksync_core_leftovers/src/lib.rs index b760a0b7e426..a665c40babd9 100644 --- a/core/lib/zksync_core_leftovers/src/lib.rs +++ b/core/lib/zksync_core_leftovers/src/lib.rs @@ -90,6 +90,8 @@ pub enum Component { DADispatcher, /// VM runner-based component that saves protective reads to Postgres. VmRunnerProtectiveReads, + /// A component to fetch and persist ETH<->BaseToken conversion ratios for chains with custom base tokens. + BaseTokenRatioPersister, } #[derive(Debug)] @@ -130,6 +132,9 @@ impl FromStr for Components { "vm_runner_protective_reads" => { Ok(Components(vec![Component::VmRunnerProtectiveReads])) } + "base_token_ratio_persister" => { + Ok(Components(vec![Component::BaseTokenRatioPersister])) + } other => Err(format!("{} is not a valid component name", other)), } } diff --git a/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs b/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs index c45b8cb8687b..3b4c8a53b84f 100644 --- a/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs +++ b/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs @@ -16,8 +16,9 @@ use zksync_config::{ GeneralConfig, ObservabilityConfig, PrometheusConfig, ProofDataHandlerConfig, ProtectiveReadsWriterConfig, PruningConfig, SnapshotRecoveryConfig, }, - ApiConfig, ContractVerifierConfig, DADispatcherConfig, DBConfig, EthConfig, EthWatchConfig, - GasAdjusterConfig, ObjectStoreConfig, PostgresConfig, SnapshotsCreatorConfig, + ApiConfig, BaseTokenAdjusterConfig, ContractVerifierConfig, DADispatcherConfig, DBConfig, + EthConfig, EthWatchConfig, GasAdjusterConfig, ObjectStoreConfig, PostgresConfig, + SnapshotsCreatorConfig, }; use zksync_protobuf::repr::ProtoRepr; @@ -66,6 +67,7 @@ pub struct TempConfigStore { pub da_dispatcher_config: Option, pub protective_reads_writer_config: Option, pub core_object_store: Option, + pub base_token_adjuster_config: Option, pub commitment_generator: Option, pub pruning: Option, pub snapshot_recovery: Option, @@ -97,6 +99,7 @@ impl TempConfigStore { da_dispatcher_config: self.da_dispatcher_config.clone(), protective_reads_writer_config: self.protective_reads_writer_config.clone(), core_object_store: self.core_object_store.clone(), + base_token_adjuster: self.base_token_adjuster_config.clone(), commitment_generator: self.commitment_generator.clone(), snapshot_recovery: self.snapshot_recovery.clone(), pruning: self.pruning.clone(), diff --git a/core/node/base_token_adjuster/Cargo.toml b/core/node/base_token_adjuster/Cargo.toml new file mode 100644 index 000000000000..7e5c5bcaae43 --- /dev/null +++ b/core/node/base_token_adjuster/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "zksync_base_token_adjuster" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + + +[dependencies] +zksync_dal.workspace = true +zksync_config.workspace = true +zksync_types.workspace = true + +tokio = { workspace = true, features = ["time"] } +anyhow.workspace = true +tracing.workspace = true +chrono.workspace = true +rand.workspace = true +async-trait.workspace = true diff --git a/core/node/base_token_adjuster/README.md b/core/node/base_token_adjuster/README.md new file mode 100644 index 000000000000..c5b6dec2b17c --- /dev/null +++ b/core/node/base_token_adjuster/README.md @@ -0,0 +1,20 @@ +# Base Token Adjuster + +This crate contains all the logic to handle ZK Chain with custom base tokens. + +## Overview + +### The Base Token Ratio Persister + +Contains the building blockss for the `BaseTokenRatioPersisterLayer`. + +- Connects with external APIs to get the current price of the base token and of ETH. +- Persists the ETH<->BaseToken ratio in the database. +- Upon certain configured threshold, update the L1 ETH<->BaseToken conversion ratio. + +### The Base Token Ratio Provider + +Contains the building blocks for the `BaseTokenRatioProviderLayer`. + +- Periodically fetches from the DB and caches the latest ETH<->BaseToken conversion ratio. +- Exposes this ratio upon request. 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 new file mode 100644 index 000000000000..b730737b992f --- /dev/null +++ b/core/node/base_token_adjuster/src/base_token_ratio_persister.rs @@ -0,0 +1,76 @@ +use std::{fmt::Debug, num::NonZero}; + +use anyhow::Context as _; +use chrono::Utc; +use tokio::sync::watch; +use zksync_config::configs::base_token_adjuster::BaseTokenAdjusterConfig; +use zksync_dal::{ConnectionPool, Core, CoreDal}; +use zksync_types::base_token_ratio::BaseTokenAPIRatio; + +#[derive(Debug, Clone)] +pub struct BaseTokenRatioPersister { + pool: ConnectionPool, + config: BaseTokenAdjusterConfig, +} + +impl BaseTokenRatioPersister { + pub fn new(pool: ConnectionPool, config: BaseTokenAdjusterConfig) -> Self { + Self { pool, config } + } + + /// Main loop for the base token ratio persister. + /// Orchestrates fetching a new ratio, persisting it, and conditionally updating the L1 with it. + pub async fn run(&mut self, mut stop_receiver: watch::Receiver) -> anyhow::Result<()> { + let mut timer = tokio::time::interval(self.config.price_polling_interval()); + let pool = self.pool.clone(); + + while !*stop_receiver.borrow_and_update() { + tokio::select! { + _ = timer.tick() => { /* continue iterations */ } + _ = stop_receiver.changed() => break, + } + + let new_ratio = self.fetch_new_ratio().await?; + self.persist_ratio(&new_ratio, &pool).await?; + // TODO(PE-128): Update L1 ratio + } + + tracing::info!("Stop signal received, base_token_ratio_persister is shutting down"); + Ok(()) + } + + // TODO (PE-135): Use real API client to fetch new ratio through self.PriceAPIClient & mock for tests. + // For now, these are hard coded dummy values. + async fn fetch_new_ratio(&self) -> anyhow::Result { + let ratio_timestamp = Utc::now(); + + Ok(BaseTokenAPIRatio { + numerator: NonZero::new(1).unwrap(), + denominator: NonZero::new(100000).unwrap(), + ratio_timestamp, + }) + } + + async fn persist_ratio( + &self, + api_price: &BaseTokenAPIRatio, + pool: &ConnectionPool, + ) -> anyhow::Result { + let mut conn = pool + .connection_tagged("base_token_ratio_persister") + .await + .context("Failed to obtain connection to the database")?; + + let id = conn + .base_token_dal() + .insert_token_ratio( + api_price.numerator, + api_price.denominator, + &api_price.ratio_timestamp.naive_utc(), + ) + .await + .context("Failed to insert base token ratio into the database")?; + + Ok(id) + } +} diff --git a/core/node/base_token_adjuster/src/base_token_ratio_provider.rs b/core/node/base_token_adjuster/src/base_token_ratio_provider.rs new file mode 100644 index 000000000000..39a96556f8de --- /dev/null +++ b/core/node/base_token_adjuster/src/base_token_ratio_provider.rs @@ -0,0 +1,124 @@ +use std::{fmt::Debug, num::NonZeroU64, time::Duration}; + +use anyhow::Context; +use async_trait::async_trait; +use tokio::sync::watch; +use zksync_dal::{ConnectionPool, Core, CoreDal}; +use zksync_types::fee_model::BaseTokenConversionRatio; + +const CACHE_UPDATE_INTERVAL: Duration = Duration::from_millis(500); + +#[async_trait] +pub trait BaseTokenRatioProvider: Debug + Send + Sync { + fn get_conversion_ratio(&self) -> BaseTokenConversionRatio; +} + +#[derive(Debug, Clone)] +pub struct DBBaseTokenRatioProvider { + pub pool: ConnectionPool, + pub latest_ratio: BaseTokenConversionRatio, +} + +impl DBBaseTokenRatioProvider { + pub async fn new(pool: ConnectionPool) -> anyhow::Result { + let mut fetcher = Self { + pool, + latest_ratio: BaseTokenConversionRatio::default(), + }; + fetcher.latest_ratio = fetcher.get_latest_price().await?; + + // TODO(PE-129): Implement latest ratio usability logic. + + tracing::debug!( + "Starting the base token ratio provider with conversion ratio: {:?}", + fetcher.latest_ratio + ); + Ok(fetcher) + } + + pub async fn run(&mut self, mut stop_receiver: watch::Receiver) -> anyhow::Result<()> { + let mut timer = tokio::time::interval(CACHE_UPDATE_INTERVAL); + + while !*stop_receiver.borrow_and_update() { + tokio::select! { + _ = timer.tick() => { /* continue iterations */ } + _ = stop_receiver.changed() => break, + } + + let latest_storage_ratio = self.get_latest_price().await?; + + // TODO(PE-129): Implement latest ratio usability logic. + self.latest_ratio = BaseTokenConversionRatio { + numerator: latest_storage_ratio.numerator, + denominator: latest_storage_ratio.denominator, + }; + } + + tracing::info!("Stop signal received, base_token_ratio_provider is shutting down"); + Ok(()) + } + + async fn get_latest_price(&self) -> anyhow::Result { + let latest_storage_ratio = self + .pool + .connection_tagged("db_base_token_ratio_provider") + .await + .context("Failed to obtain connection to the database")? + .base_token_dal() + .get_latest_ratio() + .await; + + match latest_storage_ratio { + Ok(Some(latest_storage_price)) => Ok(BaseTokenConversionRatio { + numerator: latest_storage_price.numerator, + denominator: latest_storage_price.denominator, + }), + Ok(None) => { + // TODO(PE-136): Insert initial ratio from genesis. + // Though the DB should be populated very soon after the server starts, it is possible + // to have no ratios in the DB right after genesis. Having initial ratios in the DB + // from the genesis stage will eliminate this possibility. + tracing::error!("No latest price found in the database. Using default ratio."); + Ok(BaseTokenConversionRatio::default()) + } + Err(err) => anyhow::bail!("Failed to get latest base token ratio: {:?}", err), + } + } +} + +#[async_trait] +impl BaseTokenRatioProvider for DBBaseTokenRatioProvider { + fn get_conversion_ratio(&self) -> BaseTokenConversionRatio { + self.latest_ratio + } +} + +// Struct for a no-op BaseTokenRatioProvider (conversion ratio is either always 1:1 or a forced ratio). +#[derive(Debug, Clone)] +pub struct NoOpRatioProvider { + pub latest_ratio: BaseTokenConversionRatio, +} + +impl NoOpRatioProvider { + pub fn new(latest_ratio: BaseTokenConversionRatio) -> Self { + Self { latest_ratio } + } +} + +impl Default for NoOpRatioProvider { + fn default() -> Self { + Self { + latest_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(1).unwrap(), + denominator: NonZeroU64::new(1).unwrap(), + }, + } + } +} + +#[async_trait] +impl BaseTokenRatioProvider for NoOpRatioProvider { + fn get_conversion_ratio(&self) -> BaseTokenConversionRatio { + self.latest_ratio + } +} diff --git a/core/node/base_token_adjuster/src/lib.rs b/core/node/base_token_adjuster/src/lib.rs new file mode 100644 index 000000000000..2340ca56c2a7 --- /dev/null +++ b/core/node/base_token_adjuster/src/lib.rs @@ -0,0 +1,9 @@ +pub use self::{ + base_token_ratio_persister::BaseTokenRatioPersister, + base_token_ratio_provider::{ + BaseTokenRatioProvider, DBBaseTokenRatioProvider, NoOpRatioProvider, + }, +}; + +mod base_token_ratio_persister; +mod base_token_ratio_provider; diff --git a/core/node/fee_model/Cargo.toml b/core/node/fee_model/Cargo.toml index 7ac3c1d32e88..006a2c22da7e 100644 --- a/core/node/fee_model/Cargo.toml +++ b/core/node/fee_model/Cargo.toml @@ -17,6 +17,8 @@ zksync_config.workspace = true zksync_eth_client.workspace = true zksync_utils.workspace = true zksync_web3_decl.workspace = true +zksync_base_token_adjuster.workspace = true +bigdecimal.workspace = true tokio = { workspace = true, features = ["time"] } anyhow.workspace = true diff --git a/core/node/fee_model/src/lib.rs b/core/node/fee_model/src/lib.rs index 793b5d4f8441..00d804de6c81 100644 --- a/core/node/fee_model/src/lib.rs +++ b/core/node/fee_model/src/lib.rs @@ -1,6 +1,8 @@ use std::{fmt, sync::Arc}; use anyhow::Context as _; +use async_trait::async_trait; +use zksync_base_token_adjuster::BaseTokenRatioProvider; use zksync_dal::{ConnectionPool, Core, CoreDal}; use zksync_types::{ fee_model::{ @@ -16,7 +18,7 @@ use crate::l1_gas_price::GasAdjuster; pub mod l1_gas_price; /// Trait responsible for providing fee info for a batch -#[async_trait::async_trait] +#[async_trait] pub trait BatchFeeModelInputProvider: fmt::Debug + 'static + Send + Sync { /// Returns the batch fee with scaling applied. This may be used to account for the fact that the L1 gas and pubdata prices may fluctuate, esp. /// in API methods that should return values that are valid for some period of time after the estimation was done. @@ -42,7 +44,7 @@ pub trait BatchFeeModelInputProvider: fmt::Debug + 'static + Send + Sync { }) } - /// Returns the fee model parameters. + /// Returns the fee model parameters using the denomination of the base token used (WEI for ETH). fn get_fee_model_params(&self) -> FeeParams; } @@ -53,15 +55,17 @@ impl dyn BatchFeeModelInputProvider { } } -/// The struct that represents the batch fee input provider to be used in the main node of the server, i.e. -/// it explicitly gets the L1 gas price from the provider and uses it to calculate the batch fee input instead of getting -/// it from other node. +/// The struct that represents the batch fee input provider to be used in the main node of the server. +/// This struct gets the L1 gas price directly from the provider rather than from another node, as is the +/// case with the external node. #[derive(Debug)] pub struct MainNodeFeeInputProvider { provider: Arc, + base_token_ratio_provider: Arc, config: FeeModelConfig, } +#[async_trait] impl BatchFeeModelInputProvider for MainNodeFeeInputProvider { fn get_fee_model_params(&self) -> FeeParams { match self.config { @@ -69,18 +73,27 @@ impl BatchFeeModelInputProvider for MainNodeFeeInputProvider { config, l1_gas_price: self.provider.estimate_effective_gas_price(), }), - FeeModelConfig::V2(config) => FeeParams::V2(FeeParamsV2 { + FeeModelConfig::V2(config) => FeeParams::V2(FeeParamsV2::new( config, - l1_gas_price: self.provider.estimate_effective_gas_price(), - l1_pubdata_price: self.provider.estimate_effective_pubdata_price(), - }), + self.provider.estimate_effective_gas_price(), + self.provider.estimate_effective_pubdata_price(), + self.base_token_ratio_provider.get_conversion_ratio(), + )), } } } impl MainNodeFeeInputProvider { - pub fn new(provider: Arc, config: FeeModelConfig) -> Self { - Self { provider, config } + pub fn new( + provider: Arc, + base_token_ratio_provider: Arc, + config: FeeModelConfig, + ) -> Self { + Self { + provider, + base_token_ratio_provider, + config, + } } } @@ -104,7 +117,7 @@ impl ApiFeeInputProvider { } } -#[async_trait::async_trait] +#[async_trait] impl BatchFeeModelInputProvider for ApiFeeInputProvider { async fn get_batch_fee_input_scaled( &self, @@ -156,11 +169,9 @@ fn compute_batch_fee_model_input_v2( l1_gas_price_scale_factor: f64, l1_pubdata_price_scale_factor: f64, ) -> PubdataIndependentBatchFeeModelInput { - let FeeParamsV2 { - config, - l1_gas_price, - l1_pubdata_price, - } = params; + let config = params.config(); + let l1_gas_price = params.l1_gas_price(); + let l1_pubdata_price = params.l1_pubdata_price(); let FeeModelConfigV2 { minimal_l2_gas_price, @@ -227,6 +238,7 @@ impl Default for MockBatchFeeParamsProvider { } } +#[async_trait] impl BatchFeeModelInputProvider for MockBatchFeeParamsProvider { fn get_fee_model_params(&self) -> FeeParams { self.0 @@ -235,6 +247,13 @@ impl BatchFeeModelInputProvider for MockBatchFeeParamsProvider { #[cfg(test)] mod tests { + use std::num::NonZeroU64; + + use zksync_base_token_adjuster::NoOpRatioProvider; + use zksync_config::{configs::eth_sender::PubdataSendingMode, GasAdjusterConfig}; + use zksync_eth_client::{clients::MockEthereum, BaseFees}; + use zksync_types::{commitment::L1BatchCommitmentMode, fee_model::BaseTokenConversionRatio}; + use super::*; // To test that overflow never happens, we'll use giant L1 gas price, i.e. @@ -261,11 +280,12 @@ mod tests { max_pubdata_per_batch: 100_000, }; - let params = FeeParamsV2 { + let params = FeeParamsV2::new( config, - l1_gas_price: GIANT_L1_GAS_PRICE, - l1_pubdata_price: GIANT_L1_GAS_PRICE, - }; + GIANT_L1_GAS_PRICE, + GIANT_L1_GAS_PRICE, + BaseTokenConversionRatio::default(), + ); // We'll use scale factor of 3.0 let input = compute_batch_fee_model_input_v2(params, 3.0, 3.0); @@ -287,11 +307,12 @@ mod tests { max_pubdata_per_batch: 100_000, }; - let params = FeeParamsV2 { + let params = FeeParamsV2::new( config, - l1_gas_price: SMALL_L1_GAS_PRICE, - l1_pubdata_price: SMALL_L1_GAS_PRICE, - }; + SMALL_L1_GAS_PRICE, + SMALL_L1_GAS_PRICE, + BaseTokenConversionRatio::default(), + ); let input = compute_batch_fee_model_input_v2(params, 1.0, 1.0); @@ -312,11 +333,12 @@ mod tests { max_pubdata_per_batch: 100_000, }; - let params = FeeParamsV2 { + let params = FeeParamsV2::new( config, - l1_gas_price: GIANT_L1_GAS_PRICE, - l1_pubdata_price: GIANT_L1_GAS_PRICE, - }; + GIANT_L1_GAS_PRICE, + GIANT_L1_GAS_PRICE, + BaseTokenConversionRatio::default(), + ); let input = compute_batch_fee_model_input_v2(params, 1.0, 1.0); assert_eq!(input.l1_gas_price, GIANT_L1_GAS_PRICE); @@ -327,7 +349,7 @@ mod tests { } #[test] - fn test_compute_batch_fee_model_input_v2_only_compute_overhead() { + fn test_compute_baxtch_fee_model_input_v2_only_compute_overhead() { // Here we use sensible config, but when only compute is used to close the batch let config = FeeModelConfigV2 { minimal_l2_gas_price: 100_000_000_000, @@ -338,11 +360,12 @@ mod tests { max_pubdata_per_batch: 100_000, }; - let params = FeeParamsV2 { + let params = FeeParamsV2::new( config, - l1_gas_price: GIANT_L1_GAS_PRICE, - l1_pubdata_price: GIANT_L1_GAS_PRICE, - }; + GIANT_L1_GAS_PRICE, + GIANT_L1_GAS_PRICE, + BaseTokenConversionRatio::default(), + ); let input = compute_batch_fee_model_input_v2(params, 1.0, 1.0); assert_eq!(input.l1_gas_price, GIANT_L1_GAS_PRICE); @@ -364,19 +387,22 @@ mod tests { max_pubdata_per_batch: 100_000, }; - let base_params = FeeParamsV2 { - config: base_config, - l1_gas_price: 1_000_000_000, - l1_pubdata_price: 1_000_000_000, - }; + let base_params = FeeParamsV2::new( + base_config, + 1_000_000_000, + 1_000_000_000, + BaseTokenConversionRatio::default(), + ); let base_input = compute_batch_fee_model_input_v2(base_params, 1.0, 1.0); let base_input_larger_l1_gas_price = compute_batch_fee_model_input_v2( - FeeParamsV2 { - l1_gas_price: base_params.l1_gas_price * 2, - ..base_params - }, + FeeParamsV2::new( + base_config, + 2_000_000_000, // double the L1 gas price + 1_000_000_000, + BaseTokenConversionRatio::default(), + ), 1.0, 1.0, ); @@ -396,10 +422,12 @@ mod tests { ); let base_input_larger_pubdata_price = compute_batch_fee_model_input_v2( - FeeParamsV2 { - l1_pubdata_price: base_params.l1_pubdata_price * 2, - ..base_params - }, + FeeParamsV2::new( + base_config, + 1_000_000_000, + 2_000_000_000, // double the L1 pubdata price + BaseTokenConversionRatio::default(), + ), 1.0, 1.0, ); @@ -419,13 +447,15 @@ mod tests { ); let base_input_larger_max_gas = compute_batch_fee_model_input_v2( - FeeParamsV2 { - config: FeeModelConfigV2 { + FeeParamsV2::new( + FeeModelConfigV2 { max_gas_per_batch: base_config.max_gas_per_batch * 2, ..base_config }, - ..base_params - }, + base_params.l1_gas_price(), + base_params.l1_pubdata_price(), + BaseTokenConversionRatio::default(), + ), 1.0, 1.0, ); @@ -439,13 +469,15 @@ mod tests { ); let base_input_larger_max_pubdata = compute_batch_fee_model_input_v2( - FeeParamsV2 { - config: FeeModelConfigV2 { + FeeParamsV2::new( + FeeModelConfigV2 { max_pubdata_per_batch: base_config.max_pubdata_per_batch * 2, ..base_config }, - ..base_params - }, + base_params.l1_gas_price(), + base_params.l1_pubdata_price(), + BaseTokenConversionRatio::default(), + ), 1.0, 1.0, ); @@ -458,4 +490,194 @@ mod tests { "Max pubdata increase lowers pubdata price" ); } + + #[tokio::test] + async fn test_get_fee_model_params() { + struct TestCase { + name: &'static str, + conversion_ratio: BaseTokenConversionRatio, + input_minimal_l2_gas_price: u64, // Wei denomination + input_l1_gas_price: u64, // Wei + input_l1_pubdata_price: u64, // Wei + expected_minimal_l2_gas_price: u64, // BaseToken denomination + expected_l1_gas_price: u64, // BaseToken + expected_l1_pubdata_price: u64, // BaseToken + } + let test_cases = vec![ + TestCase { + name: "1 ETH = 2 BaseToken", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(2).unwrap(), + denominator: NonZeroU64::new(1).unwrap(), + }, + input_minimal_l2_gas_price: 1000, + input_l1_gas_price: 2000, + input_l1_pubdata_price: 3000, + expected_minimal_l2_gas_price: 2000, + expected_l1_gas_price: 4000, + expected_l1_pubdata_price: 6000, + }, + TestCase { + name: "1 ETH = 0.5 BaseToken", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(1).unwrap(), + denominator: NonZeroU64::new(2).unwrap(), + }, + input_minimal_l2_gas_price: 1000, + input_l1_gas_price: 2000, + input_l1_pubdata_price: 3000, + expected_minimal_l2_gas_price: 500, + expected_l1_gas_price: 1000, + expected_l1_pubdata_price: 1500, + }, + TestCase { + name: "1 ETH = 1 BaseToken", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(1).unwrap(), + denominator: NonZeroU64::new(1).unwrap(), + }, + input_minimal_l2_gas_price: 1000, + input_l1_gas_price: 2000, + input_l1_pubdata_price: 3000, + expected_minimal_l2_gas_price: 1000, + expected_l1_gas_price: 2000, + expected_l1_pubdata_price: 3000, + }, + TestCase { + name: "Large conversion - 1 ETH = 1_000 BaseToken", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(1_000_000).unwrap(), + denominator: NonZeroU64::new(1).unwrap(), + }, + input_minimal_l2_gas_price: 1_000_000, + input_l1_gas_price: 2_000_000, + input_l1_pubdata_price: 3_000_000, + expected_minimal_l2_gas_price: 1_000_000_000_000, + expected_l1_gas_price: 2_000_000_000_000, + expected_l1_pubdata_price: 3_000_000_000_000, + }, + TestCase { + name: "Small conversion - 1 ETH = 0.001 BaseToken", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(1).unwrap(), + denominator: NonZeroU64::new(1_000).unwrap(), + }, + input_minimal_l2_gas_price: 1_000_000, + input_l1_gas_price: 2_000_000, + input_l1_pubdata_price: 3_000_000, + expected_minimal_l2_gas_price: 1_000, + expected_l1_gas_price: 2_000, + expected_l1_pubdata_price: 3_000, + }, + TestCase { + name: "Fractional conversion ratio 123456789", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(1123456789).unwrap(), + denominator: NonZeroU64::new(1_000_000_000).unwrap(), + }, + input_minimal_l2_gas_price: 1_000_000, + input_l1_gas_price: 2_000_000, + input_l1_pubdata_price: 3_000_000, + expected_minimal_l2_gas_price: 1123456, + expected_l1_gas_price: 2246913, + expected_l1_pubdata_price: 3370370, + }, + TestCase { + name: "Conversion ratio too large so clamp down to u64::MAX", + conversion_ratio: BaseTokenConversionRatio { + numerator: NonZeroU64::new(u64::MAX).unwrap(), + denominator: NonZeroU64::new(1).unwrap(), + }, + input_minimal_l2_gas_price: 2, + input_l1_gas_price: 2, + input_l1_pubdata_price: 2, + expected_minimal_l2_gas_price: u64::MAX, + expected_l1_gas_price: u64::MAX, + expected_l1_pubdata_price: u64::MAX, + }, + ]; + + for case in test_cases { + let gas_adjuster = + setup_gas_adjuster(case.input_l1_gas_price, case.input_l1_pubdata_price).await; + + let base_token_ratio_provider = NoOpRatioProvider::new(case.conversion_ratio); + + let config = FeeModelConfig::V2(FeeModelConfigV2 { + minimal_l2_gas_price: case.input_minimal_l2_gas_price, + compute_overhead_part: 1.0, + pubdata_overhead_part: 1.0, + batch_overhead_l1_gas: 1, + max_gas_per_batch: 1, + max_pubdata_per_batch: 1, + }); + + let fee_provider = MainNodeFeeInputProvider::new( + Arc::new(gas_adjuster), + Arc::new(base_token_ratio_provider), + config, + ); + + let fee_params = fee_provider.get_fee_model_params(); + + if let FeeParams::V2(params) = fee_params { + assert_eq!( + params.l1_gas_price(), + case.expected_l1_gas_price, + "Test case '{}' failed: l1_gas_price mismatch", + case.name + ); + assert_eq!( + params.l1_pubdata_price(), + case.expected_l1_pubdata_price, + "Test case '{}' failed: l1_pubdata_price mismatch", + case.name + ); + assert_eq!( + params.config().minimal_l2_gas_price, + case.expected_minimal_l2_gas_price, + "Test case '{}' failed: minimal_l2_gas_price mismatch", + case.name + ); + } else { + panic!("Expected FeeParams::V2 for test case '{}'", case.name); + } + } + } + + // Helper function to create BaseFees. + fn base_fees(block: u64, blob: U256) -> BaseFees { + BaseFees { + base_fee_per_gas: block, + base_fee_per_blob_gas: blob, + } + } + + // Helper function to setup the GasAdjuster. + async fn setup_gas_adjuster(l1_gas_price: u64, l1_pubdata_price: u64) -> GasAdjuster { + let mock = MockEthereum::builder() + .with_fee_history(vec![ + base_fees(0, U256::from(4)), + base_fees(1, U256::from(3)), + ]) + .build(); + mock.advance_block_number(2); // Ensure we have enough blocks for the fee history + + let gas_adjuster_config = GasAdjusterConfig { + internal_enforced_l1_gas_price: Some(l1_gas_price), + internal_enforced_pubdata_price: Some(l1_pubdata_price), + max_base_fee_samples: 1, // Ensure this is less than the number of blocks + num_samples_for_blob_base_fee_estimate: 2, + ..Default::default() + }; + + GasAdjuster::new( + Box::new(mock.into_client()), + gas_adjuster_config, + PubdataSendingMode::Blobs, + L1BatchCommitmentMode::Rollup, + ) + .await + .expect("Failed to create GasAdjuster") + } } diff --git a/core/node/node_framework/Cargo.toml b/core/node/node_framework/Cargo.toml index d6a2e463a533..554083b830c5 100644 --- a/core/node/node_framework/Cargo.toml +++ b/core/node/node_framework/Cargo.toml @@ -49,6 +49,7 @@ zksync_queued_job_processor.workspace = true zksync_reorg_detector.workspace = true zksync_vm_runner.workspace = true zksync_node_db_pruner.workspace = true +zksync_base_token_adjuster.workspace = true pin-project-lite.workspace = true tracing.workspace = true diff --git a/core/node/node_framework/examples/showcase.rs b/core/node/node_framework/examples/showcase.rs index 24e3c04a1759..5684e53162a9 100644 --- a/core/node/node_framework/examples/showcase.rs +++ b/core/node/node_framework/examples/showcase.rs @@ -31,7 +31,7 @@ struct MemoryDatabase { /// but in real envs we use GCP. Alternatively, we have different resource implementations for /// main node and EN, like `MempoolIO` and `ExternalIO`. /// -/// Whether it makes sense to hdie the actual resource behind a trait often depends on the resource +/// Whether it makes sense to hide the actual resource behind a trait often depends on the resource /// itself. For example, our DAL is massive and cannot realistically be changed easily, so it's OK /// for it to be a concrete resource. But for anything that may realistically have two different /// implementations, it's often a good idea to hide it behind a trait. @@ -51,7 +51,7 @@ impl Database for MemoryDatabase { } /// An idiomatic way to create a resource is to prepare a wrapper for it. -/// This way we separate the logic of framework (which is primarily about glueing things together) +/// This way we separate the logic of the framework (which is primarily about glueing things together) /// from an actual logic of the resource. #[derive(Clone)] struct DatabaseResource(pub Arc); diff --git a/core/node/node_framework/src/implementations/layers/base_token_ratio_persister.rs b/core/node/node_framework/src/implementations/layers/base_token_ratio_persister.rs new file mode 100644 index 000000000000..c9a6ef8d8b66 --- /dev/null +++ b/core/node/node_framework/src/implementations/layers/base_token_ratio_persister.rs @@ -0,0 +1,62 @@ +use zksync_base_token_adjuster::BaseTokenRatioPersister; +use zksync_config::configs::base_token_adjuster::BaseTokenAdjusterConfig; + +use crate::{ + implementations::resources::pools::{MasterPool, PoolResource}, + service::{ServiceContext, StopReceiver}, + task::{Task, TaskId}, + wiring_layer::{WiringError, WiringLayer}, +}; + +/// Wiring layer for `BaseTokenRatioPersister` +/// +/// Responsible for orchestrating communications with external API feeds to get ETH<->BaseToken +/// conversion ratios and persisting them both in the DB and in the L1. +/// +/// ## Requests resources +/// +/// - `PoolResource` +/// +/// ## Adds tasks +/// +/// - `BaseTokenRatioPersister` +#[derive(Debug)] +pub struct BaseTokenRatioPersisterLayer { + config: BaseTokenAdjusterConfig, +} + +impl BaseTokenRatioPersisterLayer { + pub fn new(config: BaseTokenAdjusterConfig) -> Self { + Self { config } + } +} + +#[async_trait::async_trait] +impl WiringLayer for BaseTokenRatioPersisterLayer { + fn layer_name(&self) -> &'static str { + "base_token_ratio_persister" + } + + async fn wire(self: Box, mut context: ServiceContext<'_>) -> Result<(), WiringError> { + let master_pool_resource = context.get_resource::>()?; + let master_pool = master_pool_resource.get().await?; + + let persister = BaseTokenRatioPersister::new(master_pool, self.config); + + context.add_task(persister); + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Task for BaseTokenRatioPersister { + fn id(&self) -> TaskId { + "base_token_ratio_persister".into() + } + + async fn run(mut self: Box, stop_receiver: StopReceiver) -> anyhow::Result<()> { + (*self).run(stop_receiver.0).await?; + Ok(()) + } +} diff --git a/core/node/node_framework/src/implementations/layers/base_token_ratio_provider.rs b/core/node/node_framework/src/implementations/layers/base_token_ratio_provider.rs new file mode 100644 index 000000000000..d213ac68c79b --- /dev/null +++ b/core/node/node_framework/src/implementations/layers/base_token_ratio_provider.rs @@ -0,0 +1,65 @@ +use std::sync::Arc; + +use zksync_base_token_adjuster::DBBaseTokenRatioProvider; + +use crate::{ + implementations::resources::{ + base_token_ratio_provider::BaseTokenRatioProviderResource, + pools::{PoolResource, ReplicaPool}, + }, + service::{ServiceContext, StopReceiver}, + task::{Task, TaskId}, + wiring_layer::{WiringError, WiringLayer}, +}; + +/// Wiring layer for `BaseTokenRatioProvider` +/// +/// Responsible for serving the latest ETH<->BaseToken conversion ratio. This layer is only wired if +/// the base token is not ETH. If wired, this layer inserts the BaseTokenRatioProviderResource and kicks +/// off a task to poll the DB for the latest ratio and cache it. +/// +/// If the base token is ETH, a default, no-op impl of the BaseTokenRatioProviderResource is used by other +/// layers to always return a conversion ratio of 1. +/// +/// ## Requests resources +/// +/// - `PoolResource` +/// +/// ## Adds tasks +/// +/// - `BaseTokenRatioProvider` +#[derive(Debug)] +pub struct BaseTokenRatioProviderLayer; + +#[async_trait::async_trait] +impl WiringLayer for BaseTokenRatioProviderLayer { + fn layer_name(&self) -> &'static str { + "base_token_ratio_provider" + } + + async fn wire(self: Box, mut context: ServiceContext<'_>) -> Result<(), WiringError> { + let replica_pool_resource = context.get_resource::>()?; + let replica_pool = replica_pool_resource.get().await.unwrap(); + + let ratio_provider = DBBaseTokenRatioProvider::new(replica_pool).await?; + + context.insert_resource(BaseTokenRatioProviderResource(Arc::new( + ratio_provider.clone(), + )))?; + context.add_task(ratio_provider); + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Task for DBBaseTokenRatioProvider { + fn id(&self) -> TaskId { + "base_token_ratio_provider".into() + } + + async fn run(mut self: Box, stop_receiver: StopReceiver) -> anyhow::Result<()> { + (*self).run(stop_receiver.0).await?; + Ok(()) + } +} diff --git a/core/node/node_framework/src/implementations/layers/l1_gas.rs b/core/node/node_framework/src/implementations/layers/l1_gas.rs index d7ece6331882..2276e73e857f 100644 --- a/core/node/node_framework/src/implementations/layers/l1_gas.rs +++ b/core/node/node_framework/src/implementations/layers/l1_gas.rs @@ -10,6 +10,7 @@ use zksync_types::fee_model::FeeModelConfig; use crate::{ implementations::resources::{ + base_token_ratio_provider::BaseTokenRatioProviderResource, eth_interface::EthInterfaceResource, fee_input::FeeInputResource, l1_tx_params::L1TxParamsResource, }, @@ -75,8 +76,11 @@ impl WiringLayer for SequencerL1GasLayer { .context("GasAdjuster::new()")?; let gas_adjuster = Arc::new(adjuster); + let ratio_provider = context.get_resource_or_default::(); + let batch_fee_input_provider = Arc::new(MainNodeFeeInputProvider::new( gas_adjuster.clone(), + ratio_provider.0.clone(), FeeModelConfig::from_state_keeper_config(&self.state_keeper_config), )); context.insert_resource(FeeInputResource(batch_fee_input_provider))?; diff --git a/core/node/node_framework/src/implementations/layers/mod.rs b/core/node/node_framework/src/implementations/layers/mod.rs index f822ef5cc909..f9d2b94bad22 100644 --- a/core/node/node_framework/src/implementations/layers/mod.rs +++ b/core/node/node_framework/src/implementations/layers/mod.rs @@ -1,3 +1,5 @@ +pub mod base_token_ratio_persister; +pub mod base_token_ratio_provider; pub mod batch_status_updater; pub mod circuit_breaker_checker; pub mod commitment_generator; diff --git a/core/node/node_framework/src/implementations/resources/base_token_ratio_provider.rs b/core/node/node_framework/src/implementations/resources/base_token_ratio_provider.rs new file mode 100644 index 000000000000..9cb43870f76c --- /dev/null +++ b/core/node/node_framework/src/implementations/resources/base_token_ratio_provider.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use zksync_base_token_adjuster::{BaseTokenRatioProvider, NoOpRatioProvider}; + +use crate::resource::Resource; + +/// A resource that provides [`BaseTokenRatioProvider`] implementation to the service. +#[derive(Clone)] +pub struct BaseTokenRatioProviderResource(pub Arc); + +impl Default for BaseTokenRatioProviderResource { + fn default() -> Self { + Self(Arc::new(NoOpRatioProvider::default())) + } +} + +impl Resource for BaseTokenRatioProviderResource { + fn name() -> String { + "common/base_token_ratio_provider".into() + } +} diff --git a/core/node/node_framework/src/implementations/resources/mod.rs b/core/node/node_framework/src/implementations/resources/mod.rs index ac090d551316..cbe08fadb8e9 100644 --- a/core/node/node_framework/src/implementations/resources/mod.rs +++ b/core/node/node_framework/src/implementations/resources/mod.rs @@ -1,4 +1,5 @@ pub mod action_queue; +pub mod base_token_ratio_provider; pub mod circuit_breakers; pub mod da_client; pub mod eth_interface; diff --git a/core/node/state_keeper/Cargo.toml b/core/node/state_keeper/Cargo.toml index 28f850d339f9..9a662affb945 100644 --- a/core/node/state_keeper/Cargo.toml +++ b/core/node/state_keeper/Cargo.toml @@ -28,6 +28,7 @@ zksync_test_account.workspace = true zksync_node_genesis.workspace = true zksync_node_test_utils.workspace = true zksync_vm_utils.workspace = true +zksync_base_token_adjuster.workspace = true anyhow.workspace = true async-trait.workspace = true diff --git a/core/node/state_keeper/src/io/tests/tester.rs b/core/node/state_keeper/src/io/tests/tester.rs index f5a132baea3b..c056191736f2 100644 --- a/core/node/state_keeper/src/io/tests/tester.rs +++ b/core/node/state_keeper/src/io/tests/tester.rs @@ -2,6 +2,7 @@ use std::{slice, sync::Arc, time::Duration}; +use zksync_base_token_adjuster::NoOpRatioProvider; use zksync_config::{ configs::{chain::StateKeeperConfig, eth_sender::PubdataSendingMode, wallets::Wallets}, GasAdjusterConfig, @@ -84,8 +85,10 @@ impl Tester { pub(super) async fn create_batch_fee_input_provider(&self) -> MainNodeFeeInputProvider { let gas_adjuster = Arc::new(self.create_gas_adjuster().await); + MainNodeFeeInputProvider::new( gas_adjuster, + Arc::new(NoOpRatioProvider::default()), FeeModelConfig::V1(FeeModelConfigV1 { minimal_l2_gas_price: self.minimal_l2_gas_price(), }), @@ -104,6 +107,7 @@ impl Tester { let gas_adjuster = Arc::new(self.create_gas_adjuster().await); let batch_fee_input_provider = MainNodeFeeInputProvider::new( gas_adjuster, + Arc::new(NoOpRatioProvider::default()), FeeModelConfig::V1(FeeModelConfigV1 { minimal_l2_gas_price: self.minimal_l2_gas_price(), }), diff --git a/etc/env/base/base_token_adjuster.toml b/etc/env/base/base_token_adjuster.toml new file mode 100644 index 000000000000..100da3b7224f --- /dev/null +++ b/etc/env/base/base_token_adjuster.toml @@ -0,0 +1,6 @@ +# Configuration for the Base Token Adjuster crate + +[base_token_adjuster] + +# How often to poll external price feeds for the base token price. +price_polling_interval_ms = "30000" diff --git a/etc/env/base/chain.toml b/etc/env/base/chain.toml index 88a4c71bbb9b..0cb8213119b3 100644 --- a/etc/env/base/chain.toml +++ b/etc/env/base/chain.toml @@ -82,9 +82,9 @@ max_pubdata_per_batch = 100000 # Also, the fair L2 gas price is expected to only include the proving/computation price for the operator and not the costs that come from # processing the batch on L1. # - `V2`, the second model that was used in ZKsync Era. There the pubdata price might be independent from the L1 gas price. Also, -# The fair L2 gas price is expected to both the proving/computation price for the operator and the costs that come from +# The fair L2 gas price is expected to be both the proving/computation price for the operator and the costs that come from # processing the batch on L1. -fee_model_version = "V1" +fee_model_version = "V2" # Max number of computational gas that validation step is allowed to take. validation_computational_gas_limit = 300000 diff --git a/etc/env/base/rust.toml b/etc/env/base/rust.toml index ee4a69721cd3..950e78a155a0 100644 --- a/etc/env/base/rust.toml +++ b/etc/env/base/rust.toml @@ -57,6 +57,7 @@ zksync_health_check=debug,\ zksync_proof_fri_compressor=info,\ vise_exporter=debug,\ snapshots_creator=debug,\ +zksync_base_token_adjuster=debug,\ """ # `RUST_BACKTRACE` variable diff --git a/etc/env/file_based/general.yaml b/etc/env/file_based/general.yaml index f2733d5d1ee4..4a258a7cd99d 100644 --- a/etc/env/file_based/general.yaml +++ b/etc/env/file_based/general.yaml @@ -295,6 +295,8 @@ prover_group: aggregation_round: 1 - circuit_id: 18 aggregation_round: 1 +base_token_adjuster: + price_polling_interval_ms: 30000 house_keeper: l1_batch_metrics_reporting_interval_ms: 10000 @@ -319,7 +321,7 @@ prometheus: observability: log_format: plain - log_directives: "zksync_node_test_utils=info,zksync_state_keeper=info,zksync_reorg_detector=info,zksync_consistency_checker=info,zksync_metadata_calculator=info,zksync_node_sync=info,zksync_node_consensus=info,zksync_contract_verification_server=info,zksync_node_api_server=info,zksync_tee_verifier_input_producer=info,zksync_node_framework=info,zksync_block_reverter=info,zksync_commitment_generator=info,zksync_node_db_pruner=info,zksync_eth_sender=info,zksync_node_fee_model=info,zksync_node_genesis=info,zksync_house_keeper=info,zksync_proof_data_handler=info,zksync_shared_metrics=info,zksync_node_test_utils=info,zksync_vm_runner=info,zksync_consensus_bft=info,zksync_consensus_network=info,zksync_consensus_storage=info,zksync_core_leftovers=debug,zksync_server=debug,zksync_contract_verifier=debug,zksync_dal=info,zksync_db_connection=info,zksync_eth_client=info,zksync_eth_watch=debug,zksync_storage=info,zksync_db_manager=info,zksync_merkle_tree=info,zksync_state=debug,zksync_utils=debug,zksync_queued_job_processor=info,zksync_types=info,zksync_mempool=debug,loadnext=info,vm=info,zksync_object_store=info,zksync_external_node=info,zksync_witness_generator=info,zksync_prover_fri=info,zksync_witness_vector_generator=info,zksync_web3_decl=debug,zksync_health_check=debug,zksync_proof_fri_compressor=info,vise_exporter=error,snapshots_creator=debug" + log_directives: "zksync_node_test_utils=info,zksync_state_keeper=info,zksync_reorg_detector=info,zksync_consistency_checker=info,zksync_metadata_calculator=info,zksync_node_sync=info,zksync_node_consensus=info,zksync_contract_verification_server=info,zksync_node_api_server=info,zksync_tee_verifier_input_producer=info,zksync_node_framework=info,zksync_block_reverter=info,zksync_commitment_generator=info,zksync_node_db_pruner=info,zksync_eth_sender=info,zksync_node_fee_model=info,zksync_node_genesis=info,zksync_house_keeper=info,zksync_proof_data_handler=info,zksync_shared_metrics=info,zksync_node_test_utils=info,zksync_vm_runner=info,zksync_consensus_bft=info,zksync_consensus_network=info,zksync_consensus_storage=info,zksync_core_leftovers=debug,zksync_server=debug,zksync_contract_verifier=debug,zksync_dal=info,zksync_db_connection=info,zksync_eth_client=info,zksync_eth_watch=debug,zksync_storage=info,zksync_db_manager=info,zksync_merkle_tree=info,zksync_state=debug,zksync_utils=debug,zksync_queued_job_processor=info,zksync_types=info,zksync_mempool=debug,loadnext=info,vm=info,zksync_object_store=info,zksync_external_node=info,zksync_witness_generator=info,zksync_prover_fri=info,zksync_witness_vector_generator=info,zksync_web3_decl=debug,zksync_health_check=debug,zksync_proof_fri_compressor=info,vise_exporter=error,snapshots_creator=debug,zksync_base_token_adjuster=debug" sentry: url: unset panic_interval: 1800 diff --git a/prover/Cargo.lock b/prover/Cargo.lock index 7483b777f68e..6b8816f0704e 100644 --- a/prover/Cargo.lock +++ b/prover/Cargo.lock @@ -8512,6 +8512,7 @@ name = "zksync_types" version = "0.1.0" dependencies = [ "anyhow", + "bigdecimal", "blake2 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)", "chrono", "derive_more", @@ -8527,6 +8528,7 @@ dependencies = [ "serde_json", "strum", "thiserror", + "tracing", "zksync_basic_types", "zksync_config", "zksync_contracts", diff --git a/prover/config/src/lib.rs b/prover/config/src/lib.rs index ac9ebc911b6d..9b6ee308b62c 100644 --- a/prover/config/src/lib.rs +++ b/prover/config/src/lib.rs @@ -8,10 +8,10 @@ use zksync_config::{ }, fri_prover_group::FriProverGroupConfig, house_keeper::HouseKeeperConfig, - DADispatcherConfig, DatabaseSecrets, FriProofCompressorConfig, FriProverConfig, - FriProverGatewayConfig, FriWitnessGeneratorConfig, FriWitnessVectorGeneratorConfig, - GeneralConfig, ObjectStoreConfig, ObservabilityConfig, PrometheusConfig, - ProofDataHandlerConfig, ProtectiveReadsWriterConfig, + BaseTokenAdjusterConfig, DADispatcherConfig, DatabaseSecrets, FriProofCompressorConfig, + FriProverConfig, FriProverGatewayConfig, FriWitnessGeneratorConfig, + FriWitnessVectorGeneratorConfig, GeneralConfig, ObjectStoreConfig, ObservabilityConfig, + PrometheusConfig, ProofDataHandlerConfig, ProtectiveReadsWriterConfig, }, ApiConfig, ContractVerifierConfig, DBConfig, EthConfig, EthWatchConfig, GasAdjusterConfig, PostgresConfig, SnapshotsCreatorConfig, @@ -51,6 +51,7 @@ fn load_env_config() -> anyhow::Result { da_dispatcher_config: DADispatcherConfig::from_env().ok(), protective_reads_writer_config: ProtectiveReadsWriterConfig::from_env().ok(), core_object_store: ObjectStoreConfig::from_env().ok(), + base_token_adjuster_config: BaseTokenAdjusterConfig::from_env().ok(), commitment_generator: None, pruning: None, snapshot_recovery: None, diff --git a/zk_toolbox/Cargo.lock b/zk_toolbox/Cargo.lock index e6f82da3ad75..62501a944bb9 100644 --- a/zk_toolbox/Cargo.lock +++ b/zk_toolbox/Cargo.lock @@ -6508,6 +6508,7 @@ name = "zksync_types" version = "0.1.0" dependencies = [ "anyhow", + "bigdecimal", "blake2 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)", "chrono", "derive_more 1.0.0-beta.6", @@ -6523,6 +6524,7 @@ dependencies = [ "serde_json", "strum 0.24.1", "thiserror", + "tracing", "zksync_basic_types", "zksync_config", "zksync_contracts",