Skip to content

Commit

Permalink
feat: add gas total to tx receipt
Browse files Browse the repository at this point in the history
This commit introduces the "gas total" as a new attribute in the
transaction receipt. With this addition, the gas price elasticity can be
calculated based on the demand of previous blocks.

This feature also allows setting a base fee for every transaction, which
is paid by the sender to cover processing costs such as deserialization
and signature verification. Insufficient funds from the sender will
cause the fee to be charged to the sequencer, leading to a failed
transaction and thwarting potential attacks. The sequencer should have
sufficient funds available from their staked amount to cover these
expenses; the implementation of the sequencer registry is responsible
for ensuring the sequencer possesses adequate funds for selecting
transactions to assemble into a blob.

To define the base cost constant, an adjustment was made to make this
feature available under the `sov-modules-macros`. Due to the need for
defining slices of unknown size for the constants file, this
functionality had to be provided there as well.
  • Loading branch information
vlopes11 committed Jan 3, 2024
1 parent 8faf706 commit 685b2c1
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 24 deletions.
4 changes: 3 additions & 1 deletion constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
},
"constants": {
"DEFERRED_SLOTS_COUNT": 2,
"GAS_TOKEN_ADDRESS": "sov1p9xxgsh78u3nxsl0zhfq4eazy0y4c8m5psjv3k3vrv45859jgazq3x72sg"
"GAS_TOKEN_ADDRESS": "sov1p9xxgsh78u3nxsl0zhfq4eazy0y4c8m5psjv3k3vrv45859jgazq3x72sg",
"GAS_TX_FIXED_COST": [0, 0],
"GAS_TX_COST_PER_BYTE": [0, 0]
}
}
7 changes: 7 additions & 0 deletions constants.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"comment": "Sovereign SDK constants",
"constants": {
"GAS_TOKEN_ADDRESS": "sov1p9xxgsh78u3nxsl0zhfq4eazy0y4c8m5psjv3k3vrv45859jgazq3x72sg",
"GAS_TX_FIXED_COST": [0, 0],
"GAS_TX_COST_PER_BYTE": [0, 0],
"TEST_U32": 42,
"TEST_BOOL": true,
"TEST_STRING": "Some Other String",
Expand Down Expand Up @@ -50,6 +52,11 @@
11,
11,
11
],
"TEST_SLICE": [
11,
11,
11
]
},
"gas": {
Expand Down
3 changes: 3 additions & 0 deletions examples/demo-rollup/src/test_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ fn batch2_tx_receipts() -> Vec<TransactionReceipt<u32>> {
body_to_save: Some(b"tx body".to_vec()),
events: vec![],
receipt: 0,
gas_total: vec![0, 0],
})
.collect()
}
Expand All @@ -113,6 +114,7 @@ fn regular_test_helper(payload: serde_json::Value, expected: &serde_json::Value)
body_to_save: Some(b"tx1 body".to_vec()),
events: vec![],
receipt: 0,
gas_total: vec![0, 0],
},
TransactionReceipt::<u32> {
tx_hash: ::sha2::Sha256::digest(b"tx2"),
Expand All @@ -122,6 +124,7 @@ fn regular_test_helper(payload: serde_json::Value, expected: &serde_json::Value)
Event::new("event2_key", "event2_value"),
],
receipt: 1,
gas_total: vec![2, 3],
},
],
inner: 0,
Expand Down
34 changes: 32 additions & 2 deletions module-system/module-implementations/sov-bank/src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use core::str::FromStr;
use sov_modules_api::hooks::TxHooks;
use sov_modules_api::macros::config_constant;
use sov_modules_api::transaction::Transaction;
use sov_modules_api::{Context, WorkingSet};
use sov_modules_api::{Context, GasUnit, WorkingSet};

use crate::{Bank, Coins};

Expand All @@ -21,6 +21,12 @@ use crate::{Bank, Coins};
// https://github.com/Sovereign-Labs/sovereign-sdk/issues/1234
const GAS_TOKEN_ADDRESS: &'static str;

#[config_constant]
const GAS_TX_FIXED_COST: &[u64];

#[config_constant]
const GAS_TX_COST_PER_BYTE: &[u64];

