Skip to content
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

Merged
merged 32 commits into from
Oct 30, 2023

Conversation

bvrooman
Copy link
Contributor

@bvrooman bvrooman commented Oct 22, 2023

Related issues:

This 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.

@bvrooman bvrooman marked this pull request as ready for review October 24, 2023 19:10
@bvrooman bvrooman self-assigned this Oct 24, 2023
@bvrooman bvrooman requested a review from a team October 24, 2023 22:16
Comment on lines 738 to 755
/// 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()
}
Copy link
Collaborator

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

Copy link
Contributor Author

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.

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;
Copy link
Collaborator

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.

Copy link
Member

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.

Copy link
Contributor Author

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,
Copy link
Collaborator

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).

Copy link
Contributor Author

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.

@bvrooman
Copy link
Contributor Author

bvrooman commented Oct 25, 2023

I will put this PR back into draft as I apply the following changes:

  • refactor calculation to use a single loop over the inputs, checking for both signed inputs and predicates at this time, rather than separately
  • refactor tests to avoid duplication of fee calculation (allowing us to centralize the fee calculation to only checked_from_tx)
  • adding dummy value for predicate costs
  • (maybe) refactoring FeeParameters

@bvrooman bvrooman marked this pull request as draft October 25, 2023 00:21
let fee = gas * gas_price;
fee as f64 / PARAMS.gas_price_factor as f64
}

#[test]
fn base_fee_is_calculated_correctly() {
Copy link
Member

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?

Copy link
Contributor Author

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?

@bvrooman bvrooman changed the title feat: Charge for input signature recovery feat: Charge for input signature verification (address recovery and predicate roots) Oct 27, 2023
@bvrooman bvrooman marked this pull request as ready for review October 27, 2023 05:30
fuel-tx/src/transaction/types/create.rs Outdated Show resolved Hide resolved
Comment on lines 1376 to 1380
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;
Copy link
Collaborator

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=)

Copy link
Contributor Author

@bvrooman bvrooman Oct 27, 2023

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,
Copy link
Collaborator

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?

Comment on lines +96 to +99
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)?;
Copy link
Contributor Author

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.

Comment on lines -312 to -329
#[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);
}
Copy link
Contributor Author

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formula is base + units/dep_pet_unit.

Suggested change
self.base + self.dep_per_unit.saturating_mul(units)
self.base + units.saturating_div(self.dep_per_unit)
image

Copy link
Contributor Author

@bvrooman bvrooman Oct 30, 2023

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?

Copy link
Collaborator

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

Comment on lines +164 to +167
contract_root: DependentCost {
base: 75,
dep_per_unit: 1,
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got other results from benchmarks=) But it's okay. We can finalize them later before the final gas prices update.

image

@bvrooman bvrooman added this pull request to the merge queue Oct 30, 2023
Merged via the queue into master with commit 8880e58 Oct 30, 2023
37 checks passed
@bvrooman bvrooman deleted the bvrooman/feat/charge-for-input-signature-recovery branch October 30, 2023 18:32
@xgreenx xgreenx mentioned this pull request Oct 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants