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

Take into account spent gas during synchronous predicates estimation #771

Merged
merged 6 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions fuel-vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
101 changes: 58 additions & 43 deletions fuel-vm/src/interpreter/executors/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,7 @@ impl<'a, Tx> PredicateRunKind<'a, Tx> {
#[derive(Debug, Clone, Copy)]
enum PredicateAction {
Verifying,
Estimating,
}

impl<Tx> 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<Tx> Interpreter<&mut MemoryInstance, PredicateStorage, Tx>
Expand All @@ -161,7 +152,7 @@ where
<Tx as IntoChecked>::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
Expand Down Expand Up @@ -199,7 +190,7 @@ where
params: &CheckPredicateParams,
mut memory: impl Memory,
) -> Result<PredicatesChecked, PredicateVerificationFailed> {
let predicates_checked = Self::run_predicate(
let predicates_checked = Self::run_predicates(
PredicateRunKind::Estimating(transaction),
params,
memory.as_mut(),
Expand Down Expand Up @@ -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)
Expand All @@ -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);
Expand All @@ -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<PredicatesChecked, PredicateVerificationFailed> {
let predicate_action = PredicateAction::from(&kind);
let mut checks = vec![];

let max_gas = kind.tx().max_gas(&params.gas_costs, &params.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);
}
}

Expand All @@ -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,
Expand All @@ -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);

Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion fuel-vm/src/interpreter/executors/main/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ fn estimate_gas_gives_proper_gas_used() {
coin_amount,
AssetId::default(),
rng.gen(),
rng.gen(),
0,
predicate,
vec![],
);
Expand Down
1 change: 1 addition & 0 deletions fuel-vm/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -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 _;

Expand Down
49 changes: 47 additions & 2 deletions fuel-vm/src/tests/predicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ async fn get_verifying_predicate() {
async fn execute_gas_metered_predicates(
predicates: Vec<Vec<Instruction>>,
) -> Result<u64, ()> {
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;
Expand Down Expand Up @@ -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![],
);
Expand Down Expand Up @@ -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::<Vec<u8>>();
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(&params, MemoryInstance::new());

// Then
assert_eq!(Ok(()), result);
Copy link
Member

Choose a reason for hiding this comment

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

if the estimation runs out of gas due to each predicate running an infinite loop, why is this an Ok(())?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It was done a long time ago to allow estimate predicates even if they return false. For cases when signature incorrect and predicate fails

I also don't like it=D I was thinking of modifying the code and returning an error in all cases except false predicate. But it will change how estimation works, so I decided not to include it in this PR.

Copy link
Member

@Voxelot Voxelot Jun 15, 2024

Choose a reason for hiding this comment

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

Ok - this seems odd but makes sense given the context. If predicates are invalid the estimation should return an error imo.

}
Loading