/// The computed addresses of a pre-dispatch tx hook.
pub struct BankTxHook<C: Context> {
/// The tx sender address
Expand All @@ -41,8 +47,32 @@ impl<C: Context> TxHooks for Bank<C> {
hook: &BankTxHook<C>,
) -> anyhow::Result<()> {
let BankTxHook { sender, sequencer } = hook;
let amount = tx.gas_limit().saturating_add(tx.gas_tip());

// Charge the base tx gas cost
let gas_fixed_cost = tx.gas_fixed_cost();
if working_set.charge_gas(&gas_fixed_cost).is_err() {
let amount = gas_fixed_cost.value(working_set.gas_price());
let token_address = C::Address::from_str(GAS_TOKEN_ADDRESS)
.map_err(|_| anyhow::anyhow!("failed to parse gas token address"))?;
let coins = Coins {
amount,
token_address,
};

// If the sender's account balance is insufficient to cover the base global cost, the
// transaction execution should be halted and the deficiency should be deducted from
// the sequencer's account. It is expected that a sequencer would have adequate funds,
// as the staked amount ought to be sufficient for executing any transaction count they
// choose. However, if a sequencer lacks sufficient staked funds, it indicates a
// critical design flaw in the sequencer registry.
self.burn(coins, sequencer, working_set).expect("Unrecoverable error: the sequencer doesn't have enough funds to pay for the transaction base cost.");

anyhow::bail!(
"Transaction sender doesn't have enough funds to pay for the transaction base cost"
);
}

let amount = tx.gas_limit().saturating_add(tx.gas_tip());
if amount > 0 {
let token_address = C::Address::from_str(GAS_TOKEN_ADDRESS)
.map_err(|_| anyhow::anyhow!("failed to parse gas token address"))?;
Expand Down
19 changes: 18 additions & 1 deletion module-system/sov-modules-api/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[cfg(feature = "native")]
use sov_modules_core::PrivateKey;
use sov_modules_core::{Context, Signature};
use sov_modules_core::{Context, GasUnit, Signature};
use sov_modules_macros::config_constant;
#[cfg(all(target_os = "zkvm", feature = "bench"))]
use sov_zk_cycle_macros::cycle_tracker;

Expand Down Expand Up @@ -69,6 +70,22 @@ impl<C: Context> Transaction<C> {
self.gas_limit
}

pub fn gas_fixed_cost(&self) -> C::GasUnit {
#[config_constant]
const GAS_TX_FIXED_COST: &[u64];

#[config_constant]
const GAS_TX_COST_PER_BYTE: &[u64];

let gas_tx_fixed_cost = C::GasUnit::from_arbitrary_dimensions(GAS_TX_FIXED_COST);
let mut gas_tx_cost = C::GasUnit::from_arbitrary_dimensions(GAS_TX_COST_PER_BYTE);

gas_tx_cost.scalar_product(self.runtime_msg.len() as u64);
gas_tx_cost.combine(&gas_tx_fixed_cost);

gas_tx_cost
}

/// Check whether the transaction has been signed correctly.
#[cfg_attr(all(target_os = "zkvm", feature = "bench"), cycle_tracker)]
pub fn verify(&self) -> anyhow::Result<()> {
Expand Down
40 changes: 40 additions & 0 deletions module-system/sov-modules-core/src/common/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ pub trait GasUnit: fmt::Debug + Clone + Send + Sync {
/// Creates a unit from a multi-dimensional unit with arbitrary dimension.
fn from_arbitrary_dimensions(dimensions: &[u64]) -> Self;

/// Creates a multi-dimensional representation of the unit.
fn into_dimensions(&self) -> Vec<u64>;

/// Converts the unit into a scalar value, given a price.
fn value(&self, price: &Self) -> u64;

/// In-place combination of gas units, resulting in an addition.
fn combine(&mut self, rhs: &Self);

/// In-place product of gas units, resulting in a multiplication.
fn scalar_product(&mut self, scalar: u64);
}

/// A multi-dimensional gas unit.
Expand All @@ -34,12 +43,28 @@ impl<const N: usize> GasUnit for TupleGasUnit<N> {
unit
}

fn into_dimensions(&self) -> Vec<u64> {
self.to_vec()
}

fn value(&self, price: &Self) -> u64 {
self.iter()
.zip(price.iter().copied())
.map(|(a, b)| a.saturating_mul(b))
.fold(0, |a, b| a.saturating_add(b))
}

fn combine(&mut self, rhs: &Self) {
for i in 0..N {
self[i] = self[i].saturating_add(rhs[i]);
}
}

fn scalar_product(&mut self, scalar: u64) {
for i in 0..N {
self[i] = self[i].saturating_mul(scalar);
}
}
}

/// A gas meter.
Expand All @@ -49,6 +74,7 @@ where
{
remaining_funds: u64,
gas_price: GU,
gas_total: GU,
}

impl<GU> Default for GasMeter<GU>
Expand All @@ -59,6 +85,7 @@ where
Self {
remaining_funds: 0,
gas_price: GU::ZEROED,
gas_total: GU::ZEROED,
}
}
}
Expand All @@ -72,6 +99,7 @@ where
Self {
remaining_funds,
gas_price,
gas_total: GU::ZEROED,
}
}

Expand All @@ -80,9 +108,21 @@ where
self.remaining_funds
}

/// Returns the total gas incurred.
pub const fn gas_total(&self) -> &GU {
&self.gas_total
}

/// Returns the gas price.
pub const fn gas_price(&self) -> &GU {
&self.gas_price
}

