Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- First `dusk-node` release

<!-- Issues -->
[#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
Expand Down
14 changes: 10 additions & 4 deletions node/src/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 =
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,
));
}
}

Expand Down
1 change: 1 addition & 0 deletions node/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
5 changes: 5 additions & 0 deletions rusk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add TX gas floor feature, including for well-known chain ids [#3940]

## [1.4.1] - 2025-12-04

### Added
Expand Down Expand Up @@ -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.

<!-- Issues -->
[#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
Expand Down
13 changes: 13 additions & 0 deletions rusk/src/lib/node/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would return this as Option to be aligned with its meaning

self.vm_config
.feature(FEATURE_MIN_TX_GAS)
.and_then(|activation| {
if activation.is_active_at(height) {
self.vm_config.min_tx_gas
} else {
None
}
})
.unwrap_or(0)
}
}

fn has_unique_elements<T>(iter: T) -> bool
Expand Down
23 changes: 23 additions & 0 deletions rusk/src/lib/node/vm/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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;
const DEFAULT_MIN_TX_GAS: u64 = 5_000_000;

/// Configuration for the execution of a transaction.
#[derive(Debug, Clone, serde::Serialize)]
Expand All @@ -45,6 +46,9 @@ pub struct Config {
#[serde(with = "humantime_serde")]
pub generation_timeout: Option<Duration>,

/// Optional minimum gas charged for any transaction.
pub min_tx_gas: Option<u64>,

/// Set of features to activate
features: HashMap<String, FeatureActivation>,
}
Expand All @@ -61,6 +65,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";
}
Expand All @@ -73,6 +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: None,
generation_timeout: None,
features: HashMap::new(),
}
Expand Down Expand Up @@ -121,6 +127,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 = Some(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
Expand All @@ -143,11 +155,22 @@ impl Config {
.feature(feature::FEATURE_DISABLE_3RD_PARTY)
.map(|activation| activation.is_active_at(block_height))
.unwrap_or_default();
let min_tx_gas = if self
.feature(feature::FEATURE_MIN_TX_GAS)
.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,
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,
Expand Down
16 changes: 14 additions & 2 deletions rusk/src/lib/node/vm/config/known.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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: Option<u64>,
pub features: [(&'static str, FeatureActivation); 7],
}

impl WellKnownConfig {
Expand Down Expand Up @@ -87,13 +91,15 @@ static MAINNET_CONFIG: LazyLock<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: None,
features: [
(FEATURE_ABI_PUBLIC_SENDER, MAINNET_SENDER_ACTIVATION_HEIGHT),
(HQ_KECCAK256, NEVER),
(FEATURE_BLOB, MAINNET_BLOB_ACTIVATION),
(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, NEVER),
],
});

Expand All @@ -108,13 +114,15 @@ 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: None,
features: [
(FEATURE_ABI_PUBLIC_SENDER, GENESIS),
(HQ_KECCAK256, NEVER),
(FEATURE_BLOB, TESTNET_AT_12_11_2025_AT_09_00_UTC),
(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, NEVER),
],
};

Expand All @@ -125,13 +133,15 @@ 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: None,
features: [
(FEATURE_ABI_PUBLIC_SENDER, GENESIS),
(HQ_KECCAK256, GENESIS),
(FEATURE_BLOB, GENESIS),
(FEATURE_DISABLE_WASM64, GENESIS),
(FEATURE_DISABLE_WASM32, NEVER),
(FEATURE_DISABLE_3RD_PARTY, NEVER),
(FEATURE_MIN_TX_GAS, NEVER),
],
};

Expand All @@ -142,12 +152,14 @@ 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: Some(DEFAULT_MIN_TX_GAS),
features: [
(FEATURE_ABI_PUBLIC_SENDER, GENESIS),
(HQ_KECCAK256, GENESIS),
(FEATURE_BLOB, GENESIS),
(FEATURE_DISABLE_WASM64, GENESIS),
(FEATURE_DISABLE_WASM32, NEVER),
(FEATURE_DISABLE_3RD_PARTY, NEVER),
(FEATURE_MIN_TX_GAS, GENESIS),
],
};
8 changes: 8 additions & 0 deletions rusk/src/lib/node/vm/config/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub struct OptionalConfig {
#[serde(default, with = "humantime_serde")]
pub generation_timeout: Option<Duration>,

/// Minimum gas charged for any transaction.
pub min_tx_gas: Option<u64>,

/// Set of features to activate
#[serde(default)]
features: HashMap<String, FeatureActivation>,
Expand Down Expand Up @@ -111,6 +114,10 @@ impl OptionalConfig {
config.block_gas_limit,
);

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) {
if v != activation {
Expand Down Expand Up @@ -192,6 +199,7 @@ impl TryFrom<OptionalConfig> for Config {
block_gas_limit: value
.block_gas_limit
.ok_or(anyhow!("Missing block_gas_limit"))?,
min_tx_gas: value.min_tx_gas,
generation_timeout: value.generation_timeout,
features: value.features,
})
Expand Down
64 changes: 63 additions & 1 deletion rusk/tests/services/gas_behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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<P: AsRef<Path>>(dir: P) -> Result<Rusk> {
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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(())
}
5 changes: 5 additions & 0 deletions vm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]

<!-- Issues -->
[#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
Expand Down
Loading
Loading