Skip to content

Commit

Permalink
feat: Charge for input signature verification (address recovery and p…
Browse files Browse the repository at this point in the history
…redicate roots) (#613)

* Charge for input signature recovery (WIP)

* Update tests

* Update CHANGELOG.md

* Propagate gas_costs

* Update fee.rs

* Unique witnesses

* clippy

* Update unique witnesses and tests

* Add signed_inputs_with_inque_witnesses

* Add multiple message input tests

* Clippy

* Calculate gas_used_by_signature_checks on Chargeable

* Increase test coverage

* Typo

* Update tests

* Revert inputs changes

* Clippy knows best

* More clippy

* Update contract_root gas cost based on benchmarks

* Remove duplicated code

* Update max fee calculation

* Update contract_root to dependent cost

* Update tests

* Update gas.rs

* Update checked_transaction.rs

* Update dependent gas resolve

* Update tests

---------

Co-authored-by: xgreenx <[email protected]>
  • Loading branch information
Brandon Vrooman and xgreenx authored Oct 30, 2023
1 parent 5015d7d commit 8880e58
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 90 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

#### Breaking

- [#613](https://github.com/FuelLabs/fuel-vm/pull/613): Transaction fees now include the cost of signature verification for each input. For signed inputs, the cost of an EC recovery is charged. For predicate inputs, the cost of a BMT root of bytecode is charged.
- [#607](https://github.com/FuelLabs/fuel-vm/pull/607): The `Interpreter` expects the third generic argument during type definition that specifies the implementer of the `EcalHandler` trait for `ecal` opcode.
- [#609](https://github.com/FuelLabs/fuel-vm/pull/609): Checked transactions (`Create`, `Script`, and `Mint`) now enforce a maximum size. The maximum size is specified by `MAX_TRANSACTION_SIZE` in the transaction parameters, under consensus parameters. Checking a transaction above this size raises `CheckError::TransactionSizeLimitExceeded`.
- [#617](https://github.com/FuelLabs/fuel-vm/pull/617): Makes memory outside `$is..$ssp` range not executable. Separates `ErrorFlag` into `InvalidFlags`, `MemoryNotExecutable` and `InvalidInstruction`. Fixes related tests.
Expand Down
9 changes: 9 additions & 0 deletions fuel-tx/src/transaction/consensus_parameters/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ pub struct GasCostsValues {
pub smo: DependentCost,
pub srwq: DependentCost,
pub swwq: DependentCost,

// Non-opcode dependent costs
pub contract_root: DependentCost,
}

/// Dependent cost is a cost that depends on the number of units.
Expand Down Expand Up @@ -457,6 +460,7 @@ impl GasCostsValues {
smo: DependentCost::free(),
srwq: DependentCost::free(),
swwq: DependentCost::free(),
contract_root: DependentCost::free(),
}
}

Expand Down Expand Up @@ -569,6 +573,7 @@ impl GasCostsValues {
smo: DependentCost::unit(),
srwq: DependentCost::unit(),
swwq: DependentCost::unit(),
contract_root: DependentCost::unit(),
}
}
}
Expand All @@ -589,6 +594,10 @@ impl DependentCost {
dep_per_unit: 0,
}
}

pub fn resolve(&self, units: Word) -> Word {
self.base + units.saturating_div(self.dep_per_unit)
}
}