/// Deducts the provided gas unit from the remaining funds, computing the scalar value of the
/// funds from the price of the instance.
pub fn charge_gas(&mut self, gas: &GU) -> Result<()> {
self.gas_total.combine(gas);

let gas = gas.value(&self.gas_price);
self.remaining_funds = self
.remaining_funds
Expand Down
10 changes: 10 additions & 0 deletions module-system/sov-modules-core/src/storage/scratchpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,16 @@ impl<C: Context> WorkingSet<C> {
self.gas_meter.charge_gas(gas)
}

/// Returns the gas price.
pub const fn gas_price(&self) -> &C::GasUnit {
self.gas_meter.gas_price()
}

/// Returns the total gas incurred.
pub const fn gas_total(&self) -> &C::GasUnit {
self.gas_meter.gas_total()
}

/// Fetches given value and provides a proof of it presence/absence.
pub fn get_with_proof(
&mut self,
Expand Down
38 changes: 30 additions & 8 deletions module-system/sov-modules-macros/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,21 @@ impl<'a> Manifest<'a> {
)
})?;
let value = self.value_to_tokens(field, value, ty)?;
let output = quote::quote! {

if let Type::Reference(tr) = ty {
if tr.lifetime.is_none() {
let output = quote::quote! {
#(#attrs)*
#vis const #field: #ty = & #value;
};
return Ok(output);
}
}

Ok(quote::quote! {
#(#attrs)*
#vis const #field: #ty = #value;
};
Ok(output)
})
}

fn value_to_tokens(
Expand Down Expand Up @@ -329,17 +339,29 @@ impl<'a> Manifest<'a> {
Value::String(s) => Ok(quote::quote!(#s)),
Value::Array(arr) => {
let mut values = Vec::with_capacity(arr.len());
let ty = if let Type::Array(ty) = ty {
&ty.elem
} else {
return Err(Self::err(
let ty = match ty {
Type::Array(ty) => &ty.elem,
Type::Reference(ty) => {
match ty.elem.as_ref() {
Type::Slice(ty) => &ty.elem,
_ => return Err(Self::err(
&self.path,
field,
format!(
"Found value of type {:?} while parsing `{}` but expected a slice type ",
ty, field
),
)),
}
}
_ => return Err(Self::err(
&self.path,
field,
format!(
"Found value of type {:?} while parsing `{}` but expected an array type ",
ty, field
),
));
))
};
for (idx, value) in arr.iter().enumerate() {
values.push(self.value_to_tokens(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ pub const TEST_U32: u32;
#[config_constant]
pub const TEST_ARRAY_OF_U8: [u8; 32];

#[config_constant]
pub const TEST_SLICE: &[u8];

#[config_constant]
/// This one has a doc attr
pub const TEST_NESTED_ARRAY: [[u8; 3]; 2];
Expand All @@ -19,6 +22,7 @@ const TEST_STRING: &str;
fn main() {
assert_eq!(TEST_U32, 42);
assert_eq!(TEST_ARRAY_OF_U8, [11; 32]);
assert_eq!(TEST_SLICE, &[11; 3]);
assert_eq!(TEST_NESTED_ARRAY, [[7; 3]; 2]);
assert_eq!(TEST_BOOL, true);
assert_eq!(TEST_STRING, "Some Other String");
Expand Down
5 changes: 5 additions & 0 deletions module-system/sov-modules-stf-blueprint/src/stf_blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,20 @@ where
// Don't revert any state changes made by the pre_dispatch_hook even if the Tx is rejected.
// For example nonce for the relevant account is incremented.
error!("Stateful verification error - the sequencer included an invalid transaction: {}", e);
let gas_total = batch_workspace.gas_total().into_dimensions();
let receipt = TransactionReceipt {
tx_hash: raw_tx_hash,
body_to_save: None,
events: batch_workspace.take_events(),
receipt: TxEffect::Reverted,
gas_total,
};

tx_receipts.push(receipt);
continue;
}
};

// Commit changes after pre_dispatch_tx_hook
batch_workspace = batch_workspace.checkpoint().to_revertable();

Expand Down Expand Up @@ -248,11 +251,13 @@ where
};
debug!("Tx {} effect: {:?}", hex::encode(raw_tx_hash), tx_effect);

let gas_total = batch_workspace.gas_total().into_dimensions();
let receipt = TransactionReceipt {
tx_hash: raw_tx_hash,
body_to_save: None,
events,
receipt: tx_effect,
gas_total,
};

tx_receipts.push(receipt);
Expand Down
2 changes: 2 additions & 0 deletions rollup-interface/src/state_machine/stf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pub struct TransactionReceipt<R> {
/// Any additional structured data to be saved in the database and served over RPC
/// For example, this might contain a status code.
pub receipt: R,
/// Total gas incurred for this transaction.
pub gas_total: Vec<u64>,
}

/// A receipt for a batch of transactions. These receipts are stored in the rollup's database
Expand Down
Loading

0 comments on commit 685b2c1

Please sign in to comment.