diff --git a/CHANGELOG.md b/CHANGELOG.md index e414c45a49..170907536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). #### Breaking +- [#622](https://github.com/FuelLabs/fuel-vm/pull/622): Divide `DependentCost` into "light" and "heavy" operations: Light operations consume `0 < x < 1` gas per unit, while heavy operations consume `x` gas per unit. This distinction provides more precision when calculating dependent costs. - [#618](https://github.com/FuelLabs/fuel-vm/pull/618): Transaction fees for `Create` now include the cost of metadata calculations, including: contract root calculation, state root calculation, and contract id calculation. - [#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. diff --git a/fuel-tx/src/transaction/consensus_parameters/gas.rs b/fuel-tx/src/transaction/consensus_parameters/gas.rs index fa7de0d927..3bcbeb943e 100644 --- a/fuel-tx/src/transaction/consensus_parameters/gas.rs +++ b/fuel-tx/src/transaction/consensus_parameters/gas.rs @@ -324,18 +324,29 @@ pub struct GasCostsValues { } /// Dependent cost is a cost that depends on the number of units. -/// The cost starts at the base and grows by `dep_per_unit` for every unit. -/// -/// For example, if the base is 10 and the `dep_per_unit` is 2, -/// then the cost for 0 units is 10, 1 unit is 12, 2 units is 14, etc. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct DependentCost { - /// The minimum that this operation can cost. - pub base: Word, - /// The amount that this operation costs per - /// increase in unit. - pub dep_per_unit: Word, +pub enum DependentCost { + /// When an operation is dependent on the magnitude of its inputs, and the + /// time per unit of input is less than a single no-op operation + LightOperation { + /// The minimum that this operation can cost. + base: Word, + /// How many elements can be processed with a single gas. The + /// higher the `units_per_gas`, the less additional cost you will incur + /// for a given number of units, because you need more units to increase + /// the total cost. + units_per_gas: Word, + }, + + /// When an operation is dependent on the magnitude of its inputs, and the + /// time per unit of input is greater than a single no-op operation + HeavyOperation { + /// The minimum that this operation can cost. + base: Word, + /// How much gas is required to process a single unit. + gas_per_unit: Word, + }, } #[cfg(feature = "alloc")] @@ -584,22 +595,66 @@ impl GasCostsValues { impl DependentCost { /// Create costs that are all set to zero. pub fn free() -> Self { - Self { + Self::HeavyOperation { base: 0, - dep_per_unit: 0, + gas_per_unit: 0, } } /// Create costs that are all set to one. pub fn unit() -> Self { - Self { + Self::HeavyOperation { base: 1, - dep_per_unit: 0, + gas_per_unit: 0, } } + pub fn from_units_per_gas(base: Word, units_per_gas: Word) -> Self { + debug_assert!( + units_per_gas > 0, + "Cannot create dependent gas cost with per-0-gas ratio" + ); + DependentCost::LightOperation { + base, + units_per_gas, + } + } + + pub fn from_gas_per_unit(base: Word, gas_per_unit: Word) -> Self { + DependentCost::HeavyOperation { base, gas_per_unit } + } + + pub fn base(&self) -> Word { + match self { + DependentCost::LightOperation { base, .. } => *base, + DependentCost::HeavyOperation { base, .. } => *base, + } + } + + pub fn set_base(&mut self, value: Word) { + match self { + DependentCost::LightOperation { base, .. } => *base = value, + DependentCost::HeavyOperation { base, .. } => *base = value, + }; + } + pub fn resolve(&self, units: Word) -> Word { - self.base + units.saturating_div(self.dep_per_unit) + let base = self.base(); + let dependent_value = match self { + DependentCost::LightOperation { units_per_gas, .. } => { + // Apply the linear transformation f(x) = 1/m * x = x/m = where: + // x is the number of units + // 1/m is the gas_per_unit + units.saturating_div(*units_per_gas) + } + DependentCost::HeavyOperation { gas_per_unit, .. } => { + // Apply the linear transformation f(x) = mx, where: + // x is the number of units + // m is the gas per unit + units.saturating_mul(*gas_per_unit) + } + }; + base + dependent_value } } @@ -623,3 +678,48 @@ impl From for GasCostsValues { (*i.0).clone() } } + +#[cfg(test)] +mod tests { + use crate::DependentCost; + + #[test] + fn light_operation_gas_cost_resolves_correctly() { + // Create a linear gas cost function with a slope of 1/10 + let cost = DependentCost::from_units_per_gas(0, 10); + let total = cost.resolve(0); + assert_eq!(total, 0); + + let total = cost.resolve(5); + assert_eq!(total, 0); + + let total = cost.resolve(10); + assert_eq!(total, 1); + + let total = cost.resolve(100); + assert_eq!(total, 10); + + let total = cost.resolve(721); + assert_eq!(total, 72); + } + + #[test] + fn heavy_operation_gas_cost_resolves_correctly() { + // Create a linear gas cost function with a slope of 10 + let cost = DependentCost::from_gas_per_unit(0, 10); + let total = cost.resolve(0); + assert_eq!(total, 0); + + let total = cost.resolve(5); + assert_eq!(total, 50); + + let total = cost.resolve(10); + assert_eq!(total, 100); + + let total = cost.resolve(100); + assert_eq!(total, 1_000); + + let total = cost.resolve(721); + assert_eq!(total, 7_210); + } +} diff --git a/fuel-tx/src/transaction/consensus_parameters/gas/default_gas_costs.rs b/fuel-tx/src/transaction/consensus_parameters/gas/default_gas_costs.rs index ee50e6c1ff..75cf8d1a96 100644 --- a/fuel-tx/src/transaction/consensus_parameters/gas/default_gas_costs.rs +++ b/fuel-tx/src/transaction/consensus_parameters/gas/default_gas_costs.rs @@ -92,82 +92,82 @@ pub fn default_gas_costs() -> GasCostsValues { wqmm: 3, xor: 1, xori: 1, - k256: DependentCost { + k256: DependentCost::LightOperation { base: 11, - dep_per_unit: 214, + units_per_gas: 214, }, - s256: DependentCost { + s256: DependentCost::LightOperation { base: 2, - dep_per_unit: 214, + units_per_gas: 214, }, - call: DependentCost { + call: DependentCost::LightOperation { base: 144, - dep_per_unit: 214, + units_per_gas: 214, }, - ccp: DependentCost { + ccp: DependentCost::LightOperation { base: 15, - dep_per_unit: 103, + units_per_gas: 103, }, - csiz: DependentCost { + csiz: DependentCost::LightOperation { base: 17, - dep_per_unit: 790, + units_per_gas: 790, }, - ldc: DependentCost { + ldc: DependentCost::LightOperation { base: 15, - dep_per_unit: 272, + units_per_gas: 272, }, - logd: DependentCost { + logd: DependentCost::LightOperation { base: 26, - dep_per_unit: 64, + units_per_gas: 64, }, - mcl: DependentCost { + mcl: DependentCost::LightOperation { base: 1, - dep_per_unit: 3333, + units_per_gas: 3333, }, - mcli: DependentCost { + mcli: DependentCost::LightOperation { base: 1, - dep_per_unit: 3333, + units_per_gas: 3333, }, - mcp: DependentCost { + mcp: DependentCost::LightOperation { base: 1, - dep_per_unit: 2000, + units_per_gas: 2000, }, - mcpi: DependentCost { + mcpi: DependentCost::LightOperation { base: 3, - dep_per_unit: 2000, + units_per_gas: 2000, }, - meq: DependentCost { + meq: DependentCost::LightOperation { base: 1, - dep_per_unit: 2500, + units_per_gas: 2500, }, rvrt: 13, - smo: DependentCost { + smo: DependentCost::LightOperation { base: 209, - dep_per_unit: 55, + units_per_gas: 55, }, - retd: DependentCost { + retd: DependentCost::LightOperation { base: 29, - dep_per_unit: 62, + units_per_gas: 62, }, - srwq: DependentCost { + srwq: DependentCost::LightOperation { base: 47, - dep_per_unit: 5, + units_per_gas: 5, }, - scwq: DependentCost { + scwq: DependentCost::LightOperation { base: 13, - dep_per_unit: 5, + units_per_gas: 5, }, - swwq: DependentCost { + swwq: DependentCost::LightOperation { base: 44, - dep_per_unit: 5, + units_per_gas: 5, }, - contract_root: DependentCost { + contract_root: DependentCost::LightOperation { base: 75, - dep_per_unit: 1, + units_per_gas: 1, }, - state_root: DependentCost { + state_root: DependentCost::LightOperation { base: 412, - dep_per_unit: 1, + units_per_gas: 1, }, } } diff --git a/fuel-vm/src/interpreter/blockchain.rs b/fuel-vm/src/interpreter/blockchain.rs index 4179db7798..c0109897d1 100644 --- a/fuel-vm/src/interpreter/blockchain.rs +++ b/fuel-vm/src/interpreter/blockchain.rs @@ -106,8 +106,8 @@ where let mut gas_cost = self.gas_costs().ldc; // Charge only for the `base` execution. // We will charge for the contracts size in the `load_contract_code`. - self.gas_charge(gas_cost.base)?; - gas_cost.base = 0; + self.gas_charge(gas_cost.base())?; + gas_cost.set_base(0); let contract_max_size = self.contract_max_size(); let current_contract = current_contract(&self.context, self.registers.fp(), self.memory.as_ref())? @@ -262,8 +262,8 @@ where let mut gas_cost = self.gas_costs().csiz; // Charge only for the `base` execution. // We will charge for the contracts size in the `code_size`. - self.gas_charge(gas_cost.base)?; - gas_cost.base = 0; + self.gas_charge(gas_cost.base())?; + gas_cost.set_base(0); let current_contract = current_contract(&self.context, self.registers.fp(), self.memory.as_ref())? .copied(); diff --git a/fuel-vm/src/interpreter/blockchain/code_tests.rs b/fuel-vm/src/interpreter/blockchain/code_tests.rs index 7145706531..591bb0c9b2 100644 --- a/fuel-vm/src/interpreter/blockchain/code_tests.rs +++ b/fuel-vm/src/interpreter/blockchain/code_tests.rs @@ -52,10 +52,7 @@ fn test_load_contract() -> IoResult<(), Infallible> { profiler: &mut Profiler::default(), input_contracts: InputContracts::new(input_contracts.iter(), &mut panic_context), current_contract: None, - gas_cost: DependentCost { - base: 13, - dep_per_unit: 1, - }, + gas_cost: DependentCost::from_units_per_gas(13, 1), cgas: RegMut::new(&mut cgas), ggas: RegMut::new(&mut ggas), ssp: RegMut::new(&mut ssp), diff --git a/fuel-vm/src/interpreter/blockchain/other_tests.rs b/fuel-vm/src/interpreter/blockchain/other_tests.rs index 9b170d01a5..34d605ca3e 100644 --- a/fuel-vm/src/interpreter/blockchain/other_tests.rs +++ b/fuel-vm/src/interpreter/blockchain/other_tests.rs @@ -322,10 +322,7 @@ fn test_code_size() { let input = CodeSizeCtx { storage: &mut storage, memory: &mut memory, - gas_cost: DependentCost { - base: 0, - dep_per_unit: 0, - }, + gas_cost: DependentCost::free(), profiler: &mut Profiler::default(), input_contracts: InputContracts::new(input_contract.iter(), &mut panic_context), current_contract: None, @@ -343,10 +340,7 @@ fn test_code_size() { let input = CodeSizeCtx { storage: &mut storage, memory: &mut memory, - gas_cost: DependentCost { - base: 0, - dep_per_unit: 0, - }, + gas_cost: DependentCost::free(), input_contracts: InputContracts::new(input_contract.iter(), &mut panic_context), profiler: &mut Profiler::default(), current_contract: None, @@ -363,10 +357,7 @@ fn test_code_size() { let input = CodeSizeCtx { storage: &mut storage, memory: &mut memory, - gas_cost: DependentCost { - base: 0, - dep_per_unit: 0, - }, + gas_cost: DependentCost::free(), input_contracts: InputContracts::new(iter::empty(), &mut panic_context), profiler: &mut Profiler::default(), current_contract: None, diff --git a/fuel-vm/src/interpreter/flow.rs b/fuel-vm/src/interpreter/flow.rs index 03509b3b73..c2991deba6 100644 --- a/fuel-vm/src/interpreter/flow.rs +++ b/fuel-vm/src/interpreter/flow.rs @@ -390,8 +390,8 @@ where let mut gas_cost = self.gas_costs().call; // Charge only for the `base` execution. // We will charge for the frame size in the `prepare_call`. - self.gas_charge(gas_cost.base)?; - gas_cost.base = 0; + self.gas_charge(gas_cost.base())?; + gas_cost.set_base(0); let current_contract = current_contract(&self.context, self.registers.fp(), self.memory.as_ref())? .copied(); diff --git a/fuel-vm/src/interpreter/flow/tests.rs b/fuel-vm/src/interpreter/flow/tests.rs index 208b80610c..2a0364ebff 100644 --- a/fuel-vm/src/interpreter/flow/tests.rs +++ b/fuel-vm/src/interpreter/flow/tests.rs @@ -52,10 +52,7 @@ impl Default for Input { input_contracts: vec![Default::default()], storage_balance: Default::default(), memory: vec![0u8; MEM_SIZE].try_into().unwrap(), - gas_cost: DependentCost { - base: 10, - dep_per_unit: 10, - }, + gas_cost: DependentCost::from_units_per_gas(10, 10), storage_contract: vec![(ContractId::default(), vec![0u8; 10])], script: None, } diff --git a/fuel-vm/src/interpreter/gas.rs b/fuel-vm/src/interpreter/gas.rs index c68bdf36a8..fe005a7d79 100644 --- a/fuel-vm/src/interpreter/gas.rs +++ b/fuel-vm/src/interpreter/gas.rs @@ -81,13 +81,9 @@ pub(crate) fn dependent_gas_charge( gas_cost: DependentCost, arg: Word, ) -> SimpleResult<()> { - if gas_cost.dep_per_unit == 0 { - gas_charge(cgas, ggas, profiler, gas_cost.base) - } else { - let cost = dependent_gas_charge_inner(cgas.as_mut(), ggas, gas_cost, arg)?; - profiler.profile(cgas.as_ref(), cost); - Ok(()) - } + let cost = dependent_gas_charge_inner(cgas.as_mut(), ggas, gas_cost, arg)?; + profiler.profile(cgas.as_ref(), cost); + Ok(()) } fn dependent_gas_charge_inner( @@ -96,9 +92,7 @@ fn dependent_gas_charge_inner( gas_cost: DependentCost, arg: Word, ) -> Result { - let cost = gas_cost - .base - .saturating_add(arg.saturating_div(gas_cost.dep_per_unit)); + let cost = gas_cost.resolve(arg); gas_charge_inner(cgas, ggas, cost).map(|_| cost) } diff --git a/fuel-vm/src/interpreter/gas/tests.rs b/fuel-vm/src/interpreter/gas/tests.rs index 103546d351..091a06ebcb 100644 --- a/fuel-vm/src/interpreter/gas/tests.rs +++ b/fuel-vm/src/interpreter/gas/tests.rs @@ -55,43 +55,43 @@ struct DepGasChargeInput { #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 0, ggas: 0, dependent_factor: 0}, - gas_cost: DependentCost{base: 0, dep_per_unit: 1} + gas_cost: DependentCost::from_units_per_gas(0, 1) } => Ok(GasChargeOutput{ cgas: 0, ggas: 0}); "zero" )] #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 1, ggas: 1, dependent_factor: 0}, - gas_cost: DependentCost{base: 1, dep_per_unit: 1} + gas_cost: DependentCost::from_units_per_gas(1, 1) } => Ok(GasChargeOutput{ cgas: 0, ggas: 0}); "just base" )] #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 1, ggas: 1, dependent_factor: 1}, - gas_cost: DependentCost{base: 1, dep_per_unit: 2} + gas_cost: DependentCost::from_units_per_gas(1, 2) } => Ok(GasChargeOutput{ cgas: 0, ggas: 0}); "just base with gas" )] #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 3, ggas: 3, dependent_factor: 8}, - gas_cost: DependentCost{base: 1, dep_per_unit: 4} + gas_cost: DependentCost::from_units_per_gas(1, 4) } => Ok(GasChargeOutput{ cgas: 0, ggas: 0}); "base with gas and a unit" )] #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 3, ggas: 3, dependent_factor: 5}, - gas_cost: DependentCost{base: 0, dep_per_unit: 4} + gas_cost: DependentCost::from_units_per_gas(0, 4) } => Ok(GasChargeOutput{ cgas: 2, ggas: 2}); "base with gas and a unit and left over" )] #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 0, ggas: 1, dependent_factor: 0}, - gas_cost: DependentCost{base: 1, dep_per_unit: 1} + gas_cost: DependentCost::from_units_per_gas(1, 1) } => Err(PanicOrBug::Panic(PanicReason::OutOfGas)); "just base with no cgas" )] #[test_case( DepGasChargeInput{ input: GasChargeInput{cgas: 5, ggas: 10, dependent_factor: 25}, - gas_cost: DependentCost{base: 1, dep_per_unit: 5} + gas_cost: DependentCost::from_units_per_gas(1, 5) } => Err(PanicOrBug::Panic(PanicReason::OutOfGas)); "unit with not enough cgas" )] fn test_dependent_gas_charge(input: DepGasChargeInput) -> SimpleResult { diff --git a/fuel-vm/src/tests/blockchain.rs b/fuel-vm/src/tests/blockchain.rs index 35703017d9..e0d67bcc3b 100644 --- a/fuel-vm/src/tests/blockchain.rs +++ b/fuel-vm/src/tests/blockchain.rs @@ -323,7 +323,10 @@ fn ldc__gas_cost_is_not_dependent_on_rC() { let gas_costs = client.gas_costs(); let ldc_cost = gas_costs.ldc; - let ldc_dep_len = ldc_cost.dep_per_unit; + let ldc_dep_len = match ldc_cost { + DependentCost::LightOperation { units_per_gas, .. } => units_per_gas, + DependentCost::HeavyOperation { gas_per_unit, .. } => gas_per_unit, + }; let noop_cost = gas_costs.noop; let contract_size = 1000; @@ -388,8 +391,10 @@ fn ldc__cost_is_proportional_to_total_contracts_size_not_rC() { let gas_costs = client.gas_costs(); let ldc_cost = gas_costs.ldc; - let ldc_dep_len = ldc_cost.dep_per_unit; - + let ldc_dep_len = match ldc_cost { + DependentCost::LightOperation { units_per_gas, .. } => units_per_gas, + DependentCost::HeavyOperation { gas_per_unit, .. } => gas_per_unit, + }; let contract_size = 0; let offset = 0; let len = 0;