diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d5d2013a..d3656671c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- [#771](https://github.com/FuelLabs/fuel-vm/pull/771): Take into account spent gas during synchronous predicates estimation. + #### Breaking - [#765](https://github.com/FuelLabs/fuel-vm/pull/765): corrected the gas units for WDOP and WQOP diff --git a/fuel-vm/Cargo.toml b/fuel-vm/Cargo.toml index 30c852928b..09ae31173d 100644 --- a/fuel-vm/Cargo.toml +++ b/fuel-vm/Cargo.toml @@ -61,6 +61,7 @@ fuel-vm = { path = ".", default-features = false, features = [ "random", ] } futures = "0.3.28" +ntest = "0.9.2" num-integer = "0.1.45" p256 = "0.13" quickcheck = "1.0" diff --git a/fuel-vm/src/interpreter/executors/main.rs b/fuel-vm/src/interpreter/executors/main.rs index 1b3e3e1528..8ae97f8367 100644 --- a/fuel-vm/src/interpreter/executors/main.rs +++ b/fuel-vm/src/interpreter/executors/main.rs @@ -131,16 +131,7 @@ impl<'a, Tx> PredicateRunKind<'a, Tx> { #[derive(Debug, Clone, Copy)] enum PredicateAction { Verifying, - Estimating, -} - -impl From<&PredicateRunKind<'_, Tx>> for PredicateAction { - fn from(kind: &PredicateRunKind<'_, Tx>) -> Self { - match kind { - PredicateRunKind::Verifying(_) => PredicateAction::Verifying, - PredicateRunKind::Estimating(_) => PredicateAction::Estimating, - } - } + Estimating { available_gas: Word }, } impl Interpreter<&mut MemoryInstance, PredicateStorage, Tx> @@ -161,7 +152,7 @@ where ::Metadata: CheckedMetadata, { let tx = checked.transaction(); - Self::run_predicate(PredicateRunKind::Verifying(tx), params, memory.as_mut()) + Self::run_predicates(PredicateRunKind::Verifying(tx), params, memory.as_mut()) } /// Initialize the VM with the provided transaction and check all predicates defined @@ -199,7 +190,7 @@ where params: &CheckPredicateParams, mut memory: impl Memory, ) -> Result { - let predicates_checked = Self::run_predicate( + let predicates_checked = Self::run_predicates( PredicateRunKind::Estimating(transaction), params, memory.as_mut(), @@ -242,9 +233,18 @@ where E: ParallelExecutor, { let mut checks = vec![]; - let predicate_action = PredicateAction::from(&kind); let tx_offset = params.tx_offset; + let max_gas_per_tx = params.max_gas_per_tx; + let max_gas_per_predicate = params.max_gas_per_predicate; + let available_gas = core::cmp::min(max_gas_per_predicate, max_gas_per_tx); + let predicate_action = match kind { + PredicateRunKind::Verifying(_) => PredicateAction::Verifying, + PredicateRunKind::Estimating(_) => { + PredicateAction::Estimating { available_gas } + } + }; + for index in 0..kind.tx().inputs().len() { if let Some(predicate) = RuntimePredicate::from_tx(kind.tx(), tx_offset, index) @@ -254,14 +254,16 @@ where let mut memory = pool.get_new().await; let verify_task = E::create_task(move || { - Interpreter::check_predicate( + let (used_gas, result) = Interpreter::check_predicate( tx, index, predicate_action, predicate, my_params, memory.as_mut(), - ) + ); + + result.map(|_| (used_gas, index)) }); checks.push(verify_task); @@ -273,28 +275,42 @@ where Self::finalize_check_predicate(kind, checks, params) } - fn run_predicate( + fn run_predicates( kind: PredicateRunKind<'_, Tx>, params: &CheckPredicateParams, mut memory: impl Memory, ) -> Result { - let predicate_action = PredicateAction::from(&kind); let mut checks = vec![]; + let max_gas = kind.tx().max_gas(¶ms.gas_costs, ¶ms.fee_params); + let max_gas_per_tx = params.max_gas_per_tx; + let max_gas_per_predicate = params.max_gas_per_predicate; + let mut available_gas = + core::cmp::min(max_gas_per_predicate, max_gas_per_tx).saturating_sub(max_gas); + for index in 0..kind.tx().inputs().len() { let tx = kind.tx().clone(); if let Some(predicate) = RuntimePredicate::from_tx(&tx, params.tx_offset, index) { - checks.push(Interpreter::check_predicate( + let predicate_action = match kind { + PredicateRunKind::Verifying(_) => PredicateAction::Verifying, + PredicateRunKind::Estimating(_) => { + PredicateAction::Estimating { available_gas } + } + }; + let (gas_used, result) = Interpreter::check_predicate( tx, index, predicate_action, predicate, params.clone(), memory.as_mut(), - )); + ); + available_gas = available_gas.saturating_sub(gas_used); + let result = result.map(|_| (gas_used, index)); + checks.push(result); } } @@ -308,7 +324,7 @@ where predicate: RuntimePredicate, params: CheckPredicateParams, memory: &mut MemoryInstance, - ) -> Result<(Word, usize), PredicateVerificationFailed> { + ) -> (Word, Result<(), PredicateVerificationFailed>) { match &tx.inputs()[index] { Input::CoinPredicate(CoinPredicate { owner: address, @@ -326,14 +342,12 @@ where .. }) => { if !Input::is_predicate_owner_valid(address, predicate) { - return Err(PredicateVerificationFailed::InvalidOwner); + return (0, Err(PredicateVerificationFailed::InvalidOwner)); } } _ => {} } - let max_gas_per_tx = params.max_gas_per_tx; - let max_gas_per_predicate = params.max_gas_per_predicate; let zero_gas_price = 0; let interpreter_params = InterpreterParams::new(zero_gas_price, params); @@ -343,47 +357,48 @@ where interpreter_params, ); - let available_gas = match predicate_action { + let (context, available_gas) = match predicate_action { PredicateAction::Verifying => { let context = Context::PredicateVerification { program: predicate }; - let available_gas = - if let Some(x) = tx.inputs()[index].predicate_gas_used() { - x - } else { - return Err(PredicateVerificationFailed::GasNotSpecified); - }; + let available_gas = tx.inputs()[index] + .predicate_gas_used() + .expect("We only run predicates at this stage, so it should exist."); - vm.init_predicate(context, tx, available_gas)?; - available_gas + (context, available_gas) } - PredicateAction::Estimating => { + PredicateAction::Estimating { available_gas } => { let context = Context::PredicateEstimation { program: predicate }; - let available_gas = core::cmp::min(max_gas_per_predicate, max_gas_per_tx); - vm.init_predicate(context, tx, available_gas)?; - available_gas + (context, available_gas) } }; + if let Err(err) = vm.init_predicate(context, tx, available_gas) { + return (0, Err(err.into())); + } + let result = vm.verify_predicate(); let is_successful = matches!(result, Ok(ProgramState::Return(0x01))); - let gas_used = available_gas - .checked_sub(vm.remaining_gas()) - .ok_or_else(|| Bug::new(BugVariant::GlobalGasUnderflow))?; + let Some(gas_used) = available_gas.checked_sub(vm.remaining_gas()) else { + return (0, Err(Bug::new(BugVariant::GlobalGasUnderflow).into())); + }; if let PredicateAction::Verifying = predicate_action { if !is_successful { - result?; - return Err(PredicateVerificationFailed::False); + return if let Err(err) = result { + (gas_used, Err(err)) + } else { + (gas_used, Err(PredicateVerificationFailed::False)) + } } if vm.remaining_gas() != 0 { - return Err(PredicateVerificationFailed::GasMismatch); + return (gas_used, Err(PredicateVerificationFailed::GasMismatch)); } } - Ok((gas_used, index)) + (gas_used, Ok(())) } fn finalize_check_predicate( diff --git a/fuel-vm/src/interpreter/executors/main/tests.rs b/fuel-vm/src/interpreter/executors/main/tests.rs index 79228d7771..261dda95d6 100644 --- a/fuel-vm/src/interpreter/executors/main/tests.rs +++ b/fuel-vm/src/interpreter/executors/main/tests.rs @@ -83,7 +83,7 @@ fn estimate_gas_gives_proper_gas_used() { coin_amount, AssetId::default(), rng.gen(), - rng.gen(), + 0, predicate, vec![], ); diff --git a/fuel-vm/src/tests/mod.rs b/fuel-vm/src/tests/mod.rs index a94cfcf385..ddd6e5fca5 100644 --- a/fuel-vm/src/tests/mod.rs +++ b/fuel-vm/src/tests/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] use futures as _; +use ntest as _; use tokio as _; use tokio_rayon as _; diff --git a/fuel-vm/src/tests/predicate.rs b/fuel-vm/src/tests/predicate.rs index 1967043bcd..6b6ec7dd70 100644 --- a/fuel-vm/src/tests/predicate.rs +++ b/fuel-vm/src/tests/predicate.rs @@ -201,7 +201,7 @@ async fn get_verifying_predicate() { async fn execute_gas_metered_predicates( predicates: Vec>, ) -> Result { - const GAS_LIMIT: Word = 10000; + const GAS_LIMIT: Word = 100_000; let rng = &mut StdRng::seed_from_u64(2322u64); let arb_max_fee = 2_000; @@ -404,7 +404,7 @@ async fn gas_used_by_predicates_not_causes_out_of_gas_during_script() { coin_amount, AssetId::default(), rng.gen(), - rng.gen(), + 0, predicate, vec![], ); @@ -563,3 +563,48 @@ async fn gas_used_by_predicates_more_than_limit() { CheckError::PredicateVerificationFailed(_) )); } + +#[test] +#[ntest::timeout(5_000)] +fn synchronous_estimate_predicates_respects_total_tx_gas_limit() { + let limit = 1_000_000; + let rng = &mut StdRng::seed_from_u64(2322u64); + + let params = CheckPredicateParams { + max_gas_per_predicate: limit, + max_gas_per_tx: limit, + gas_costs: GasCosts::unit(), + ..Default::default() + }; + + // Infinite loop + let predicate = vec![op::noop(), op::jmpb(RegId::ZERO, 0)] + .into_iter() + .collect::>(); + let predicate_owner = Input::predicate_owner(&predicate); + + let mut builder = TransactionBuilder::script(vec![], vec![]); + builder.max_fee_limit(1000).maturity(Default::default()); + + let coin_amount = 100_000; + + for _ in 0..255 { + builder.add_input(Input::coin_predicate( + rng.gen(), + predicate_owner, + coin_amount, + AssetId::default(), + rng.gen(), + 0, + predicate.clone(), + vec![], + )); + } + let mut transaction = builder.finalize(); + + // When + let result = transaction.estimate_predicates(¶ms, MemoryInstance::new()); + + // Then + assert_eq!(Ok(()), result); +}