Skip to content

Commit

Permalink
perf(api): More efficient gas estimation (#2937)
Browse files Browse the repository at this point in the history
## What ❔

Makes gas estimation more efficient by using heuristics similar to ones
used by Ethereum clients:

- Defines the lower gas limit bound as the gas consumed with an
effectively infinite gas limit.
- Defines an initial binary search pivot as a slightly larger value so
that in the best / average case, most of the search space is immediately
discarded.

## Why ❔

Improves gas estimation latency in the best and average case.

## Checklist

- [x] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [x] Tests for the changes have been added / updated.
- [x] Documentation comments have been added / updated.
- [x] Code has been formatted via `zk fmt` and `zk lint`.
  • Loading branch information
slowli authored Oct 1, 2024
1 parent cdc1288 commit 3b69e37
Show file tree
Hide file tree
Showing 20 changed files with 1,472 additions and 357 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions core/bin/external_node/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ pub(crate) struct OptionalENConfig {
/// The max possible number of gas that `eth_estimateGas` is allowed to overestimate.
#[serde(default = "OptionalENConfig::default_estimate_gas_acceptable_overestimation")]
pub estimate_gas_acceptable_overestimation: u32,
/// Enables optimizations for the binary search of the gas limit in `eth_estimateGas`. These optimizations are currently
/// considered experimental.
#[serde(default)]
pub estimate_gas_optimize_search: bool,
/// The multiplier to use when suggesting gas price. Should be higher than one,
/// otherwise if the L1 prices soar, the suggested gas price won't be sufficient to be included in block.
#[serde(default = "OptionalENConfig::default_gas_price_scale_factor")]
Expand Down Expand Up @@ -558,6 +562,11 @@ impl OptionalENConfig {
web3_json_rpc.estimate_gas_acceptable_overestimation,
default_estimate_gas_acceptable_overestimation
),
estimate_gas_optimize_search: general_config
.api_config
.as_ref()
.map(|a| a.web3_json_rpc.estimate_gas_optimize_search)
.unwrap_or_default(),
gas_price_scale_factor: load_config_or_default!(
general_config.api_config,
web3_json_rpc.gas_price_scale_factor,
Expand Down Expand Up @@ -1380,6 +1389,7 @@ impl From<&ExternalNodeConfig> for InternalApiConfig {
estimate_gas_acceptable_overestimation: config
.optional
.estimate_gas_acceptable_overestimation,
estimate_gas_optimize_search: config.optional.estimate_gas_optimize_search,
bridge_addresses: BridgeAddresses {
l1_erc20_default_bridge: config.remote.l1_erc20_bridge_proxy_addr,
l2_erc20_default_bridge: config.remote.l2_erc20_bridge_addr,
Expand Down
5 changes: 5 additions & 0 deletions core/lib/config/src/configs/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ pub struct Web3JsonRpcConfig {
pub estimate_gas_scale_factor: f64,
/// The max possible number of gas that `eth_estimateGas` is allowed to overestimate.
pub estimate_gas_acceptable_overestimation: u32,
/// Enables optimizations for the binary search of the gas limit in `eth_estimateGas`. These optimizations are currently
/// considered experimental.
#[serde(default)]
pub estimate_gas_optimize_search: bool,
/// Max possible size of an ABI encoded tx (in bytes).
pub max_tx_size: usize,
/// Max number of cache misses during one VM execution. If the number of cache misses exceeds this value, the API server panics.
Expand Down Expand Up @@ -237,6 +241,7 @@ impl Web3JsonRpcConfig {
gas_price_scale_factor: 1.2,
estimate_gas_scale_factor: 1.2,
estimate_gas_acceptable_overestimation: 1000,
estimate_gas_optimize_search: false,
max_tx_size: 1000000,
vm_execution_cache_misses_limit: Default::default(),
vm_concurrency_limit: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions core/lib/config/src/testonly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl Distribution<configs::api::Web3JsonRpcConfig> for EncodeDist {
gas_price_scale_factor: self.sample(rng),
estimate_gas_scale_factor: self.sample(rng),
estimate_gas_acceptable_overestimation: self.sample(rng),
estimate_gas_optimize_search: self.sample(rng),
max_tx_size: self.sample(rng),
vm_execution_cache_misses_limit: self.sample(rng),
vm_concurrency_limit: self.sample(rng),
Expand Down
1 change: 1 addition & 0 deletions core/lib/env_config/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ mod tests {
estimate_gas_scale_factor: 1.0f64,
gas_price_scale_factor: 1.2,
estimate_gas_acceptable_overestimation: 1000,
estimate_gas_optimize_search: false,
max_tx_size: 1000000,
vm_execution_cache_misses_limit: None,
vm_concurrency_limit: Some(512),
Expand Down
2 changes: 2 additions & 0 deletions core/lib/protobuf_config/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl ProtoRepr for proto::Web3JsonRpc {
&self.estimate_gas_acceptable_overestimation,
)
.context("acceptable_overestimation")?,
estimate_gas_optimize_search: self.estimate_gas_optimize_search.unwrap_or(false),
max_tx_size: required(&self.max_tx_size)
.and_then(|x| Ok((*x).try_into()?))
.context("max_tx_size")?,
Expand Down Expand Up @@ -167,6 +168,7 @@ impl ProtoRepr for proto::Web3JsonRpc {
estimate_gas_acceptable_overestimation: Some(
this.estimate_gas_acceptable_overestimation,
),
estimate_gas_optimize_search: Some(this.estimate_gas_optimize_search),
max_tx_size: Some(this.max_tx_size.try_into().unwrap()),
vm_execution_cache_misses_limit: this
.vm_execution_cache_misses_limit
Expand Down
2 changes: 2 additions & 0 deletions core/lib/protobuf_config/src/proto/config/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ message Web3JsonRpc {
repeated MaxResponseSizeOverride max_response_body_size_overrides = 31;
repeated string api_namespaces = 32; // Optional, if empty all namespaces are available
optional bool extended_api_tracing = 33; // optional, default false
optional bool estimate_gas_optimize_search = 34; // optional, default false

reserved 15; reserved "l1_to_l2_transactions_compatibility_mode";
reserved 11; reserved "request_timeout";
reserved 12; reserved "account_pks";
Expand Down
1 change: 1 addition & 0 deletions core/node/api_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ tower-http = { workspace = true, features = ["cors", "metrics"] }
lru.workspace = true

[dev-dependencies]
zk_evm_1_5_0.workspace = true
zksync_node_genesis.workspace = true
zksync_node_test_utils.workspace = true

Expand Down
5 changes: 4 additions & 1 deletion core/node/api_server/src/execution_sandbox/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
use zksync_multivm::interface::storage::ReadStorage;
use zksync_types::{
api::state_override::{OverrideState, StateOverride},
get_code_key, get_nonce_key,
get_code_key, get_known_code_key, get_nonce_key,
utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance},
AccountTreeId, StorageKey, StorageValue, H256,
};
Expand Down Expand Up @@ -56,6 +56,9 @@ impl<S: ReadStorage> StorageWithOverrides<S> {
let code_key = get_code_key(account);
let code_hash = code.hash();
self.overridden_slots.insert(code_key, code_hash);
let known_code_key = get_known_code_key(&code_hash);
self.overridden_slots
.insert(known_code_key, H256::from_low_u64_be(1));
self.store_factory_dep(code_hash, code.clone().into_bytes());
}

Expand Down
39 changes: 11 additions & 28 deletions core/node/api_server/src/execution_sandbox/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ use zksync_node_test_utils::{create_l2_block, prepare_recovery_snapshot};
use zksync_state::PostgresStorageCaches;
use zksync_types::{
api::state_override::{OverrideAccount, StateOverride},
fee::Fee,
fee_model::BatchFeeInput,
l2::L2Tx,
transaction_request::PaymasterParams,
Address, K256PrivateKey, L2ChainId, Nonce, ProtocolVersionId, Transaction, U256,
K256PrivateKey, ProtocolVersionId, Transaction, U256,
};

use super::*;
use crate::{execution_sandbox::execute::SandboxExecutor, tx_sender::SandboxExecutorOptions};
use crate::{
execution_sandbox::execute::SandboxExecutor, testonly::TestAccount,
tx_sender::SandboxExecutorOptions,
};

#[tokio::test]
async fn creating_block_args() {
Expand Down Expand Up @@ -210,7 +210,11 @@ async fn test_instantiating_vm(connection: Connection<'static, Core>, block_args
let fee_input = BatchFeeInput::l1_pegged(55, 555);
let (base_fee, gas_per_pubdata) =
derive_base_fee_and_gas_per_pubdata(fee_input, ProtocolVersionId::latest().into());
let tx = Transaction::from(create_transfer(base_fee, gas_per_pubdata));
let tx = Transaction::from(K256PrivateKey::random().create_transfer(
0.into(),
base_fee,
gas_per_pubdata,
));

let (limiter, _) = VmConcurrencyLimiter::new(1);
let vm_permit = limiter.acquire().await.unwrap();
Expand All @@ -229,27 +233,6 @@ async fn test_instantiating_vm(connection: Connection<'static, Core>, block_args
assert!(!tx_result.result.is_failed(), "{tx_result:#?}");
}

fn create_transfer(fee_per_gas: u64, gas_per_pubdata: u64) -> L2Tx {
let fee = Fee {
gas_limit: 200_000.into(),
max_fee_per_gas: fee_per_gas.into(),
max_priority_fee_per_gas: 0_u64.into(),
gas_per_pubdata_limit: gas_per_pubdata.into(),
};
L2Tx::new_signed(
Some(Address::random()),
vec![],
Nonce(0),
fee,
U256::zero(),
L2ChainId::default(),
&K256PrivateKey::random(),
vec![],
PaymasterParams::default(),
)
.unwrap()
}

#[test_casing(2, [false, true])]
#[tokio::test]
async fn validating_transaction(set_balance: bool) {
Expand All @@ -270,7 +253,7 @@ async fn validating_transaction(set_balance: bool) {
let fee_input = BatchFeeInput::l1_pegged(55, 555);
let (base_fee, gas_per_pubdata) =
derive_base_fee_and_gas_per_pubdata(fee_input, ProtocolVersionId::latest().into());
let tx = create_transfer(base_fee, gas_per_pubdata);
let tx = K256PrivateKey::random().create_transfer(0.into(), base_fee, gas_per_pubdata);

let (limiter, _) = VmConcurrencyLimiter::new(1);
let vm_permit = limiter.acquire().await.unwrap();
Expand Down
10 changes: 10 additions & 0 deletions core/node/api_server/src/execution_sandbox/vm_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,18 @@ pub(crate) struct SandboxMetrics {
pub(super) sandbox_execution_permits: Histogram<usize>,
#[metrics(buckets = Buckets::LATENCIES)]
submit_tx: Family<SubmitTxStage, Histogram<Duration>>,

/// Number of iterations necessary to estimate gas for a transaction.
#[metrics(buckets = Buckets::linear(0.0..=30.0, 3.0))]
pub estimate_gas_binary_search_iterations: Histogram<usize>,
/// Relative difference between the unscaled final gas estimate and the optimized lower bound. Positive if the lower bound
/// is (as expected) lower than the final gas estimate.
#[metrics(buckets = Buckets::linear(-0.05..=0.15, 0.01))]
pub estimate_gas_lower_bound_relative_diff: Histogram<f64>,
/// Relative difference between the optimistic gas limit and the unscaled final gas estimate. Positive if the optimistic gas limit
/// is (as expected) greater than the final gas estimate.
#[metrics(buckets = Buckets::linear(-0.05..=0.15, 0.01))]
pub estimate_gas_optimistic_gas_limit_relative_diff: Histogram<f64>,
}

impl SandboxMetrics {
Expand Down
2 changes: 2 additions & 0 deletions core/node/api_server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
mod utils;
pub mod execution_sandbox;
pub mod healthcheck;
#[cfg(test)]
mod testonly;
pub mod tx_sender;
pub mod web3;
Loading

0 comments on commit 3b69e37

Please sign in to comment.