From 2c870ae146098f8684421b6a80efa67aef58fab0 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Thu, 11 Dec 2025 01:26:43 +0100 Subject: [PATCH 1/4] vm: Enforce a per-transaction minimum gas floor - Add min_tx_gas field to VM config - Reject transactions whose gas_limit is below configured min_tx_gas - Floor receipt.gas_spent to at least min_tx_gas on successful exeuction --- vm/CHANGELOG.md | 5 +++++ vm/src/execute.rs | 16 ++++++++++++++++ vm/src/execute/config.rs | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/vm/CHANGELOG.md b/vm/CHANGELOG.md index 53a02dbcc9..5ef0be3561 100644 --- a/vm/CHANGELOG.md +++ b/vm/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Enforce a per‑transaction minimum gas floor [#3940] + ## [1.4.1] - 2025-12-04 ### Added @@ -58,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add vm to interact with Dusk network [#3235] +[#3940]: https://github.com/dusk-network/rusk/issues/3940 [#3774]: https://github.com/dusk-network/rusk/issues/3774 [#3235]: https://github.com/dusk-network/rusk/issues/3235 [#3341]: https://github.com/dusk-network/rusk/issues/3341 diff --git a/vm/src/execute.rs b/vm/src/execute.rs index 984a2bceeb..befc528dd0 100644 --- a/vm/src/execute.rs +++ b/vm/src/execute.rs @@ -69,6 +69,13 @@ pub fn execute( tx: &Transaction, config: &Config, ) -> Result, ContractError>>, Error> { + // Enforce gas_limit >= min_tx_gas if min_tx_gas feature is enabled + if config.min_tx_gas > 0 && tx.gas_limit() < config.min_tx_gas { + return Err(Error::Panic( + "transaction gas_limit is below the minimum required gas".into(), + )); + } + // Transaction will be discarded if it is a deployment transaction // with gas limit smaller than deploy charge. tx.deploy_check( @@ -149,6 +156,15 @@ pub fn execute( // Ensure all gas is consumed if there's an error in the contract call if receipt.data.is_err() { receipt.gas_spent = receipt.gas_limit; + } else if config.min_tx_gas > 0 { + // On success, enforce the global per-tx minimum gas floor if the + // feature min_tx_gas is enabled + use core::cmp::min; + + let floor = min(config.min_tx_gas, receipt.gas_limit); + if receipt.gas_spent < floor { + receipt.gas_spent = floor; + } } // Refund the appropriate amount to the transaction. This call is guaranteed diff --git a/vm/src/execute/config.rs b/vm/src/execute/config.rs index 7e5d630027..f9dd9ba7e3 100644 --- a/vm/src/execute/config.rs +++ b/vm/src/execute/config.rs @@ -32,6 +32,9 @@ pub struct Config { /// Disable calls to 3rd party contracts pub disable_3rd_party: bool, + + /// Minimum gas to charge for any transaction. 0 disables the floor + pub min_tx_gas: u64, } impl Default for Config { @@ -52,5 +55,6 @@ impl Config { disable_wasm64: false, disable_wasm32: false, disable_3rd_party: false, + min_tx_gas: 0, }; } From 02fecffafa630f9a528fc1f26b24c84b9d94fa04 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Thu, 11 Dec 2025 01:34:21 +0100 Subject: [PATCH 2/4] node: Enforce min_tx_gas in mempool preverify --- node/CHANGELOG.md | 5 +++++ node/src/mempool.rs | 16 +++++++++++----- node/src/vm.rs | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/node/CHANGELOG.md b/node/CHANGELOG.md index 0fd7a187e9..d667dcbed5 100644 --- a/node/CHANGELOG.md +++ b/node/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Enforce `min_tx_gas` in mempool preverify, reject TXs where `gas_limit < max(min_gas_limit, min_tx_gas)` [#3940] + ## [1.4.1] - 2025-12-04 ### Added @@ -76,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - First `dusk-node` release +[#3940]: https://github.com/dusk-network/rusk/issues/3940 [#3917]: https://github.com/dusk-network/rusk/issues/3917 [#3874]: https://github.com/dusk-network/rusk/issues/3874 [#3871]: https://github.com/dusk-network/rusk/issues/3871 diff --git a/node/src/mempool.rs b/node/src/mempool.rs index 00f002ac29..553aa9ba3d 100644 --- a/node/src/mempool.rs +++ b/node/src/mempool.rs @@ -61,7 +61,7 @@ pub enum TxAcceptanceError { VerificationFailed(String), #[error("gas price lower than minimum {0}")] GasPriceTooLow(u64), - #[error("gas limit lower than minimum {0}")] + #[error("gas limit lower than minimum {0} LUX")] GasLimitTooLow(u64), #[error("Maximum count of transactions exceeded {0}")] MaxTxnCountExceeded(usize), @@ -349,10 +349,16 @@ impl MempoolSrv { dusk_consensus::validate_blob_sidecars(tx)?; } - // Check global minimum gas limit - let min_gas_limit = vm.min_gas_limit(); - if tx.inner.gas_limit() < min_gas_limit { - return Err(TxAcceptanceError::GasLimitTooLow(min_gas_limit)); + // Check global minimum gas limit and per-tx fee floor + let chain_min_gas_limit = vm.min_gas_limit(); + let min_tx_gas = vm.min_tx_gas(tip_height); + let required_gas_limit = + core::cmp::max(chain_min_gas_limit, min_tx_gas); + + if tx.inner.gas_limit() < required_gas_limit { + return Err(TxAcceptanceError::GasLimitTooLow( + required_gas_limit, + )); } } diff --git a/node/src/vm.rs b/node/src/vm.rs index e9c97480c5..e3fdcc38d2 100644 --- a/node/src/vm.rs +++ b/node/src/vm.rs @@ -98,6 +98,7 @@ pub trait VMExecution: Send + Sync + 'static { fn wasm64_disabled(&self, block_height: u64) -> bool; fn wasm32_disabled(&self, block_height: u64) -> bool; fn third_party_disabled(&self, block_height: u64) -> bool; + fn min_tx_gas(&self, height: u64) -> u64; } #[allow(clippy::large_enum_variant)] From 114416c99b8c57ce65dbb75349e37fa2bfe7b7d5 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Thu, 11 Dec 2025 01:43:49 +0100 Subject: [PATCH 3/4] rusk: Add TX floor price feautre and activation height --- rusk/CHANGELOG.md | 5 +++ rusk/src/lib/node/vm.rs | 13 ++++++ rusk/src/lib/node/vm/config.rs | 25 +++++++++++ rusk/src/lib/node/vm/config/known.rs | 22 +++++++++- rusk/src/lib/node/vm/config/opt.rs | 12 ++++++ rusk/tests/services/gas_behavior.rs | 64 +++++++++++++++++++++++++++- 6 files changed, 138 insertions(+), 3 deletions(-) diff --git a/rusk/CHANGELOG.md b/rusk/CHANGELOG.md index 60f387b82a..f749519d10 100644 --- a/rusk/CHANGELOG.md +++ b/rusk/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add TX floor price feature, including for well-known chain ids [#3940] + ## [1.4.1] - 2025-12-04 ### Added @@ -406,6 +410,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add build system that generates keys for circuits and caches them. +[#3940]: https://github.com/dusk-network/rusk/issues/3940 [#3917]: https://github.com/dusk-network/rusk/issues/3917 [#3897]: https://github.com/dusk-network/rusk/issues/3897 [#3894]: https://github.com/dusk-network/rusk/issues/3894 diff --git a/rusk/src/lib/node/vm.rs b/rusk/src/lib/node/vm.rs index dff2278e1a..0edc022d8a 100644 --- a/rusk/src/lib/node/vm.rs +++ b/rusk/src/lib/node/vm.rs @@ -321,6 +321,19 @@ impl VMExecution for Rusk { .map(|activation| activation.is_active_at(block_height)) .unwrap_or(false) } + + fn min_tx_gas(&self, height: u64) -> u64 { + self.vm_config + .feature(FEATURE_MIN_TX_GAS) + .map(|activation| { + if activation.is_active_at(height) { + self.vm_config.min_tx_gas + } else { + 0 + } + }) + .unwrap_or(0) + } } fn has_unique_elements(iter: T) -> bool diff --git a/rusk/src/lib/node/vm/config.rs b/rusk/src/lib/node/vm/config.rs index d63dc00190..fefe1beab7 100644 --- a/rusk/src/lib/node/vm/config.rs +++ b/rusk/src/lib/node/vm/config.rs @@ -21,6 +21,8 @@ const DEFAULT_GAS_PER_BLOB: u64 = 1_000_000; const DEFAULT_MIN_DEPLOY_POINTS: u64 = 5_000_000; const DEFAULT_MIN_DEPLOYMENT_GAS_PRICE: u64 = 2_000; const DEFAULT_BLOCK_GAS_LIMIT: u64 = 3 * 1_000_000_000; +/// Default per‑tx minimum gas floor. 0 disables it. +const DEFAULT_MIN_TX_GAS: u64 = 5_000_000; /// Configuration for the execution of a transaction. #[derive(Debug, Clone, serde::Serialize)] @@ -45,6 +47,9 @@ pub struct Config { #[serde(with = "humantime_serde")] pub generation_timeout: Option, + /// Minimum gas charged for any transaction. + pub min_tx_gas: u64, + /// Set of features to activate features: HashMap, } @@ -61,6 +66,7 @@ pub(crate) mod feature { pub const FEATURE_DISABLE_WASM64: &str = "DISABLE_WASM64"; pub const FEATURE_DISABLE_WASM32: &str = "DISABLE_WASM32"; pub const FEATURE_DISABLE_3RD_PARTY: &str = "DISABLE_3RD_PARTY"; + pub const FEATURE_MIN_TX_GAS: &str = "MIN_TX_GAS"; pub const HQ_KECCAK256: &str = "HQ_KECCAK256"; } @@ -73,6 +79,7 @@ impl Config { min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, + min_tx_gas: DEFAULT_MIN_TX_GAS, generation_timeout: None, features: HashMap::new(), } @@ -121,6 +128,12 @@ impl Config { self } + /// Set the minimum gas charged for any transaction. + pub const fn with_min_tx_gas(mut self, min_tx_gas: u64) -> Self { + self.min_tx_gas = min_tx_gas; + self + } + /// Create a new `Config` with the given parameters. pub fn to_execution_config(&self, block_height: u64) -> ExecutionConfig { let with_public_sender: bool = self @@ -143,11 +156,23 @@ impl Config { .feature(feature::FEATURE_DISABLE_3RD_PARTY) .map(|activation| activation.is_active_at(block_height)) .unwrap_or_default(); + let min_tx_gas = self + .feature(feature::FEATURE_MIN_TX_GAS) + .map(|activation| { + if activation.is_active_at(block_height) { + self.min_tx_gas + } else { + 0 + } + }) + .unwrap_or(0); + ExecutionConfig { gas_per_blob: self.gas_per_blob, gas_per_deploy_byte: self.gas_per_deploy_byte, min_deploy_points: self.min_deploy_points, min_deploy_gas_price: self.min_deployment_gas_price, + min_tx_gas, with_public_sender, with_blob, disable_wasm64, diff --git a/rusk/src/lib/node/vm/config/known.rs b/rusk/src/lib/node/vm/config/known.rs index a5d77e5f84..eac1195a61 100644 --- a/rusk/src/lib/node/vm/config/known.rs +++ b/rusk/src/lib/node/vm/config/known.rs @@ -10,7 +10,9 @@ use std::sync::LazyLock; use dusk_vm::FeatureActivation; -use crate::node::{FEATURE_DISABLE_3RD_PARTY, FEATURE_DISABLE_WASM32}; +use crate::node::{ + FEATURE_DISABLE_3RD_PARTY, FEATURE_DISABLE_WASM32, FEATURE_MIN_TX_GAS, +}; use super::feature::{ FEATURE_ABI_PUBLIC_SENDER, FEATURE_BLOB, FEATURE_DISABLE_WASM64, @@ -19,6 +21,7 @@ use super::feature::{ use super::{ DEFAULT_BLOCK_GAS_LIMIT, DEFAULT_GAS_PER_BLOB, DEFAULT_GAS_PER_DEPLOY_BYTE, DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, DEFAULT_MIN_DEPLOY_POINTS, + DEFAULT_MIN_TX_GAS, }; pub const MAINNET_ID: u8 = 1; @@ -36,7 +39,8 @@ pub struct WellKnownConfig { pub min_deploy_points: u64, pub min_deployment_gas_price: u64, pub block_gas_limit: u64, - pub features: [(&'static str, FeatureActivation); 6], + pub min_tx_gas: u64, + pub features: [(&'static str, FeatureActivation); 7], } impl WellKnownConfig { @@ -79,6 +83,9 @@ static MAINNET_DISABLE_WASM_64: LazyLock = const MAINNET_BLOB_ACTIVATION: FeatureActivation = FeatureActivation::Height(MAINNET_AT_10_12_2025_AT_09_00_UTC); +const MAINNET_MIN_TX_GAS_ACTIVATION: FeatureActivation = + FeatureActivation::Height(3_060_210); + /// Mainnet VM configuration. static MAINNET_CONFIG: LazyLock = LazyLock::new(|| WellKnownConfig { @@ -87,6 +94,7 @@ static MAINNET_CONFIG: LazyLock = min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, + min_tx_gas: DEFAULT_MIN_TX_GAS, features: [ (FEATURE_ABI_PUBLIC_SENDER, MAINNET_SENDER_ACTIVATION_HEIGHT), (HQ_KECCAK256, NEVER), @@ -94,6 +102,7 @@ static MAINNET_CONFIG: LazyLock = (FEATURE_DISABLE_WASM64, MAINNET_DISABLE_WASM_64.clone()), (FEATURE_DISABLE_WASM32, MAINNET_3RD_PARTY_OFF.clone()), (FEATURE_DISABLE_3RD_PARTY, MAINNET_3RD_PARTY_OFF.clone()), + (FEATURE_MIN_TX_GAS, MAINNET_MIN_TX_GAS_ACTIVATION), ], }); @@ -101,6 +110,9 @@ static MAINNET_CONFIG: LazyLock = const TESTNET_AT_12_11_2025_AT_09_00_UTC: FeatureActivation = FeatureActivation::Height(1_814_090); +const TESTNET_MIN_TX_GAS_ACTIVATION: FeatureActivation = + FeatureActivation::Height(2_242_800); + /// Testnet VM configuration. const TESTNET_CONFIG: WellKnownConfig = WellKnownConfig { gas_per_blob: DEFAULT_GAS_PER_BLOB, @@ -108,6 +120,7 @@ const TESTNET_CONFIG: WellKnownConfig = WellKnownConfig { min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, + min_tx_gas: DEFAULT_MIN_TX_GAS, features: [ (FEATURE_ABI_PUBLIC_SENDER, GENESIS), (HQ_KECCAK256, NEVER), @@ -115,6 +128,7 @@ const TESTNET_CONFIG: WellKnownConfig = WellKnownConfig { (FEATURE_DISABLE_WASM64, TESTNET_AT_12_11_2025_AT_09_00_UTC), (FEATURE_DISABLE_WASM32, NEVER), (FEATURE_DISABLE_3RD_PARTY, NEVER), + (FEATURE_MIN_TX_GAS, TESTNET_MIN_TX_GAS_ACTIVATION), ], }; @@ -125,6 +139,7 @@ const DEVNET_CONFIG: WellKnownConfig = WellKnownConfig { min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, + min_tx_gas: DEFAULT_MIN_TX_GAS, features: [ (FEATURE_ABI_PUBLIC_SENDER, GENESIS), (HQ_KECCAK256, GENESIS), @@ -132,6 +147,7 @@ const DEVNET_CONFIG: WellKnownConfig = WellKnownConfig { (FEATURE_DISABLE_WASM64, GENESIS), (FEATURE_DISABLE_WASM32, NEVER), (FEATURE_DISABLE_3RD_PARTY, NEVER), + (FEATURE_MIN_TX_GAS, GENESIS), ], }; @@ -142,6 +158,7 @@ const LOCALNET_CONFIG: WellKnownConfig = WellKnownConfig { min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, + min_tx_gas: DEFAULT_MIN_TX_GAS, features: [ (FEATURE_ABI_PUBLIC_SENDER, GENESIS), (HQ_KECCAK256, GENESIS), @@ -149,5 +166,6 @@ const LOCALNET_CONFIG: WellKnownConfig = WellKnownConfig { (FEATURE_DISABLE_WASM64, GENESIS), (FEATURE_DISABLE_WASM32, NEVER), (FEATURE_DISABLE_3RD_PARTY, NEVER), + (FEATURE_MIN_TX_GAS, GENESIS), ], }; diff --git a/rusk/src/lib/node/vm/config/opt.rs b/rusk/src/lib/node/vm/config/opt.rs index 394052da8a..f8534921db 100644 --- a/rusk/src/lib/node/vm/config/opt.rs +++ b/rusk/src/lib/node/vm/config/opt.rs @@ -43,6 +43,9 @@ pub struct OptionalConfig { #[serde(default, with = "humantime_serde")] pub generation_timeout: Option, + /// Minimum gas charged for any transaction. + pub min_tx_gas: Option, + /// Set of features to activate #[serde(default)] features: HashMap, @@ -111,6 +114,12 @@ impl OptionalConfig { config.block_gas_limit, ); + Self::set_or_warn( + "min_tx_gas", + &mut self.min_tx_gas, + config.min_tx_gas, + ); + for (feature, activation) in &config.features { if let Some(v) = self.feature(feature) { if v != activation { @@ -192,6 +201,9 @@ impl TryFrom for Config { block_gas_limit: value .block_gas_limit .ok_or(anyhow!("Missing block_gas_limit"))?, + min_tx_gas: value + .min_tx_gas + .ok_or(anyhow!("Missing min_tx_gas"))?, generation_timeout: value.generation_timeout, features: value.features, }) diff --git a/rusk/tests/services/gas_behavior.rs b/rusk/tests/services/gas_behavior.rs index 403546cbae..918787fc31 100644 --- a/rusk/tests/services/gas_behavior.rs +++ b/rusk/tests/services/gas_behavior.rs @@ -20,7 +20,7 @@ use tempfile::tempdir; use tracing::info; use crate::common::logger; -use crate::common::state::{generator_procedure, new_state}; +use crate::common::state::{generator_procedure, new_state, ExecuteResult}; use crate::common::wallet::{ test_wallet as wallet, TestStateClient, TestStore, }; @@ -34,6 +34,8 @@ const GAS_LIMIT_0: u64 = 100_000_000; const GAS_LIMIT_1: u64 = 300_000_000; const GAS_PRICE: u64 = 1; const DEPOSIT: u64 = 0; +const MIN_TX_GAS: u64 = 5_000_000; +const LOW_GAS_LIMIT: u64 = 1_000_000; // Creates the Rusk initial state for the tests below async fn initial_state>(dir: P) -> Result { @@ -130,6 +132,13 @@ fn make_transactions( tx_1.gas_spent < GAS_LIMIT_1, "Successful transaction should consume less than provided" ); + assert!( + tx_1.gas_spent >= MIN_TX_GAS, + "Successful transaction should be charged at least MIN_TX_GAS \ + (got {}, expected >= {})", + tx_1.gas_spent, + MIN_TX_GAS, + ); } #[tokio::test(flavor = "multi_thread")] @@ -172,3 +181,56 @@ pub async fn erroring_tx_charged_full() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +pub async fn tx_below_min_tx_gas_is_discarded() -> Result<()> { + // Setup the logger + logger(); + + let tmp = tempdir().expect("Should be able to create temporary directory"); + let rusk = initial_state(&tmp).await?; + + let cache = Arc::new(RwLock::new(HashMap::new())); + + let wallet = wallet::Wallet::new( + TestStore, + TestStateClient { + rusk: rusk.clone(), + cache, + }, + ); + + let mut rng = StdRng::seed_from_u64(0xbeef); + + // This is the same call as in make_transactions, but with a gas_limit + // that is below the MIN_TX_GAS + let contract_call = ContractCall::new(TRANSFER_CONTRACT, "root"); + let tx_low = wallet + .phoenix_execute( + &mut rng, + SENDER_INDEX_0, + LOW_GAS_LIMIT, + GAS_PRICE, + DEPOSIT, + TransactionData::Call(contract_call), + ) + .expect("Making the transaction should succeed"); + + // With the MIN_TX_GAS feature active, this tx must be discarded + // during block generation + let res = generator_procedure( + &rusk, + &[tx_low], + BLOCK_HEIGHT, + BLOCK_GAS_LIMIT, + vec![], + Some(ExecuteResult { + executed: 0, + discarded: 1, + }), + ); + + res.expect("generator procedure should succeed"); + + Ok(()) +} From ff702643982768ae68dc04f1313fb7a1ba63ff0d Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Thu, 11 Dec 2025 13:25:13 +0100 Subject: [PATCH 4/4] rusk: Gas floor per TX RFCs --- node/src/mempool.rs | 4 ++-- rusk/CHANGELOG.md | 2 +- rusk/src/lib/node/vm.rs | 4 ++-- rusk/src/lib/node/vm/config.rs | 26 ++++++++++++-------------- rusk/src/lib/node/vm/config/known.rs | 22 ++++++++-------------- rusk/src/lib/node/vm/config/opt.rs | 12 ++++-------- vm/src/execute.rs | 7 ++----- 7 files changed, 31 insertions(+), 46 deletions(-) diff --git a/node/src/mempool.rs b/node/src/mempool.rs index 553aa9ba3d..2bc5ad8617 100644 --- a/node/src/mempool.rs +++ b/node/src/mempool.rs @@ -61,7 +61,7 @@ pub enum TxAcceptanceError { VerificationFailed(String), #[error("gas price lower than minimum {0}")] GasPriceTooLow(u64), - #[error("gas limit lower than minimum {0} LUX")] + #[error("gas limit lower than minimum {0}")] GasLimitTooLow(u64), #[error("Maximum count of transactions exceeded {0}")] MaxTxnCountExceeded(usize), @@ -349,7 +349,7 @@ impl MempoolSrv { dusk_consensus::validate_blob_sidecars(tx)?; } - // Check global minimum gas limit and per-tx fee floor + // Check global minimum gas limit and per-tx gas floor let chain_min_gas_limit = vm.min_gas_limit(); let min_tx_gas = vm.min_tx_gas(tip_height); let required_gas_limit = diff --git a/rusk/CHANGELOG.md b/rusk/CHANGELOG.md index f749519d10..be912e3249 100644 --- a/rusk/CHANGELOG.md +++ b/rusk/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add TX floor price feature, including for well-known chain ids [#3940] +- Add TX gas floor feature, including for well-known chain ids [#3940] ## [1.4.1] - 2025-12-04 diff --git a/rusk/src/lib/node/vm.rs b/rusk/src/lib/node/vm.rs index 0edc022d8a..2e57d29dfb 100644 --- a/rusk/src/lib/node/vm.rs +++ b/rusk/src/lib/node/vm.rs @@ -325,11 +325,11 @@ impl VMExecution for Rusk { fn min_tx_gas(&self, height: u64) -> u64 { self.vm_config .feature(FEATURE_MIN_TX_GAS) - .map(|activation| { + .and_then(|activation| { if activation.is_active_at(height) { self.vm_config.min_tx_gas } else { - 0 + None } }) .unwrap_or(0) diff --git a/rusk/src/lib/node/vm/config.rs b/rusk/src/lib/node/vm/config.rs index fefe1beab7..ec0a5d4236 100644 --- a/rusk/src/lib/node/vm/config.rs +++ b/rusk/src/lib/node/vm/config.rs @@ -21,7 +21,6 @@ const DEFAULT_GAS_PER_BLOB: u64 = 1_000_000; const DEFAULT_MIN_DEPLOY_POINTS: u64 = 5_000_000; const DEFAULT_MIN_DEPLOYMENT_GAS_PRICE: u64 = 2_000; const DEFAULT_BLOCK_GAS_LIMIT: u64 = 3 * 1_000_000_000; -/// Default per‑tx minimum gas floor. 0 disables it. const DEFAULT_MIN_TX_GAS: u64 = 5_000_000; /// Configuration for the execution of a transaction. @@ -47,8 +46,8 @@ pub struct Config { #[serde(with = "humantime_serde")] pub generation_timeout: Option, - /// Minimum gas charged for any transaction. - pub min_tx_gas: u64, + /// Optional minimum gas charged for any transaction. + pub min_tx_gas: Option, /// Set of features to activate features: HashMap, @@ -79,7 +78,7 @@ impl Config { min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, - min_tx_gas: DEFAULT_MIN_TX_GAS, + min_tx_gas: None, generation_timeout: None, features: HashMap::new(), } @@ -130,7 +129,7 @@ impl Config { /// Set the minimum gas charged for any transaction. pub const fn with_min_tx_gas(mut self, min_tx_gas: u64) -> Self { - self.min_tx_gas = min_tx_gas; + self.min_tx_gas = Some(min_tx_gas); self } @@ -156,16 +155,15 @@ impl Config { .feature(feature::FEATURE_DISABLE_3RD_PARTY) .map(|activation| activation.is_active_at(block_height)) .unwrap_or_default(); - let min_tx_gas = self + let min_tx_gas = if self .feature(feature::FEATURE_MIN_TX_GAS) - .map(|activation| { - if activation.is_active_at(block_height) { - self.min_tx_gas - } else { - 0 - } - }) - .unwrap_or(0); + .map(|activation| activation.is_active_at(block_height)) + .unwrap_or_default() + { + self.min_tx_gas.unwrap_or(0) + } else { + 0 + }; ExecutionConfig { gas_per_blob: self.gas_per_blob, diff --git a/rusk/src/lib/node/vm/config/known.rs b/rusk/src/lib/node/vm/config/known.rs index eac1195a61..f503af6ca8 100644 --- a/rusk/src/lib/node/vm/config/known.rs +++ b/rusk/src/lib/node/vm/config/known.rs @@ -39,7 +39,7 @@ pub struct WellKnownConfig { pub min_deploy_points: u64, pub min_deployment_gas_price: u64, pub block_gas_limit: u64, - pub min_tx_gas: u64, + pub min_tx_gas: Option, pub features: [(&'static str, FeatureActivation); 7], } @@ -83,9 +83,6 @@ static MAINNET_DISABLE_WASM_64: LazyLock = const MAINNET_BLOB_ACTIVATION: FeatureActivation = FeatureActivation::Height(MAINNET_AT_10_12_2025_AT_09_00_UTC); -const MAINNET_MIN_TX_GAS_ACTIVATION: FeatureActivation = - FeatureActivation::Height(3_060_210); - /// Mainnet VM configuration. static MAINNET_CONFIG: LazyLock = LazyLock::new(|| WellKnownConfig { @@ -94,7 +91,7 @@ static MAINNET_CONFIG: LazyLock = min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, - min_tx_gas: DEFAULT_MIN_TX_GAS, + min_tx_gas: None, features: [ (FEATURE_ABI_PUBLIC_SENDER, MAINNET_SENDER_ACTIVATION_HEIGHT), (HQ_KECCAK256, NEVER), @@ -102,7 +99,7 @@ static MAINNET_CONFIG: LazyLock = (FEATURE_DISABLE_WASM64, MAINNET_DISABLE_WASM_64.clone()), (FEATURE_DISABLE_WASM32, MAINNET_3RD_PARTY_OFF.clone()), (FEATURE_DISABLE_3RD_PARTY, MAINNET_3RD_PARTY_OFF.clone()), - (FEATURE_MIN_TX_GAS, MAINNET_MIN_TX_GAS_ACTIVATION), + (FEATURE_MIN_TX_GAS, NEVER), ], }); @@ -110,9 +107,6 @@ static MAINNET_CONFIG: LazyLock = const TESTNET_AT_12_11_2025_AT_09_00_UTC: FeatureActivation = FeatureActivation::Height(1_814_090); -const TESTNET_MIN_TX_GAS_ACTIVATION: FeatureActivation = - FeatureActivation::Height(2_242_800); - /// Testnet VM configuration. const TESTNET_CONFIG: WellKnownConfig = WellKnownConfig { gas_per_blob: DEFAULT_GAS_PER_BLOB, @@ -120,7 +114,7 @@ const TESTNET_CONFIG: WellKnownConfig = WellKnownConfig { min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, - min_tx_gas: DEFAULT_MIN_TX_GAS, + min_tx_gas: None, features: [ (FEATURE_ABI_PUBLIC_SENDER, GENESIS), (HQ_KECCAK256, NEVER), @@ -128,7 +122,7 @@ const TESTNET_CONFIG: WellKnownConfig = WellKnownConfig { (FEATURE_DISABLE_WASM64, TESTNET_AT_12_11_2025_AT_09_00_UTC), (FEATURE_DISABLE_WASM32, NEVER), (FEATURE_DISABLE_3RD_PARTY, NEVER), - (FEATURE_MIN_TX_GAS, TESTNET_MIN_TX_GAS_ACTIVATION), + (FEATURE_MIN_TX_GAS, NEVER), ], }; @@ -139,7 +133,7 @@ const DEVNET_CONFIG: WellKnownConfig = WellKnownConfig { min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, - min_tx_gas: DEFAULT_MIN_TX_GAS, + min_tx_gas: None, features: [ (FEATURE_ABI_PUBLIC_SENDER, GENESIS), (HQ_KECCAK256, GENESIS), @@ -147,7 +141,7 @@ const DEVNET_CONFIG: WellKnownConfig = WellKnownConfig { (FEATURE_DISABLE_WASM64, GENESIS), (FEATURE_DISABLE_WASM32, NEVER), (FEATURE_DISABLE_3RD_PARTY, NEVER), - (FEATURE_MIN_TX_GAS, GENESIS), + (FEATURE_MIN_TX_GAS, NEVER), ], }; @@ -158,7 +152,7 @@ const LOCALNET_CONFIG: WellKnownConfig = WellKnownConfig { min_deploy_points: DEFAULT_MIN_DEPLOY_POINTS, min_deployment_gas_price: DEFAULT_MIN_DEPLOYMENT_GAS_PRICE, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, - min_tx_gas: DEFAULT_MIN_TX_GAS, + min_tx_gas: Some(DEFAULT_MIN_TX_GAS), features: [ (FEATURE_ABI_PUBLIC_SENDER, GENESIS), (HQ_KECCAK256, GENESIS), diff --git a/rusk/src/lib/node/vm/config/opt.rs b/rusk/src/lib/node/vm/config/opt.rs index f8534921db..60238e79a0 100644 --- a/rusk/src/lib/node/vm/config/opt.rs +++ b/rusk/src/lib/node/vm/config/opt.rs @@ -114,11 +114,9 @@ impl OptionalConfig { config.block_gas_limit, ); - Self::set_or_warn( - "min_tx_gas", - &mut self.min_tx_gas, - config.min_tx_gas, - ); + if let Some(value) = config.min_tx_gas { + Self::set_or_warn("min_tx_gas", &mut self.min_tx_gas, value); + } for (feature, activation) in &config.features { if let Some(v) = self.feature(feature) { @@ -201,9 +199,7 @@ impl TryFrom for Config { block_gas_limit: value .block_gas_limit .ok_or(anyhow!("Missing block_gas_limit"))?, - min_tx_gas: value - .min_tx_gas - .ok_or(anyhow!("Missing min_tx_gas"))?, + min_tx_gas: value.min_tx_gas, generation_timeout: value.generation_timeout, features: value.features, }) diff --git a/vm/src/execute.rs b/vm/src/execute.rs index befc528dd0..a086880173 100644 --- a/vm/src/execute.rs +++ b/vm/src/execute.rs @@ -159,11 +159,8 @@ pub fn execute( } else if config.min_tx_gas > 0 { // On success, enforce the global per-tx minimum gas floor if the // feature min_tx_gas is enabled - use core::cmp::min; - - let floor = min(config.min_tx_gas, receipt.gas_limit); - if receipt.gas_spent < floor { - receipt.gas_spent = floor; + if receipt.gas_spent < config.min_tx_gas { + receipt.gas_spent = config.min_tx_gas; } }