diff --git a/packages/fuels-accounts/src/accounts_utils.rs b/packages/fuels-accounts/src/accounts_utils.rs index fb39660e81..6125bf7a81 100644 --- a/packages/fuels-accounts/src/accounts_utils.rs +++ b/packages/fuels-accounts/src/accounts_utils.rs @@ -1,4 +1,4 @@ -use fuel_tx::{ConsensusParameters, Output, Receipt}; +use fuel_tx::{Output, Receipt}; use fuel_types::MessageId; use fuels_core::{ constants::BASE_ASSET_ID, @@ -6,7 +6,7 @@ use fuels_core::{ bech32::Bech32Address, errors::{error, Error, Result}, input::Input, - transaction_builders::TransactionBuilder, + transaction_builders::{NetworkInfo, TransactionBuilder}, }, }; @@ -16,11 +16,11 @@ pub fn extract_message_id(receipts: &[Receipt]) -> Option { pub fn calculate_base_amount_with_fee( tb: &impl TransactionBuilder, - consensus_params: &ConsensusParameters, + network_info: &NetworkInfo, previous_base_amount: u64, ) -> Result { let transaction_fee = tb - .fee_checked_from_tx(consensus_params)? + .fee_checked_from_tx(network_info)? .ok_or(error!(InvalidData, "Error calculating TransactionFee"))?; let mut new_base_amount = transaction_fee.max_fee() + previous_base_amount; diff --git a/packages/fuels-accounts/src/predicate.rs b/packages/fuels-accounts/src/predicate.rs index 5b47cf42aa..1949384d3b 100644 --- a/packages/fuels-accounts/src/predicate.rs +++ b/packages/fuels-accounts/src/predicate.rs @@ -148,9 +148,9 @@ impl Account for Predicate { mut tb: Tb, previous_base_amount: u64, ) -> Result { - let consensus_parameters = self.try_provider()?.consensus_parameters(); + let network_info = self.try_provider()?.network_info().await?; let new_base_amount = - calculate_base_amount_with_fee(&tb, &consensus_parameters, previous_base_amount)?; + calculate_base_amount_with_fee(&tb, &network_info, previous_base_amount)?; let new_base_inputs = self .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount) diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 89a89b42af..0b7645337a 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -193,7 +193,25 @@ impl Provider { Ok(tx_id) } - pub async fn send_transaction(&self, tx: T) -> Result { + pub async fn send_transaction(&self, mut tx: T) -> Result { + tx.precompute(&self.chain_id())?; + + let chain_info = self.chain_info().await?; + tx.check_without_signatures( + chain_info.latest_block.header.height, + &self.consensus_parameters(), + )?; + + if tx.is_using_predicates() { + tx.estimate_predicates(&self.consensus_parameters, &chain_info.gas_costs)?; + } + + self.validate_transaction(tx.clone()).await?; + + Ok(self.client.submit(&tx.into()).await?) + } + + async fn validate_transaction(&self, tx: T) -> Result<()> { let tolerance = 0.0; let TransactionCost { gas_used, @@ -219,14 +237,7 @@ impl Provider { )); } - let chain_info = self.chain_info().await?; - tx.check_without_signatures( - chain_info.latest_block.header.height, - &self.consensus_parameters(), - )?; - - let tx_id = self.client.submit(&tx.into()).await?; - Ok(tx_id) + Ok(()) } pub async fn tx_status(&self, tx_id: &TxId) -> ProviderResult { diff --git a/packages/fuels-accounts/src/wallet.rs b/packages/fuels-accounts/src/wallet.rs index f63c8d60ef..11511217aa 100644 --- a/packages/fuels-accounts/src/wallet.rs +++ b/packages/fuels-accounts/src/wallet.rs @@ -264,11 +264,11 @@ impl Account for WalletUnlocked { mut tb: Tb, previous_base_amount: u64, ) -> Result { - let consensus_parameters = self.try_provider()?.consensus_parameters(); + let network_info = self.try_provider()?.network_info().await?; self.sign_transaction(&mut tb); let new_base_amount = - calculate_base_amount_with_fee(&tb, &consensus_parameters, previous_base_amount)?; + calculate_base_amount_with_fee(&tb, &network_info, previous_base_amount)?; let new_base_inputs = self .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount) diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index 1c9639c43e..2b22b41198 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -5,12 +5,11 @@ use std::collections::HashMap; use fuel_asm::{op, GTFArgs, RegId}; use fuel_crypto::{Message as CryptoMessage, SecretKey, Signature}; use fuel_tx::{ - field::{GasLimit, GasPrice, Witnesses}, - Cacheable, ConsensusParameters, Create, Input as FuelInput, Output, Script, StorageSlot, + field::Witnesses, ConsensusParameters, Create, Input as FuelInput, Output, Script, StorageSlot, Transaction as FuelTransaction, TransactionFee, TxPointer, UniqueIdentifier, Witness, }; use fuel_types::{bytes::padded_len_usize, Bytes32, ChainId, MemLayout, Salt}; -use fuel_vm::{checked_transaction::EstimatePredicates, gas::GasCosts}; +use fuel_vm::gas::GasCosts; use zeroize::{Zeroize, ZeroizeOnDrop}; use super::{chain_info::ChainInfo, node_info::NodeInfo}; @@ -65,7 +64,7 @@ pub trait TransactionBuilder: Send { fn build(self) -> Result; fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey); - fn fee_checked_from_tx(&self, params: &ConsensusParameters) -> Result>; + fn fee_checked_from_tx(&self, network_info: &NetworkInfo) -> Result>; fn with_maturity(self, maturity: u32) -> Self; fn with_gas_price(self, gas_price: u64) -> Self; fn with_gas_limit(self, gas_limit: u64) -> Self; @@ -86,25 +85,20 @@ macro_rules! impl_tx_trait { impl TransactionBuilder for $ty { type TxType = $tx_ty; fn build(self) -> Result<$tx_ty> { - let uses_predicates = self.is_using_predicates(); - let base_offset = if uses_predicates { + let is_using_predicates = self.is_using_predicates(); + let base_offset = if is_using_predicates { self.base_offset() } else { 0 }; - let network_info = self.network_info.clone(); - let num_witnesses = self.num_witnesses()?; - let mut tx = self.resolve_fuel_tx(base_offset, num_witnesses)?; - - tx.precompute(&network_info.chain_id())?; - - if uses_predicates { - estimate_predicates(&mut tx, &network_info)?; - }; + let tx = self.resolve_fuel_tx(base_offset, num_witnesses)?; - Ok($tx_ty { tx }) + Ok($tx_ty { + tx, + is_using_predicates, + }) } fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey) { @@ -117,10 +111,21 @@ macro_rules! impl_tx_trait { fn fee_checked_from_tx( &self, - params: &ConsensusParameters, + network_info: &NetworkInfo, ) -> Result> { - let tx = self.clone().build()?.tx; - Ok(TransactionFee::checked_from_tx(params, &tx)) + let mut tx = self.clone().build()?; + + if tx.is_using_predicates() { + tx.estimate_predicates( + &network_info.consensus_parameters, + &network_info.gas_costs, + )?; + }; + + Ok(TransactionFee::checked_from_tx( + &network_info.consensus_parameters, + &tx.tx, + )) } fn with_maturity(mut self, maturity: u32) -> Self { @@ -704,24 +709,6 @@ fn generate_missing_witnesses( .collect() } -fn estimate_predicates(tx: &mut T, network_info: &NetworkInfo) -> Result<()> -where - T: GasLimit + GasPrice + EstimatePredicates, -{ - let consensus_parameters = &network_info.consensus_parameters; - - let gas_price = *tx.gas_price(); - let gas_limit = *tx.gas_limit(); - *tx.gas_price_mut() = 0; - *tx.gas_limit_mut() = consensus_parameters.max_gas_per_tx; - - tx.estimate_predicates(consensus_parameters, &network_info.gas_costs)?; - *tx.gas_price_mut() = gas_price; - *tx.gas_limit_mut() = gas_limit; - - Ok(()) -} - #[cfg(test)] mod tests { use super::*; diff --git a/packages/fuels-core/src/types/wrappers/transaction.rs b/packages/fuels-core/src/types/wrappers/transaction.rs index 00f3fa907a..a50ff9795d 100644 --- a/packages/fuels-core/src/types/wrappers/transaction.rs +++ b/packages/fuels-core/src/types/wrappers/transaction.rs @@ -4,11 +4,13 @@ use fuel_tx::{ field::{ GasLimit, GasPrice, Inputs, Maturity, Outputs, Script as ScriptField, ScriptData, Witnesses, }, - Bytes32, Chargeable, ConsensusParameters, Create, FormatValidityChecks, Input, Output, - Salt as FuelSalt, Script, StorageSlot, Transaction as FuelTransaction, TransactionFee, + Bytes32, Cacheable, Chargeable, ConsensusParameters, Create, FormatValidityChecks, Input, + Output, Salt as FuelSalt, Script, StorageSlot, Transaction as FuelTransaction, TransactionFee, UniqueIdentifier, Witness, }; + use fuel_types::ChainId; +use fuel_vm::{checked_transaction::EstimatePredicates, prelude::GasCosts}; use crate::types::Result; @@ -98,6 +100,21 @@ pub trait Transaction: Into + Clone { fn witnesses(&self) -> &Vec; + fn is_using_predicates(&self) -> bool; + + /// Precompute transaction metadata. The metadata is required for + /// `check_without_signatures` validation. + fn precompute(&mut self, chain_id: &ChainId) -> Result<()>; + + /// If a transactions contains predicates, we have to estimate them + /// before sending the transaction to the node. The estimation will check + /// all predicates and set the `predicate_gas_used` to the actual consumed gas. + fn estimate_predicates( + &mut self, + consensus_parameters: &ConsensusParameters, + gas_costs: &GasCosts, + ) -> Result<()>; + /// Append witness and return the corresponding witness index fn append_witness(&mut self, witness: Witness) -> usize; } @@ -214,6 +231,31 @@ impl Transaction for TransactionType { } } + fn is_using_predicates(&self) -> bool { + match self { + TransactionType::Script(tx) => tx.is_using_predicates(), + TransactionType::Create(tx) => tx.is_using_predicates(), + } + } + + fn precompute(&mut self, chain_id: &ChainId) -> Result<()> { + match self { + TransactionType::Script(tx) => tx.precompute(chain_id), + TransactionType::Create(tx) => tx.precompute(chain_id), + } + } + + fn estimate_predicates( + &mut self, + consensus_parameters: &ConsensusParameters, + gas_costs: &GasCosts, + ) -> Result<()> { + match self { + TransactionType::Script(tx) => tx.estimate_predicates(consensus_parameters, gas_costs), + TransactionType::Create(tx) => tx.estimate_predicates(consensus_parameters, gas_costs), + } + } + fn append_witness(&mut self, witness: Witness) -> usize { match self { TransactionType::Script(tx) => tx.append_witness(witness), @@ -227,6 +269,7 @@ macro_rules! impl_tx_wrapper { #[derive(Debug, Clone)] pub struct $wrapper { pub(crate) tx: $wrapped, + pub(crate) is_using_predicates: bool, } impl From<$wrapper> for $wrapped { @@ -243,7 +286,19 @@ macro_rules! impl_tx_wrapper { impl From<$wrapped> for $wrapper { fn from(tx: $wrapped) -> Self { - $wrapper { tx } + let is_using_predicates = tx.inputs().iter().any(|input| { + matches!( + input, + Input::CoinPredicate { .. } + | Input::MessageCoinPredicate { .. } + | Input::MessageDataPredicate { .. } + ) + }); + + $wrapper { + tx, + is_using_predicates, + } } } @@ -312,6 +367,32 @@ macro_rules! impl_tx_wrapper { self.tx.witnesses() } + fn is_using_predicates(&self) -> bool { + self.is_using_predicates + } + + fn precompute(&mut self, chain_id: &ChainId) -> Result<()> { + Ok(self.tx.precompute(chain_id)?) + } + + fn estimate_predicates( + &mut self, + consensus_parameters: &ConsensusParameters, + gas_costs: &GasCosts, + ) -> Result<()> { + let gas_price = *self.tx.gas_price(); + let gas_limit = *self.tx.gas_limit(); + *self.tx.gas_price_mut() = 0; + *self.tx.gas_limit_mut() = consensus_parameters.max_gas_per_tx; + + self.tx + .estimate_predicates(consensus_parameters, gas_costs)?; + *self.tx.gas_price_mut() = gas_price; + *self.tx.gas_limit_mut() = gas_limit; + + Ok(()) + } + fn append_witness(&mut self, witness: Witness) -> usize { let idx = self.tx.witnesses().len(); self.tx.witnesses_mut().push(witness); diff --git a/packages/fuels/Forc.toml b/packages/fuels/Forc.toml index a7b81926e8..370c2dc9a5 100644 --- a/packages/fuels/Forc.toml +++ b/packages/fuels/Forc.toml @@ -35,6 +35,7 @@ members = [ 'tests/logs/script_with_contract_logs', 'tests/predicates/basic_predicate', 'tests/predicates/predicate_configurables', + 'tests/predicates/predicate_witnesses', 'tests/predicates/signatures', 'tests/scripts/arguments', 'tests/scripts/basic_script', diff --git a/packages/fuels/tests/contracts.rs b/packages/fuels/tests/contracts.rs index 9c3f326e21..97db715242 100644 --- a/packages/fuels/tests/contracts.rs +++ b/packages/fuels/tests/contracts.rs @@ -1644,7 +1644,7 @@ async fn heap_types_correctly_offset_in_create_transactions_w_storage_slots() -> )? .with_data(data) .with_provider(provider); - let wallet: WalletUnlocked = wallet; + wallet .transfer( predicate.address(), diff --git a/packages/fuels/tests/predicates.rs b/packages/fuels/tests/predicates.rs index 289d9741f5..379bac35f9 100644 --- a/packages/fuels/tests/predicates.rs +++ b/packages/fuels/tests/predicates.rs @@ -8,7 +8,11 @@ use fuels::{ transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, }, }; -use fuels_core::types::{coin_type::CoinType, input::Input}; +use fuels_core::{ + codec::ABIEncoder, + traits::Tokenizable, + types::{coin_type::CoinType, input::Input}, +}; async fn assert_address_balance( address: &Bech32Address, @@ -768,3 +772,125 @@ async fn predicate_transfer_non_base_asset() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn predicate_can_access_manually_added_witnesses() -> Result<()> { + abigen!(Predicate( + name = "MyPredicate", + abi = "packages/fuels/tests/predicates/predicate_witnesses/out/debug/predicate_witnesses-abi.json" + )); + + let predicate_data = MyPredicateEncoder::encode_data(0, 1); + + let mut predicate: Predicate = Predicate::load_from( + "tests/predicates/predicate_witnesses/out/debug/predicate_witnesses.bin", + )? + .with_data(predicate_data); + + let num_coins = 4; + let num_messages = 0; + let amount = 16; + let (provider, predicate_balance, receiver, receiver_balance, asset_id) = + setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?; + + predicate.set_provider(provider.clone()); + + let amount_to_send = 12; + let inputs = predicate + .get_asset_inputs_for_amount(asset_id, amount_to_send) + .await?; + let outputs = + predicate.get_asset_outputs_for_amount(receiver.address(), asset_id, amount_to_send); + + let network_info = provider.network_info().await?; + let mut tx = ScriptTransactionBuilder::prepare_transfer( + inputs, + outputs, + TxParameters::default(), + network_info.clone(), + ) + .build()?; + + let witness = ABIEncoder::encode(&[64u8.into_token()])?.resolve(0); + let witness2 = ABIEncoder::encode(&[4096u64.into_token()])?.resolve(0); + + tx.append_witness(witness.into()); + tx.append_witness(witness2.into()); + + provider.send_transaction_and_await_commit(tx).await?; + + // The predicate has spent the funds + assert_address_balance( + predicate.address(), + &provider, + asset_id, + predicate_balance - amount_to_send, + ) + .await; + + // Funds were transferred + assert_address_balance( + receiver.address(), + &provider, + asset_id, + receiver_balance + amount_to_send, + ) + .await; + + Ok(()) +} + +#[tokio::test] +async fn tx_id_not_changed_after_adding_witnesses() -> Result<()> { + abigen!(Predicate( + name = "MyPredicate", + abi = "packages/fuels/tests/predicates/predicate_witnesses/out/debug/predicate_witnesses-abi.json" + )); + + let predicate_data = MyPredicateEncoder::encode_data(0, 1); + + let mut predicate: Predicate = Predicate::load_from( + "tests/predicates/predicate_witnesses/out/debug/predicate_witnesses.bin", + )? + .with_data(predicate_data); + + let num_coins = 4; + let num_messages = 0; + let amount = 16; + let (provider, _predicate_balance, receiver, _receiver_balance, asset_id) = + setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?; + + predicate.set_provider(provider.clone()); + + let amount_to_send = 12; + let inputs = predicate + .get_asset_inputs_for_amount(asset_id, amount_to_send) + .await?; + let outputs = + predicate.get_asset_outputs_for_amount(receiver.address(), asset_id, amount_to_send); + + let network_info = provider.network_info().await?; + let mut tx = ScriptTransactionBuilder::prepare_transfer( + inputs, + outputs, + TxParameters::default(), + network_info.clone(), + ) + .build()?; + + let tx_id = tx.id(network_info.chain_id()); + + let witness = ABIEncoder::encode(&[64u8.into_token()])?.resolve(0); + let witness2 = ABIEncoder::encode(&[4096u64.into_token()])?.resolve(0); + + tx.append_witness(witness.into()); + tx.append_witness(witness2.into()); + let tx_id_after_witnesses = tx.id(network_info.chain_id()); + + let tx_id_from_provider = provider.send_transaction_and_await_commit(tx).await?; + + assert_eq!(tx_id, tx_id_after_witnesses); + assert_eq!(tx_id, tx_id_from_provider); + + Ok(()) +} diff --git a/packages/fuels/tests/predicates/predicate_witnesses/Forc.toml b/packages/fuels/tests/predicates/predicate_witnesses/Forc.toml new file mode 100644 index 0000000000..690196372f --- /dev/null +++ b/packages/fuels/tests/predicates/predicate_witnesses/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "predicate_witnesses" diff --git a/packages/fuels/tests/predicates/predicate_witnesses/src/main.sw b/packages/fuels/tests/predicates/predicate_witnesses/src/main.sw new file mode 100644 index 0000000000..7103330dc0 --- /dev/null +++ b/packages/fuels/tests/predicates/predicate_witnesses/src/main.sw @@ -0,0 +1,10 @@ +predicate; + +use std::tx::tx_witness_data; + +fn main(witness_index: u64, witness_index2: u64) -> bool { + let witness: u8 = tx_witness_data(witness_index); + let witness2: u64 = tx_witness_data(witness_index2); + + witness == 64 && witness2 == 4096 +}