-
Notifications
You must be signed in to change notification settings - Fork 87
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
feat: Charge for input signature verification (address recovery and predicate roots) #613
Conversation
fuel-tx/src/transaction.rs
Outdated
/// Returns all signed inputs. This excludes predicate inputs. | ||
fn signed_inputs(&self) -> Vec<&Input> { | ||
self.inputs() | ||
.iter() | ||
.filter_map(|input| input.witness_index().is_some().then_some(input)) | ||
.collect_vec() | ||
} | ||
|
||
/// Returns all signed inputs filtered by witness uniqueness. | ||
fn signed_inputs_with_unique_witnesses(&self) -> Vec<&Input> { | ||
self.signed_inputs() | ||
.into_iter() | ||
.sorted_unstable_by(|a, b| { | ||
Ord::cmp(&a.witness_index(), &b.witness_index()) | ||
}) | ||
.dedup() | ||
.collect_vec() | ||
} |
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.
It is better to not add one-time functions into the trait. We only need them in one place, plus we need to iterate over all inputs because you need to charge for Signed
and for Predicate
inputs
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.
I understand your suggestion. Instead specializing the trait here, I can just iterate over the inputs at the time of calculating the fee, as you suggest in another comment.
Currently, this method is also used in tests to verify the fee for a transaction is correct. But maybe I can refactor the tests so that we can test the fees without duplicating the calculation logic, and we can keep the calculation isolated to the one place.
fuel-tx/src/transaction/fee.rs
Outdated
params: &FeeParameters, | ||
tx: &T, | ||
) -> Option<Self> { | ||
let metered_bytes = tx.metered_bytes_size() as Word; | ||
let unique_witnesses = tx.signed_inputs_with_unique_witnesses().len() as Word; |
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.
Because you need to find Signed
and Predicate
inputs, it is better to iterate over them here and calculate the number of unique witnesses, plus charge for predicates.
You can use one loop to cover all inputs. Plus, you can already implement charging for the predicates too, but with some dummy values that you will update later.
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.
I will point out that the calculation for predicate gas is done by the tx
:
tx.gas_used_by_predicates()
whereas the signature cost is done here.
Would it be wrong to have a
tx.gas_used_for_sign_verification()
method on the tx
? And if yes, would it make sense to change the predicate calculation?
I don't really have an opinion yet, but seems strange that they are so different.
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.
I think it may be beneficial to move this to the Chargeable
trait, and allow the method to take in a reference to GasCosts
. We calculate the gas cost when checking the transaction, but also in a number of tests to verify the fee. Ideally, we want to test the calculation used in production, rather than testing a recreation of the calculation that may become out of sync. This means we need to wrap it in a method that we can call on the tx.
@@ -113,18 +120,23 @@ 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: Chargeable + Inputs>( | |||
gas_costs: &GasCosts, |
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.
Regarding predicates, you can already add a new cost into GasCosts
and add a new structure that we can use later to charge for the predicates.
Maybe you want to extend FeeParameters
, for example, the price of the ecr1
can be duplicated there. The constructor can accept gas_costs
, like FeeParameters::new(gas_costs)
.
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.
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 GasCosts
around, which is a little bit simpler.
On the other hand, it is not clear to me why that should be the responsibility of FeeParameters
when GasCosts
already contains these values. FeeParameters
is responsible for providing information about how to convert gas to fees, rather than storing any concrete gas costs themselves.
I will put this PR back into draft as I apply the following changes:
|
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 comment
The 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 comment
The 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?
fuel-vm/src/checked_transaction.rs
Outdated
let fee = tx.metered_bytes_size() as u128 | ||
* fee_params.gas_per_byte as u128 | ||
* tx.price() as u128; | ||
let gas = tx.limit() as u128 * tx.price() as u128; | ||
let total = bytes + gas; | ||
let total = fee + gas; |
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.
max_fee
it is min_fee
+ (GasLimit
converted into the price). So you need to update it in the same way as for is_valid_min_fee
=)
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.
If I understand correctly:
max_fee = min_fee + gas_limit * gas_price
That would mean that max_fee
is always greater than or equal to min_fee
.
After making that change, some tests fail, such as:
#[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,
)
.expect("failed to calculate fee");
assert!(fee.min_fee > fee.max_fee);
}
where we test that the min_fee
is greater than max_fee
.
Is there a specification for min/max fee?
@@ -161,5 +161,6 @@ pub fn default_gas_costs() -> GasCostsValues { | |||
base: 44, | |||
dep_per_unit: 5, | |||
}, | |||
contract_root: 3024932, |
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?
…ttps://github.com/FuelLabs/fuel-vm into bvrooman/feat/charge-for-input-signature-recovery
…ttps://github.com/FuelLabs/fuel-vm into bvrooman/feat/charge-for-input-signature-recovery
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)?; |
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.
Fee is effectively calculated using the formula defined in the spec. Min fee and max fee are separated by the gas limit.
In effect, max_fee = min_fee + gas_limit * gas_price
.
#[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); | ||
} |
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.
I am removing this test because it is now no longer possible for min_fee
to be greater than max_fee
.
pub fn resolve(&self, units: Word) -> Word { | ||
// Apply the linear transformation from units to cost: | ||
// f(x) = base + mx | ||
self.base + self.dep_per_unit.saturating_mul(units) |
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.
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.
How do we handle the case where dep_per_unit
is 0, as with DependentCost::unit()
? Is that only for testing?
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.
In production, we don't have cases like this=) But in the tests it overflows
contract_root: DependentCost { | ||
base: 75, | ||
dep_per_unit: 1, | ||
}, |
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.
Related issues:
min_fee
#599This PR adds support for charging users for recovering addresses from signatures of signed inputs. For each signed input (non-predicate in effect), we charge the cost of an EC recovery. For each predicate input, we calculate the gas cost based on the length of the bytecode. The calculation is based on the gas cost of calculating the Merkle root of contract bytecode, which is dependent on contract length.
This PR uses the gas price generated by the benchmark in this PR. This benchmark calculates a gas cost by timing contract root calculation using the maximum size for contracts at 17mb.