From 52966d9d0ec75d99e3ec00c2f313818f759e6458 Mon Sep 17 00:00:00 2001 From: hal3e Date: Thu, 14 Dec 2023 17:31:03 +0100 Subject: [PATCH 01/23] feat!: add `build_without_signatures` to `TransactionBuilder` --- .../transaction-builders.md | 21 +- examples/cookbook/src/lib.rs | 8 +- packages/fuels-accounts/src/account.rs | 17 +- packages/fuels-accounts/src/provider.rs | 2 +- packages/fuels-accounts/src/wallet.rs | 24 ++- packages/fuels-core/Cargo.toml | 1 + packages/fuels-core/src/types/errors.rs | 7 +- .../src/types/transaction_builders.rs | 193 +++++++++++++++--- .../src/types/wrappers/transaction.rs | 12 +- packages/fuels-programs/src/call_utils.rs | 2 +- packages/fuels-programs/src/contract.rs | 5 +- packages/fuels-programs/src/script_calls.rs | 2 +- packages/fuels/tests/contracts.rs | 42 ++++ 13 files changed, 272 insertions(+), 64 deletions(-) diff --git a/docs/src/custom-transactions/transaction-builders.md b/docs/src/custom-transactions/transaction-builders.md index be821b518..f5bc19b7d 100644 --- a/docs/src/custom-transactions/transaction-builders.md +++ b/docs/src/custom-transactions/transaction-builders.md @@ -52,12 +52,6 @@ We combine all of the inputs and outputs and set them on the builder: {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_io}} ``` -As we have used coins that require a signature, we sign the transaction builder with: - -```rust,ignore -{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_sign}} -``` - > **Note** The signature is not created until the transaction is finalized with `build(&provider)` We need to do one more thing before we stop thinking about transaction inputs. Executing the transaction also incurs a fee that is paid with the base asset. Our base asset inputs need to be large enough so that the total amount covers the transaction fee and any other operations we are doing. The Account trait lets us use `adjust_for_fee()` for adjusting the transaction inputs if needed to cover the fee. The second argument to `adjust_for_fee()` is the total amount of the base asset that we expect our transaction to spend regardless of fees. In our case, this is the **ask_amount** we are transferring to the predicate. @@ -65,6 +59,11 @@ We need to do one more thing before we stop thinking about transaction inputs. E ```rust,ignore {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_adjust}} ``` +As we have used coins that require a signature, we sign the transaction builder with: + +```rust,ignore +{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_sign}} +``` We can also define transaction policies. For example, we can limit the gas price by doing the following: @@ -83,3 +82,13 @@ Finally, we verify the transaction succeeded and that the cold storage indeed ho ```rust,ignore {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_verify}} ``` + +## Building a transaction without signatures + +If you need to build the transaction without signatures - useful when estimating transaction costs or simulations, you can use the `build_without_signatures(&provider)` method and later sign the built transaction. + +```rust,ignore +{{#include ../../../packages/fuels/tests/contracts.rs:tb_build_without_signatures}} +``` + +> **Note** In contrast to signing a transaction builder, when signing a built transaction, you will have to make sure that the order of signatures matches the order of signed inputs. diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index 8a412d25a..37fcdae15 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -296,14 +296,14 @@ mod tests { let mut tb = tb.with_inputs(inputs).with_outputs(outputs); // ANCHOR_END: custom_tx_io - // ANCHOR: custom_tx_sign - hot_wallet.sign_transaction(&mut tb); - // ANCHOR_END: custom_tx_sign - // ANCHOR: custom_tx_adjust hot_wallet.adjust_for_fee(&mut tb, 100).await?; // ANCHOR_END: custom_tx_adjust + // ANCHOR: custom_tx_sign + hot_wallet.sign_transaction(&mut tb); + // ANCHOR_END: custom_tx_sign + // ANCHOR: custom_tx_policies let tx_policies = TxPolicies::default().with_gas_price(1); let tb = tb.with_tx_policies(tx_policies); diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index 69c2f595a..07c977f41 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -16,7 +16,7 @@ use fuels_core::{ errors::{Error, Result}, input::Input, message::Message, - transaction::TxPolicies, + transaction::{Transaction, TxPolicies}, transaction_builders::{ BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder, }, @@ -42,8 +42,12 @@ pub trait Signer: std::fmt::Debug + Send + Sync { message: S, ) -> std::result::Result; - /// Signs the transaction - fn sign_transaction(&self, message: &mut impl TransactionBuilder); + fn sign_transaction(&self, tb: &mut impl TransactionBuilder); + + fn sign_built_transaction( + &self, + tx: &mut impl Transaction, + ) -> std::result::Result; } #[derive(Debug)] @@ -372,6 +376,7 @@ mod tests { Ok(()) } + #[derive(Default)] struct MockDryRunner { c_param: ConsensusParameters, } @@ -426,11 +431,7 @@ mod tests { wallet.sign_transaction(&mut tb); // Add the private key to the transaction builder // ANCHOR_END: sign_tx - let tx = tb - .build(&MockDryRunner { - c_param: ConsensusParameters::default(), - }) - .await?; // Resolve signatures and add corresponding witness indexes + let tx = tb.build(&MockDryRunner::default()).await?; // Resolve signatures and add corresponding witness indexes // Extract the signature from the tx witnesses let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?; diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 88413c67a..ed96da90e 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -218,7 +218,7 @@ impl Provider { tx.precompute(&self.chain_id())?; let chain_info = self.chain_info().await?; - tx.check_without_signatures( + tx.check( chain_info.latest_block.header.height, self.consensus_parameters(), )?; diff --git a/packages/fuels-accounts/src/wallet.rs b/packages/fuels-accounts/src/wallet.rs index 1db313565..cfaa3a83c 100644 --- a/packages/fuels-accounts/src/wallet.rs +++ b/packages/fuels-accounts/src/wallet.rs @@ -8,6 +8,7 @@ use fuels_core::types::{ bech32::{Bech32Address, FUEL_BECH32_HRP}, errors::{Error, Result}, input::Input, + transaction::Transaction, transaction_builders::TransactionBuilder, AssetId, }; @@ -263,19 +264,32 @@ impl Account for WalletUnlocked { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for WalletUnlocked { - type Error = WalletError; - async fn sign_message>( - &self, - message: S, - ) -> WalletResult { + type Error = Error; + async fn sign_message>(&self, message: S) -> Result { let message = Message::new(message); let sig = Signature::sign(&self.private_key, &message); + Ok(sig) } fn sign_transaction(&self, tb: &mut impl TransactionBuilder) { tb.add_unresolved_signature(self.address().clone(), self.private_key); } + + fn sign_built_transaction(&self, tx: &mut impl Transaction) -> Result { + let consensus_parameters = self + .try_provider() + .map_err(|_| WalletError::NoProviderError)? + .consensus_parameters(); + let id = tx.id(consensus_parameters.chain_id); + + let message = Message::from_bytes(*id); + let sig = Signature::sign(&self.private_key, &message); + + tx.append_witness(sig.as_ref().into())?; + + Ok(sig) + } } impl fmt::Debug for Wallet { diff --git a/packages/fuels-core/Cargo.toml b/packages/fuels-core/Cargo.toml index 0bfaafa6b..196249a71 100644 --- a/packages/fuels-core/Cargo.toml +++ b/packages/fuels-core/Cargo.toml @@ -33,6 +33,7 @@ zeroize = { workspace = true, features = ["derive"] } [dev-dependencies] fuels-macros = { workspace = true } +tokio = { workspace = true, features = ["test-util"] } [features] default = ["std"] diff --git a/packages/fuels-core/src/types/errors.rs b/packages/fuels-core/src/types/errors.rs index afcb497c6..5e42bf608 100644 --- a/packages/fuels-core/src/types/errors.rs +++ b/packages/fuels-core/src/types/errors.rs @@ -71,6 +71,11 @@ impl From for Error { } } +impl From for Error { + fn from(err: ValidityError) -> Error { + Error::ValidationError(format!("{:?}", err)) + } +} + impl_error_from!(InvalidData, bech32::Error); impl_error_from!(InvalidData, TryFromSliceError); -impl_error_from!(ValidationError, ValidityError); diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index b26fcc213..771bf4de3 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -13,6 +13,7 @@ use fuel_tx::{ Witness, }; use fuel_types::{bytes::padded_len_usize, canonical::Serialize, Bytes32, ChainId, Salt}; +use itertools::Itertools; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ @@ -56,10 +57,13 @@ impl DryRunner for &T { } } +#[derive(Debug, Clone, Default)] +struct UnresolvedWitnessIndexes { + owner_to_idx_offset: HashMap, +} + #[derive(Debug, Clone, Default, Zeroize, ZeroizeOnDrop)] struct UnresolvedSignatures { - #[zeroize(skip)] - addr_idx_offset_map: HashMap, secret_keys: Vec, } @@ -68,6 +72,8 @@ pub trait BuildableTransaction { type TxType: Transaction; async fn build(self, provider: &impl DryRunner) -> Result; + + async fn build_without_signatures(self, provider: &impl DryRunner) -> Result; } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -77,6 +83,13 @@ impl BuildableTransaction for ScriptTransactionBuilder { async fn build(self, provider: &impl DryRunner) -> Result { self.build(provider).await } + + async fn build_without_signatures(mut self, provider: &impl DryRunner) -> Result { + self.set_witness_indexes(); + self.unresolved_signatures = Default::default(); + + self.build(provider).await + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -86,6 +99,13 @@ impl BuildableTransaction for CreateTransactionBuilder { async fn build(self, provider: &impl DryRunner) -> Result { self.build(provider).await } + + async fn build_without_signatures(mut self, provider: &impl DryRunner) -> Result { + self.set_witness_indexes(); + self.unresolved_signatures = Default::default(); + + self.build(provider).await + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -118,8 +138,8 @@ macro_rules! impl_tx_trait { fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey) { let index_offset = self.unresolved_signatures.secret_keys.len() as u64; self.unresolved_signatures.secret_keys.push(secret_key); - self.unresolved_signatures - .addr_idx_offset_map + self.unresolved_witness_indexes + .owner_to_idx_offset .insert(owner, index_offset); } @@ -127,7 +147,8 @@ macro_rules! impl_tx_trait { &self, provider: &impl DryRunner, ) -> Result> { - let mut tx = BuildableTransaction::build(self.clone(), provider).await?; + let mut tx = + BuildableTransaction::build_without_signatures(self.clone(), provider).await?; let consensus_parameters = provider.consensus_parameters(); if tx.is_using_predicates() { @@ -186,6 +207,21 @@ macro_rules! impl_tx_trait { } impl $ty { + fn set_witness_indexes(&mut self) { + self.unresolved_witness_indexes.owner_to_idx_offset = self + .inputs() + .iter() + .filter_map(|input| match input { + Input::ResourceSigned { resource } => Some(resource.owner()), + _ => None, + }) + .unique() + .cloned() + .enumerate() + .map(|(idx, owner)| (owner, idx as u64)) + .collect(); + } + fn generate_fuel_policies(&self, network_min_gas_price: u64) -> Policies { let mut policies = Policies::default(); policies.set(PolicyType::MaxFee, self.tx_policies.max_fee()); @@ -226,8 +262,8 @@ macro_rules! impl_tx_trait { fn calculate_witnesses_size(&self) -> Option { let witnesses_size = calculate_witnesses_size(&self.witnesses); - let signature_size = - SIGNATURE_WITNESS_SIZE * self.unresolved_signatures.secret_keys.len(); + let signature_size = SIGNATURE_WITNESS_SIZE + * self.unresolved_witness_indexes.owner_to_idx_offset.len(); Some(padded_len_usize(witnesses_size + signature_size) as u64) } @@ -244,6 +280,7 @@ pub struct ScriptTransactionBuilder { pub witnesses: Vec, pub tx_policies: TxPolicies, pub gas_estimation_tolerance: f32, + unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signatures: UnresolvedSignatures, } @@ -257,6 +294,7 @@ pub struct CreateTransactionBuilder { pub witnesses: Vec, pub tx_policies: TxPolicies, pub salt: Salt, + unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signatures: UnresolvedSignatures, } @@ -272,10 +310,7 @@ impl ScriptTransactionBuilder { 0 }; - let num_witnesses = self.num_witnesses()?; - let tx = self - .resolve_fuel_tx_provider(base_offset, num_witnesses, &provider) - .await?; + let tx = self.resolve_fuel_tx(base_offset, &provider).await?; Ok(ScriptTransaction { tx, @@ -287,7 +322,7 @@ impl ScriptTransactionBuilder { // However, the node will check if the right number of witnesses is present. // This function will create empty witnesses such that the total length matches the expected one. fn create_dry_run_witnesses(&self, num_witnesses: u8) -> Vec { - let unresolved_witnesses_len = self.unresolved_signatures.addr_idx_offset_map.len(); + let unresolved_witnesses_len = self.unresolved_witness_indexes.owner_to_idx_offset.len(); repeat_with(Default::default) .take(num_witnesses as usize + unresolved_witnesses_len) .collect() @@ -353,12 +388,12 @@ impl ScriptTransactionBuilder { Ok(()) } - async fn resolve_fuel_tx_provider( + async fn resolve_fuel_tx( self, base_offset: usize, - num_witnesses: u8, provider: &impl DryRunner, ) -> Result