#[cfg(feature = "alloc")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,9 @@ pub fn default_gas_costs() -> GasCostsValues {
base: 44,
dep_per_unit: 5,
},
contract_root: DependentCost {
base: 75,
dep_per_unit: 1,
},
}
}
187 changes: 143 additions & 44 deletions fuel-tx/src/transaction/fee.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
use crate::FeeParameters;
use crate::{
field,
input::{
coin::{
CoinPredicate,
CoinSigned,
},
message::{
MessageCoinPredicate,
MessageCoinSigned,
MessageDataPredicate,
MessageDataSigned,
},
},
FeeParameters,
GasCosts,
Input,
};
use fuel_asm::Word;
use hashbrown::HashSet;

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down Expand Up @@ -66,6 +84,7 @@ impl TransactionFee {
pub fn checked_from_values(
params: &FeeParameters,
metered_bytes: Word,
gas_used_by_signature_checks: Word,
gas_used_by_predicates: Word,
gas_limit: Word,
gas_price: Word,
Expand All @@ -74,8 +93,10 @@ impl TransactionFee {

// TODO: use native div_ceil once stabilized out from nightly
let bytes_gas = params.gas_per_byte.checked_mul(metered_bytes)?;
let min_gas = bytes_gas.checked_add(gas_used_by_predicates)?;
let max_gas = bytes_gas.checked_add(gas_limit)?;
let min_gas = bytes_gas
.checked_add(gas_used_by_signature_checks)?
.checked_add(gas_used_by_predicates)?;
let max_gas = min_gas.checked_add(gas_limit)?;

let max_gas_to_pay = max_gas.checked_mul(gas_price).and_then(|total| {
num_integer::div_ceil(total as u128, factor).try_into().ok()
Expand Down Expand Up @@ -113,18 +134,24 @@ impl TransactionFee {
/// Attempt to create a transaction fee from parameters and transaction internals
///
/// Will return `None` if arithmetic overflow occurs.
pub fn checked_from_tx<T: Chargeable>(
pub fn checked_from_tx<T>(
gas_costs: &GasCosts,
params: &FeeParameters,
tx: &T,
) -> Option<Self> {
) -> Option<Self>
where
T: Chargeable + field::Inputs,
{
let metered_bytes = tx.metered_bytes_size() as Word;
let gas_used_by_signature_checks = tx.gas_used_by_signature_checks(gas_costs);
let gas_used_by_predicates = tx.gas_used_by_predicates();
let gas_limit = tx.limit();
let gas_price = tx.price();

Self::checked_from_values(
params,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
Expand All @@ -144,7 +171,62 @@ pub trait Chargeable {
fn metered_bytes_size(&self) -> usize;

/// Used for accounting purposes when charging for predicates.
fn gas_used_by_predicates(&self) -> Word;
fn gas_used_by_predicates(&self) -> Word
where
Self: field::Inputs,
{
let mut cumulative_predicate_gas: Word = 0;
for input in self.inputs() {
if let Some(predicate_gas_used) = input.predicate_gas_used() {
cumulative_predicate_gas =
cumulative_predicate_gas.saturating_add(predicate_gas_used);
}
}
cumulative_predicate_gas
}

fn gas_used_by_signature_checks(&self, gas_costs: &GasCosts) -> Word
where
Self: field::Inputs,
{
let mut witness_cache: HashSet<u8> = HashSet::new();
self.inputs()
.iter()
.filter(|input| match input {
// Include signed inputs of unique witness indices
Input::CoinSigned(CoinSigned { witness_index, .. })
| Input::MessageCoinSigned(MessageCoinSigned { witness_index, .. })
| Input::MessageDataSigned(MessageDataSigned { witness_index, .. })
if !witness_cache.contains(witness_index) =>
{
witness_cache.insert(*witness_index);
true
}
// Include all predicates
Input::CoinPredicate(_)
| Input::MessageCoinPredicate(_)
| Input::MessageDataPredicate(_) => true,
// Ignore all other inputs
_ => false,
})
.map(|input| match input {
// Charge EC recovery cost for signed inputs
Input::CoinSigned(_)
| Input::MessageCoinSigned(_)
| Input::MessageDataSigned(_) => gas_costs.ecr1,
// Charge the cost of the contract root for predicate inputs
Input::CoinPredicate(CoinPredicate { predicate, .. })
| Input::MessageCoinPredicate(MessageCoinPredicate {
predicate, ..
})
| Input::MessageDataPredicate(MessageDataPredicate {
predicate, ..
}) => gas_costs.contract_root.resolve(predicate.len() as u64),
// Charge nothing for all other inputs
_ => 0,
})
.fold(0, |acc, cost| acc.saturating_add(cost))
}
}

#[cfg(test)]
Expand All @@ -159,68 +241,96 @@ mod tests {
.with_gas_per_byte(2)
.with_gas_price_factor(3);

fn gas_to_fee(params: &FeeParameters, gas: u64, gas_price: Word) -> f64 {
let fee = gas * gas_price;
fee as f64 / params.gas_price_factor as f64
}

#[test]
fn base_fee_is_calculated_correctly() {
let metered_bytes = 5;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = 7;
let gas_limit = 7;
let gas_price = 11;

let params = PARAMS;
let fee = TransactionFee::checked_from_values(
&PARAMS,
&params,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
)
.expect("failed to calculate fee");

let expected = PARAMS.gas_per_byte * metered_bytes + gas_limit;
let expected = expected * gas_price;
let expected = expected as f64 / PARAMS.gas_price_factor as f64;
let expected = expected.ceil() as Word;

assert_eq!(expected, fee.max_fee);
assert_eq!(expected, fee.min_fee);
let expected_max_gas = params.gas_per_byte * metered_bytes
+ gas_used_by_signature_checks
+ gas_used_by_predicates
+ gas_limit;
let expected_max_fee =
gas_to_fee(&params, expected_max_gas, gas_price).ceil() as Word;
let expected_min_gas = params.gas_per_byte * metered_bytes
+ gas_used_by_signature_checks
+ gas_used_by_predicates;
let expected_min_fee =
gas_to_fee(&params, expected_min_gas, gas_price).ceil() as Word;

assert_eq!(expected_max_fee, fee.max_fee);
assert_eq!(expected_min_fee, fee.min_fee);
}

#[test]
fn base_fee_ceils() {
let metered_bytes = 5;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = 7;
let gas_limit = 7;
let gas_price = 11;

let params = PARAMS.with_gas_price_factor(10);
let fee = TransactionFee::checked_from_values(
&PARAMS,
&params,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
)
.expect("failed to calculate fee");

let expected = PARAMS.gas_per_byte * metered_bytes + gas_limit;
let expected = expected * gas_price;
let expected = expected as f64 / PARAMS.gas_price_factor as f64;
let truncated = expected as Word;
let expected = expected.ceil() as Word;

assert_ne!(truncated, expected);
assert_eq!(expected, fee.max_fee);
assert_eq!(expected, fee.min_fee);
let expected_max_gas = params.gas_per_byte * metered_bytes
+ gas_used_by_signature_checks
+ gas_used_by_predicates
+ gas_limit;
let expected_max_fee = gas_to_fee(&params, expected_max_gas, gas_price);
let truncated = expected_max_fee as Word;
let expected_max_fee = expected_max_fee.ceil() as Word;
assert_ne!(truncated, fee.max_fee);
assert_eq!(expected_max_fee, fee.max_fee);

let expected_min_gas = params.gas_per_byte * metered_bytes
+ gas_used_by_signature_checks
+ gas_used_by_predicates;
let expected_min_fee = gas_to_fee(&params, expected_min_gas, gas_price);
let truncated = expected_min_fee as Word;
let expected_min_fee = expected_min_fee.ceil() as Word;
assert_ne!(truncated, fee.min_fee);
assert_eq!(expected_min_fee, fee.min_fee);
}

#[test]
fn base_fee_zeroes() {
let metered_bytes = 5;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = 7;
let gas_limit = 7;
let gas_price = 0;

let fee = TransactionFee::checked_from_values(
&PARAMS,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
Expand All @@ -236,13 +346,15 @@ mod tests {
#[test]
fn base_fee_wont_overflow_on_bytes() {
let metered_bytes = Word::MAX;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = 7;
let gas_limit = 7;
let gas_price = 11;

let overflow = TransactionFee::checked_from_values(
&PARAMS,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
Expand All @@ -255,13 +367,15 @@ mod tests {
#[test]
fn base_fee_wont_overflow_on_gas_used_by_predicates() {
let metered_bytes = 5;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = Word::MAX;
let gas_limit = 7;
let gas_price = 11;

let overflow = TransactionFee::checked_from_values(
&PARAMS,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
Expand All @@ -274,13 +388,15 @@ mod tests {
#[test]
fn base_fee_wont_overflow_on_limit() {
let metered_bytes = 5;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = 7;
let gas_limit = Word::MAX;
let gas_price = 11;

let overflow = TransactionFee::checked_from_values(
&PARAMS,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
Expand All @@ -293,13 +409,15 @@ mod tests {
#[test]
fn base_fee_wont_overflow_on_price() {
let metered_bytes = 5;
let gas_used_by_signature_checks = 12;
let gas_used_by_predicates = 7;
let gas_limit = 7;
let gas_price = Word::MAX;

let overflow = TransactionFee::checked_from_values(
&PARAMS,
metered_bytes,
gas_used_by_signature_checks,
gas_used_by_predicates,
gas_limit,
gas_price,
Expand All @@ -308,23 +426,4 @@ mod tests {

assert!(overflow);
}

#[test]
fn base_fee_gas_limit_less_than_gas_used_by_predicates() {
let metered_bytes = 5;
let gas_used_by_predicates = 8;
let gas_limit = 7;
let gas_price = 11;

let fee = TransactionFee::checked_from_values(
&PARAMS,
metered_bytes,
gas_used_by_predicates,
gas_limit,
gas_price,
)
.expect("failed to calculate fee");

assert!(fee.min_fee > fee.max_fee);
}
}
Loading

0 comments on commit 8880e58

Please sign in to comment.