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: validate predicates using the VM before sending out transaction #1286

Merged
merged 11 commits into from
Mar 19, 2024
22 changes: 11 additions & 11 deletions packages/fuels-accounts/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ impl Provider {
/// Sends a transaction to the underlying Provider's client.
pub async fn send_transaction_and_await_commit<T: Transaction>(
&self,
mut tx: T,
tx: T,
) -> Result<TxStatus> {
self.prepare_transaction_for_sending(&mut tx).await?;
let tx = self.prepare_transaction_for_sending(tx).await?;
let tx_status = self
.client
.submit_and_await_commit(&tx.clone().into())
Expand All @@ -199,26 +199,26 @@ impl Provider {
Ok(tx_status)
}

async fn prepare_transaction_for_sending<T: Transaction>(&self, tx: &mut T) -> Result<()> {
async fn prepare_transaction_for_sending<T: Transaction>(&self, mut tx: T) -> Result<T> {
tx.precompute(&self.chain_id())?;

let chain_info = self.chain_info().await?;
tx.check(
chain_info.latest_block.header.height,
self.consensus_parameters(),
)?;
let latest_block_height = chain_info.latest_block.header.height;
tx.check(latest_block_height, self.consensus_parameters())?;

if tx.is_using_predicates() {
tx.estimate_predicates(&self.consensus_parameters)?;
tx.estimate_predicates(self.consensus_parameters())?;
tx.clone()
.validate_predicates(self.consensus_parameters(), latest_block_height)?;
}

self.validate_transaction(tx.clone()).await?;

Ok(())
Ok(tx)
}

pub async fn send_transaction<T: Transaction>(&self, mut tx: T) -> Result<TxId> {
self.prepare_transaction_for_sending(&mut tx).await?;
pub async fn send_transaction<T: Transaction>(&self, tx: T) -> Result<TxId> {
let tx = self.prepare_transaction_for_sending(tx).await?;
self.submit(tx).await
}

Expand Down
38 changes: 36 additions & 2 deletions packages/fuels-core/src/types/wrappers/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use fuel_tx::{
TransactionFee, UniqueIdentifier, Witness,
};
use fuel_types::{bytes::padded_len_usize, AssetId, ChainId};
use fuel_vm::checked_transaction::EstimatePredicates;
use fuel_vm::checked_transaction::{
CheckPredicateParams, CheckPredicates, EstimatePredicates, IntoChecked,
};
use itertools::Itertools;

use crate::{
Expand Down Expand Up @@ -197,10 +199,26 @@ pub trait GasValidation: sealed::Sealed {
fn validate_gas(&self, _gas_used: u64) -> Result<()>;
}

pub trait ValidatablePredicates: sealed::Sealed {
/// If a transaction contains predicates, we can verify that these predicates validate, ie
/// that they return `true`
fn validate_predicates(
self,
consensus_parameters: &ConsensusParameters,
block_height: u32,
) -> Result<()>;
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Transaction:
Into<FuelTransaction> + EstimablePredicates + GasValidation + Clone + Debug + sealed::Sealed
Into<FuelTransaction>
+ EstimablePredicates
+ ValidatablePredicates
+ GasValidation
+ Clone
+ Debug
+ sealed::Sealed
{
fn fee_checked_from_tx(
&self,
Expand Down Expand Up @@ -319,6 +337,22 @@ macro_rules! impl_tx_wrapper {
}
}

impl ValidatablePredicates for $wrapper {
fn validate_predicates(
self,
consensus_parameters: &ConsensusParameters,
block_height: u32,
) -> Result<()> {
let checked = self
.tx
.into_checked(block_height.into(), consensus_parameters)?;
let check_predicates_parameters: CheckPredicateParams = consensus_parameters.into();
checked.check_predicates(&check_predicates_parameters)?;

Ok(())
}
}

impl sealed::Sealed for $wrapper {}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
Expand Down
154 changes: 153 additions & 1 deletion packages/fuels/tests/predicates.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::default::Default;
use std::{default::Default, str::FromStr};

use fuels::{
core::{
Expand Down Expand Up @@ -915,3 +915,155 @@ async fn predicate_encoder_config_is_applied() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn predicate_validation() -> Result<()> {
let default_asset_id = AssetId::default();
let hex_str = "0xfefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe";
let other_asset_id = AssetId::from_str(hex_str)?;
let begin_coin_amount = 1_000;

let tx_policies = TxPolicies::default();
let wallets_config = WalletsConfig::new_multiple_assets(
2,
vec![
AssetConfig {
id: default_asset_id,
num_coins: 1,
coin_amount: begin_coin_amount,
},
AssetConfig {
id: other_asset_id,
num_coins: 1,
coin_amount: begin_coin_amount,
},
],
);

let wallets = &launch_custom_provider_and_get_wallets(wallets_config, None, None).await?;

let first_wallet = &wallets[0];
let second_wallet = &wallets[1];

abigen!(Predicate(
name = "MyPredicate",
abi = "packages/fuels/tests/predicates/basic_predicate/out/debug/basic_predicate-abi.json"
));
let code_path =
"../../packages/fuels/tests/predicates/basic_predicate/out/debug/basic_predicate.bin";

// the predicate evaluates to true if the two arguments are equal
let correct_predicate_data = MyPredicateEncoder::default().encode_data(4096, 4096)?;
let predicate_with_correct_data: Predicate = Predicate::load_from(code_path)?
.with_provider(first_wallet.try_provider()?.clone())
.with_data(correct_predicate_data);

let incorrect_predicate_data = MyPredicateEncoder::default().encode_data(1000, 0)?;
let predicate_with_incorrect_data: Predicate = Predicate::load_from(code_path)?
.with_provider(first_wallet.try_provider()?.clone())
.with_data(incorrect_predicate_data);

// The predicate needs to be funded with the target asset id, and the base asset id to pay for
// gas, even for just validation.
let amount_to_transfer_to_predicate = 1000;
first_wallet
.transfer(
// the data doesn't change the predicate's address
predicate_with_correct_data.address(),
amount_to_transfer_to_predicate,
other_asset_id,
tx_policies,
)
.await?;
first_wallet
.transfer(
predicate_with_correct_data.address(),
amount_to_transfer_to_predicate,
default_asset_id,
tx_policies,
)
.await?;

let amount_to_unlock = 500;
// Test with a non-default asset ID,to check that fee estimation works
{
// Check that a validated predicate => transfer can occur
predicate_with_correct_data
.transfer(
second_wallet.address(),
amount_to_unlock,
other_asset_id,
tx_policies,
)
.await?;
assert_eq!(
second_wallet.get_asset_balance(&other_asset_id).await?,
amount_to_unlock + begin_coin_amount
);

let error_string = predicate_with_incorrect_data
.transfer(second_wallet.address(), 10, other_asset_id, tx_policies)
.await
.unwrap_err()
.to_string();
assert!(
error_string.contains("PredicateVerificationFailed(Panic(PredicateReturnedNonOne))")
);
let transfer_error_string = predicate_with_incorrect_data
.transfer(
second_wallet.address(),
amount_to_unlock,
other_asset_id,
tx_policies,
)
.await
.unwrap_err()
.to_string();
// the transfer failed as expected
assert!(transfer_error_string
.contains("PredicateVerificationFailed(Panic(PredicateReturnedNonOne))"));
// so the balance is not modified
assert_eq!(
second_wallet.get_asset_balance(&other_asset_id).await?,
amount_to_unlock + begin_coin_amount
);
}

// Test with default asset ID
{
// Check that a validated predicate => transfer can occur
predicate_with_correct_data
.transfer(
second_wallet.address(),
amount_to_unlock,
default_asset_id,
tx_policies,
)
.await?;
assert_eq!(
second_wallet.get_asset_balance(&default_asset_id).await?,
amount_to_unlock + begin_coin_amount
);

let transfer_error_string = predicate_with_incorrect_data
.transfer(
second_wallet.address(),
amount_to_unlock,
default_asset_id,
tx_policies,
)
.await
.unwrap_err()
.to_string();
// the transfer failed as expected
assert!(transfer_error_string
.contains("PredicateVerificationFailed(Panic(PredicateReturnedNonOne))"));
// so the balance is not modified
assert_eq!(
second_wallet.get_asset_balance(&default_asset_id).await?,
amount_to_unlock + begin_coin_amount
);
}

Ok(())
}
Loading