-
Notifications
You must be signed in to change notification settings - Fork 93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Charge for input signature verification (address recovery and predicate roots) #613
Changes from 22 commits
9834392
c2b6b01
8f8f119
78bac01
f3dd93d
1978ef9
880f217
da91cb4
da4f93b
9a9661e
942cc6e
670dba3
d9acf65
dbc7425
c0625c7
dfbea1d
87f0893
ca9753f
bc3e060
2e1b06a
5c2ec9c
b83516c
c0a1180
125d598
43ae1f1
496278d
035cf28
1448a3a
b80af0f
759aa57
0bd7c3e
cdb0bee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,5 +161,6 @@ pub fn default_gas_costs() -> GasCostsValues { | |
base: 44, | ||
dep_per_unit: 5, | ||
}, | ||
contract_root: 3024932, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
use crate::FeeParameters; | ||
use crate::{ | ||
field, | ||
input::{ | ||
coin::CoinSigned, | ||
message::{ | ||
MessageCoinSigned, | ||
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))] | ||
|
@@ -66,6 +79,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, | ||
|
@@ -74,7 +88,9 @@ 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 min_gas = bytes_gas | ||
.checked_add(gas_used_by_signature_checks)? | ||
.checked_add(gas_used_by_predicates)?; | ||
let max_gas = bytes_gas.checked_add(gas_limit)?; | ||
|
||
let max_gas_to_pay = max_gas.checked_mul(gas_price).and_then(|total| { | ||
|
@@ -113,18 +129,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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding predicates, you can already add a new cost into Maybe you want to extend There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add new fields for both the cost of signed input address recovery and the cost of predicate ownership check. The advantage of this is that we don't have to pass On the other hand, it is not clear to me why that should be the responsibility of |
||
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, | ||
|
@@ -144,7 +166,58 @@ 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(_) | ||
| Input::MessageCoinPredicate(_) | ||
| Input::MessageDataPredicate(_) => gas_costs.contract_root, | ||
// Charge nothing for all other inputs | ||
_ => 0, | ||
}) | ||
.fold(0, |acc, cost| acc.saturating_add(cost)) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
|
@@ -159,68 +232,87 @@ mod tests { | |
.with_gas_per_byte(2) | ||
.with_gas_price_factor(3); | ||
|
||
fn gas_to_fee(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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth considering adding multi-sig tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added additional tests for multiple signed inputs. Is that what you mean by multi-sig tests? |
||
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 fee = TransactionFee::checked_from_values( | ||
&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; | ||
let expected_max_gas = PARAMS.gas_per_byte * metered_bytes + gas_limit; | ||
let expected_max_fee = gas_to_fee(expected_max_gas, gas_price).ceil() as Word; | ||
let expected_min_gas = PARAMS.gas_per_byte * metered_bytes | ||
+ gas_used_by_signature_checks | ||
+ gas_limit; | ||
let expected_min_fee = gas_to_fee(expected_min_gas, gas_price).ceil() as Word; | ||
|
||
assert_eq!(expected, fee.max_fee); | ||
assert_eq!(expected, fee.min_fee); | ||
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 fee = TransactionFee::checked_from_values( | ||
&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_limit; | ||
let expected_max_fee = gas_to_fee(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, expected_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_limit; | ||
let expected_min_fee = gas_to_fee(expected_min_gas, gas_price); | ||
let truncated = expected_max_fee as Word; | ||
let expected_min_fee = expected_min_fee.ceil() as Word; | ||
assert_ne!(truncated, expected_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, | ||
|
@@ -236,13 +328,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, | ||
|
@@ -255,13 +349,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, | ||
|
@@ -274,13 +370,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, | ||
|
@@ -293,13 +391,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, | ||
|
@@ -312,13 +412,15 @@ mod tests { | |
#[test] | ||
fn base_fee_gas_limit_less_than_gas_used_by_predicates() { | ||
let metered_bytes = 5; | ||
let gas_used_by_signature_checks = 12; | ||
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_signature_checks, | ||
gas_used_by_predicates, | ||
gas_limit, | ||
gas_price, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it is a very huge value. Maybe it is better if we use
DependentCost
instead. Could you check how expensive it is in this case, please?