Skip to content

Commit 0ec1bb6

Browse files
authored
Merge pull request #3831 from dusk-network/charge_blob
rusk: add gas_per_blob configuration
2 parents 79e906d + f88cf96 commit 0ec1bb6

File tree

12 files changed

+247
-49
lines changed

12 files changed

+247
-49
lines changed

consensus/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ mod validation;
2727

2828
pub use ratification::step::build_ratification_payload;
2929
pub use validation::step::build_validation_payload;
30-
pub use validation::step::validate_blobs;
30+
pub use validation::step::validate_blob_sidecars;
3131

3232
mod iteration_ctx;
3333
pub mod merkle;

consensus/src/validation/step.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ impl<T: Operations + 'static, D: Database> ValidationStep<T, D> {
145145

146146
for tx in candidate.txs().iter() {
147147
// Validate blobs
148-
validate_blobs(tx).map_err(|e| {
148+
validate_blob_sidecars(tx).map_err(|e| {
149149
OperationError::InvalidBlob(format!(
150150
"Failed to validate blobs in transaction {}: {e}",
151151
hex::encode(tx.id())
@@ -187,17 +187,16 @@ impl<T: Operations + 'static, D: Database> ValidationStep<T, D> {
187187
}
188188
}
189189

190-
pub fn validate_blobs(tx: &Transaction) -> Result<(), BlobError> {
190+
/// Validates the blob sidecars of a transaction.
191+
///
192+
/// This function checks the following:
193+
/// - Each blob has a valid commitment.
194+
/// - Each blob has a valid KZG proof.
195+
/// - Each blob's hash matches the commitment in the sidecar.
196+
///
197+
/// If the transaction is not a blob transaction, it returns `Ok(())`.
198+
pub fn validate_blob_sidecars(tx: &Transaction) -> Result<(), BlobError> {
191199
if let Some(blobs) = tx.inner.blob() {
192-
match blobs.len() {
193-
0 => Err(BlobError::BlobEmpty),
194-
n if n > 6 => Err(BlobError::BlobTooMany(n)),
195-
_n => {
196-
// TODO: Add checks for gas price and gas limit
197-
Ok(())
198-
}
199-
}?;
200-
201200
for blob in blobs {
202201
// Check sidecar is present
203202
let sidecar = blob

core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Add support for `TransactionData::Blob`
13+
- Add `error::TxPreconditionError`
14+
- Add `transfer::deploy_check`
15+
- Add `transfer::blob_check`
1316

1417
## [1.3.0] - 2025-04-17
1518

core/src/error.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
//! Error-type for dusk-core.
88
9-
use alloc::string::String;
9+
use alloc::string::{String, ToString};
1010
use core::fmt;
1111

1212
/// The dusk-core error type.
@@ -76,3 +76,47 @@ impl From<dusk_bytes::Error> for Error {
7676
}
7777
}
7878
}
79+
80+
/// Error type for checking transaction conditions.
81+
///
82+
/// This error is used to indicate that a transaction does not meet the
83+
/// minimum requirements for deployment or blob gas charges.
84+
#[derive(Debug, Clone, PartialEq)]
85+
pub enum TxPreconditionError {
86+
/// The gas price is too low to deploy a transaction.
87+
DeployLowPrice(u64),
88+
/// The gas limit is too low to deploy a transaction.
89+
DeployLowLimit(u64),
90+
/// The gas limit is too low to cover the blob gas charges.
91+
BlobLowLimit(u64),
92+
/// No blob attached to the transaction.
93+
BlobEmpty,
94+
/// Too many blobs attached to the transaction.
95+
BlobTooMany(usize),
96+
}
97+
98+
impl TxPreconditionError {
99+
/// Return the implementation of toString to be used inside the VM.
100+
///
101+
/// Replacing this with the standard display will break the state root
102+
/// backward compatibility
103+
#[must_use]
104+
pub fn legacy_to_string(&self) -> String {
105+
match self {
106+
TxPreconditionError::DeployLowPrice(_) => {
107+
"gas price too low to deploy"
108+
}
109+
TxPreconditionError::DeployLowLimit(_) => {
110+
"not enough gas to deploy"
111+
}
112+
TxPreconditionError::BlobLowLimit(_) => "not enough gas for blobs",
113+
TxPreconditionError::BlobEmpty => {
114+
"no blob attached to the transaction"
115+
}
116+
TxPreconditionError::BlobTooMany(_) => {
117+
"too many blobs in the transaction"
118+
}
119+
}
120+
.to_string()
121+
}
122+
}

core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub mod stake;
2525
pub mod transfer;
2626

2727
mod error;
28-
pub use error::Error;
28+
pub use error::{Error, TxPreconditionError};
2929

3030
mod dusk;
3131
pub use dusk::{dusk, from_dusk, Dusk, LUX};

core/src/transfer.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use rand::{CryptoRng, RngCore};
2222
use rkyv::{Archive, Deserialize, Serialize};
2323

2424
use crate::abi::ContractId;
25+
use crate::error::TxPreconditionError;
2526
use crate::signatures::bls::{
2627
PublicKey as AccountPublicKey, SecretKey as AccountSecretKey,
2728
};
@@ -430,6 +431,79 @@ impl Transaction {
430431
0
431432
}
432433
}
434+
435+
/// Returns the minimum gas charged for a blob transaction deployment.
436+
/// If the transaction is not a blob transaction, it returns None.
437+
#[must_use]
438+
pub fn blob_charge(&self, gas_per_blob: u64) -> Option<u64> {
439+
self.blob().map(|blobs| blobs.len() as u64 * gas_per_blob)
440+
}
441+
442+
/// Check if the transaction is a deployment transaction and if it
443+
/// meets the minimum requirements for gas price and gas limit.
444+
///
445+
/// # Errors
446+
/// Returns an error if the transaction is a deployment transaction and
447+
/// the gas price is lower than the minimum required gas price or if the
448+
/// gas limit is lower than the required deployment charge.
449+
pub fn deploy_check(
450+
&self,
451+
gas_per_deploy_byte: u64,
452+
min_deploy_gas_price: u64,
453+
min_deploy_points: u64,
454+
) -> Result<(), TxPreconditionError> {
455+
if self.deploy().is_some() {
456+
let deploy_charge =
457+
self.deploy_charge(gas_per_deploy_byte, min_deploy_points);
458+
459+
if self.gas_price() < min_deploy_gas_price {
460+
return Err(TxPreconditionError::DeployLowPrice(
461+
min_deploy_gas_price,
462+
));
463+
}
464+
if self.gas_limit() < deploy_charge {
465+
return Err(TxPreconditionError::DeployLowLimit(deploy_charge));
466+
}
467+
}
468+
469+
Ok(())
470+
}
471+
472+
/// Check if the transaction is a blob transaction and if it meets the
473+
/// minimum requirements for gas limit.
474+
///
475+
/// # Returns
476+
/// - `Ok(Some(u64))` the minimum gas amount to charge if the transaction is
477+
/// a blob transaction.
478+
/// - `Ok(None)` if the transaction is not a blob transaction.
479+
/// - `Err(TxPreconditionError)` in case of an error.
480+
///
481+
/// # Errors
482+
/// Returns an error if the transaction is a blob transaction and:
483+
/// - the gas limit is lower than the minimum charge.
484+
/// - the number of blobs is zero or greater than 6.
485+
pub fn blob_check(
486+
&self,
487+
gas_per_blob: u64,
488+
) -> Result<Option<u64>, TxPreconditionError> {
489+
if let Some(blobs) = self.blob() {
490+
match blobs.len() {
491+
0 => Err(TxPreconditionError::BlobEmpty),
492+
n if n > 6 => Err(TxPreconditionError::BlobTooMany(n)),
493+
_ => Ok(()),
494+
}?;
495+
} else {
496+
return Ok(None);
497+
}
498+
499+
let min_charge = self.blob_charge(gas_per_blob);
500+
if let Some(min_charge) = min_charge {
501+
if self.gas_limit() < min_charge {
502+
return Err(TxPreconditionError::BlobLowLimit(min_charge));
503+
}
504+
}
505+
Ok(min_charge)
506+
}
433507
}
434508

435509
impl From<PhoenixTransaction> for Transaction {

node/src/mempool.rs

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use conf::{
1616
};
1717
use dusk_consensus::config::MAX_BLOCK_SIZE;
1818
use dusk_consensus::errors::BlobError;
19+
use dusk_core::TxPreconditionError;
1920
use node_data::events::{Event, TransactionEvent};
2021
use node_data::get_current_timestamp;
2122
use node_data::ledger::{Header, SpendingId, Transaction};
@@ -89,6 +90,26 @@ impl From<BlobError> for TxAcceptanceError {
8990
}
9091
}
9192

93+
impl From<TxPreconditionError> for TxAcceptanceError {
94+
fn from(err: TxPreconditionError) -> Self {
95+
match err {
96+
TxPreconditionError::BlobLowLimit(min) => {
97+
TxAcceptanceError::GasLimitTooLow(min)
98+
}
99+
TxPreconditionError::DeployLowLimit(min) => {
100+
TxAcceptanceError::GasLimitTooLow(min)
101+
}
102+
TxPreconditionError::DeployLowPrice(min) => {
103+
TxAcceptanceError::GasPriceTooLow(min)
104+
}
105+
TxPreconditionError::BlobEmpty => TxAcceptanceError::BlobEmpty,
106+
TxPreconditionError::BlobTooMany(n) => {
107+
TxAcceptanceError::BlobTooMany(n)
108+
}
109+
}
110+
}
111+
}
112+
92113
pub struct MempoolSrv {
93114
inbound: AsyncQueue<Message>,
94115
conf: Params,
@@ -258,27 +279,43 @@ impl MempoolSrv {
258279
return Err(TxAcceptanceError::GasPriceTooLow(1));
259280
}
260281

261-
if tx.inner.deploy().is_some() {
262-
// TODO: Remove this duplicated code in favor of vm::deploy_check
282+
{
283+
// Mimic the VM's additional checks for transactions
263284
let vm = vm.read().await;
264-
let min_deployment_gas_price = vm.min_deployment_gas_price();
265-
if tx.gas_price() < min_deployment_gas_price {
266-
return Err(TxAcceptanceError::GasPriceTooLow(
285+
286+
// Check deployment tx
287+
if tx.inner.deploy().is_some() {
288+
let min_deployment_gas_price = vm.min_deployment_gas_price();
289+
let gas_per_deploy_byte = vm.gas_per_deploy_byte();
290+
let min_deploy_points = vm.min_deploy_points();
291+
tx.inner.deploy_check(
292+
gas_per_deploy_byte,
267293
min_deployment_gas_price,
268-
));
294+
min_deploy_points,
295+
)?;
269296
}
270297

271-
let gas_per_deploy_byte = vm.gas_per_deploy_byte();
272-
let deploy_charge = tx
273-
.inner
274-
.deploy_charge(gas_per_deploy_byte, vm.min_deploy_points());
275-
if tx.inner.gas_limit() < deploy_charge {
276-
return Err(TxAcceptanceError::GasLimitTooLow(deploy_charge));
298+
// Check blob tx
299+
if tx.inner.blob().is_some() {
300+
db.read()
301+
.await
302+
.view(|db| {
303+
db.block_label_by_height(vm.blob_activation_height())
304+
})
305+
.map_err(|e| {
306+
anyhow!("Cannot get blob activation height: {e}")
307+
})?
308+
.ok_or(anyhow!(
309+
"Blobs acceptance will start at block height: {}",
310+
vm.blob_activation_height()
311+
))?;
312+
313+
let gas_per_blob = vm.gas_per_blob();
314+
tx.inner.blob_check(gas_per_blob)?;
315+
dusk_consensus::validate_blob_sidecars(tx)?;
277316
}
278-
} else if tx.inner.blob().is_some() {
279-
dusk_consensus::validate_blobs(tx)?;
280-
} else {
281-
let vm = vm.read().await;
317+
318+
// Check global minimum gas limit
282319
let min_gas_limit = vm.min_gas_limit();
283320
if tx.inner.gas_limit() < min_gas_limit {
284321
return Err(TxAcceptanceError::GasLimitTooLow(min_gas_limit));

node/src/vm.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ pub trait VMExecution: Send + Sync + 'static {
9292
fn min_deployment_gas_price(&self) -> u64;
9393
fn min_gas_limit(&self) -> u64;
9494
fn min_deploy_points(&self) -> u64;
95+
96+
fn gas_per_blob(&self) -> u64;
97+
fn blob_activation_height(&self) -> u64;
9598
}
9699

97100
#[allow(clippy::large_enum_variant)]

rusk/src/lib/node/vm.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@ impl VMExecution for Rusk {
287287
fn min_deploy_points(&self) -> u64 {
288288
self.vm_config.min_deploy_points
289289
}
290+
291+
fn gas_per_blob(&self) -> u64 {
292+
self.vm_config.gas_per_blob
293+
}
294+
295+
fn blob_activation_height(&self) -> u64 {
296+
self.vm_config.feature(FEATURE_BLOB).unwrap_or(u64::MAX)
297+
}
290298
}
291299

292300
fn has_unique_elements<T>(iter: T) -> bool

rusk/src/lib/node/vm/config.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use serde::{Deserialize, Serialize};
1313
const fn default_gas_per_deploy_byte() -> u64 {
1414
100
1515
}
16+
17+
// TODO: This is a temporary value. Change this value to the tuned one as soon
18+
// as it's rolled out.
19+
const fn default_gas_per_blob() -> u64 {
20+
0
21+
}
22+
1623
const fn default_min_deploy_points() -> u64 {
1724
5_000_000
1825
}
@@ -26,6 +33,10 @@ const fn default_block_gas_limit() -> u64 {
2633
/// Configuration for the execution of a transaction.
2734
#[derive(Debug, Clone, Serialize, Deserialize)]
2835
pub struct Config {
36+
/// The amount of gas points charged for each blob in a transaction
37+
#[serde(default = "default_gas_per_blob")]
38+
pub gas_per_blob: u64,
39+
2940
/// The amount of gas points charged for each byte in a contract-deployment
3041
/// bytecode.
3142
#[serde(default = "default_gas_per_deploy_byte")]
@@ -60,11 +71,13 @@ impl Default for Config {
6071

6172
pub(crate) mod feature {
6273
pub const FEATURE_ABI_PUBLIC_SENDER: &str = "ABI_PUBLIC_SENDER";
74+
pub const FEATURE_BLOB: &str = "BLOB";
6375
}
6476

6577
impl Config {
6678
pub fn new() -> Self {
6779
Self {
80+
gas_per_blob: default_gas_per_blob(),
6881
gas_per_deploy_byte: default_gas_per_deploy_byte(),
6982
min_deployment_gas_price: default_min_deployment_gas_price(),
7083
min_deploy_points: default_min_deploy_points(),
@@ -123,11 +136,17 @@ impl Config {
123136
.feature(feature::FEATURE_ABI_PUBLIC_SENDER)
124137
.map(|activation| block_height >= activation)
125138
.unwrap_or_default();
139+
let with_blob = self
140+
.feature(feature::FEATURE_BLOB)
141+
.map(|activation| block_height >= activation)
142+
.unwrap_or_default();
126143
ExecutionConfig {
144+
gas_per_blob: self.gas_per_blob,
127145
gas_per_deploy_byte: self.gas_per_deploy_byte,
128146
min_deploy_points: self.min_deploy_points,
129147
min_deploy_gas_price: self.min_deployment_gas_price,
130148
with_public_sender,
149+
with_blob,
131150
}
132151
}
133152

0 commit comments

Comments
 (0)