Skip to content

Commit

Permalink
feat: validate predicates using the VM before sending out transaction (
Browse files Browse the repository at this point in the history
…#1286)

This PR normally closes (correctly this time I hope) #1195 by adding
predicates
validation within the SDK.
- This means that now the SDK can detect if an invalid predicate
transaction is
  about to be sent to the node and prevents it.

---------

Co-authored-by: Rodrigo Araújo <[email protected]>
Co-authored-by: MujkicA <[email protected]>
Co-authored-by: hal3e <[email protected]>
Co-authored-by: Ahmed Sagdati <[email protected]>
  • Loading branch information
5 people authored Mar 19, 2024
1 parent 834a848 commit df7ba2d
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 14 deletions.
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(())
}

0 comments on commit df7ba2d

Please sign in to comment.