From bc32f7a0ab27bbc98bd0d4510be7d3135c85348b Mon Sep 17 00:00:00 2001 From: hal3e Date: Mon, 15 Jan 2024 14:01:23 +0100 Subject: [PATCH] refactor!: signing and the `Signer` trait (#1241) NOTE: If the user builds without signatures, it is their responsibility to sign the transaction in the right order. For example, if we have `CoinSignedA` then `CoinSignedB`, we need to sign with `WalletA` and then with `WalletB`. This comes from the fact that we need to set the witness indexes while building the transaction. BREAKING CHANGE: - Removed `sign_message` and `sign_transaction` from the `Signer` trait - Added `sign` and `address` methods to the `Signer` trait - `Signer` trait moved do `fuels::core::traits:::Signer` - `Message`, `PublicKey`, `SecretKey` and `Signature` moved to `fuels::crypto::` - Replaced `Transaction`'s `check_without_signatures` with `check` - Renamed `Account`s `add_witnessses` to `add_witnesses` - Removed `Clone` for `TransactionBuilder`s --- .../transaction-builders.md | 18 +- docs/src/wallets/signing.md | 20 +- examples/contracts/src/lib.rs | 2 +- examples/cookbook/src/lib.rs | 13 +- examples/predicates/src/lib.rs | 23 +- examples/providers/src/lib.rs | 2 +- examples/wallets/src/lib.rs | 2 +- packages/fuels-accounts/src/account.rs | 62 +-- packages/fuels-accounts/src/provider.rs | 2 +- packages/fuels-accounts/src/wallet.rs | 45 +- packages/fuels-core/Cargo.toml | 2 +- packages/fuels-core/src/traits.rs | 2 + packages/fuels-core/src/traits/signer.rs | 14 + packages/fuels-core/src/types/errors.rs | 7 +- .../src/types/transaction_builders.rs | 383 ++++++++++++++---- .../src/types/wrappers/transaction.rs | 42 +- packages/fuels-core/src/utils.rs | 4 +- 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-test-helpers/Cargo.toml | 1 + packages/fuels-test-helpers/src/accounts.rs | 8 +- packages/fuels/Cargo.toml | 2 + packages/fuels/src/lib.rs | 11 +- packages/fuels/tests/contracts.rs | 47 ++- packages/fuels/tests/predicates.rs | 6 +- packages/fuels/tests/providers.rs | 28 +- packages/fuels/tests/scripts.rs | 2 +- packages/fuels/tests/wallets.rs | 6 +- 29 files changed, 545 insertions(+), 218 deletions(-) create mode 100644 packages/fuels-core/src/traits/signer.rs diff --git a/docs/src/custom-transactions/transaction-builders.md b/docs/src/custom-transactions/transaction-builders.md index be821b518..de8eaffba 100644 --- a/docs/src/custom-transactions/transaction-builders.md +++ b/docs/src/custom-transactions/transaction-builders.md @@ -52,20 +52,22 @@ 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: +As we have used coins that require a signature, we have to add the signer to the transaction builder with: ```rust,ignore -{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_sign}} +{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_add_signer}} ``` > **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. +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. ```rust,ignore {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_adjust}} ``` +> **Note** It is recommended to add signers before calling `adjust_for_fee()` as the estimation will include the size of the witnesses. + We can also define transaction policies. For example, we can limit the gas price by doing the following: ```rust,ignore @@ -83,3 +85,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, which is 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 adding signers to a transaction builder, when signing a built transaction, you must ensure that the order of signatures matches the order of signed inputs. Multiple signed inputs with the same owner will have the same witness index. diff --git a/docs/src/wallets/signing.md b/docs/src/wallets/signing.md index b0144e1c0..33f54fcfd 100644 --- a/docs/src/wallets/signing.md +++ b/docs/src/wallets/signing.md @@ -1,19 +1,27 @@ # Signing -Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with `wallet.sign_message`. Below is a full example of how to sign and recover a message. +Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with `wallet.sign`. Below is a full example of how to sign and recover a message. ```rust,ignore {{#include ../../../packages/fuels-accounts/src/account.rs:sign_message}} ``` -## Signing a transaction +## Adding `Signers` to a transaction builder -Every signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signatures in the transaction builder and resolve the final transaction automatically. This is done by storing the secret keys of all signers until the final transaction is built. +Every signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signers in the transaction builder and resolve the final transaction automatically. This is done by storing signers until the final transaction is built. -To sign a _transaction builder_ use the `wallet.sign_transaction`. Below is a full example of how to create a transaction and sign it. +Below is a full example of how to create a transaction builder and add signers to it. -> Note: When you sign a transaction builder the secret key is stored inside it and will not be resolved until you call `build()`! +> Note: When you add a `Signer` to a transaction builder, the signer is stored inside it and the transaction will not be resolved until you call `build()`! ```rust,ignore -{{#include ../../../packages/fuels-accounts/src/account.rs:sign_tx}} +{{#include ../../../packages/fuels-accounts/src/account.rs:sign_tb}} +``` + +## Signing a built transaction + +If you have a built transaction and want to add a signature, you can use the `sign_with` method. + +```rust,ignore +{{#include ../../../packages/fuels/tests/contracts.rs:tx_sign_with}} ``` diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index 4296f7d48..dfef678a0 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -899,7 +899,7 @@ mod tests { // customize the builder... wallet.adjust_for_fee(&mut tb, 0).await?; - wallet.sign_transaction(&mut tb); + tb.add_signer(wallet.clone())?; let tx = tb.build(provider).await?; diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index 8a412d25a..d37d1a286 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -3,9 +3,7 @@ mod tests { use std::str::FromStr; use fuels::{ - accounts::{ - predicate::Predicate, wallet::WalletUnlocked, Account, Signer, ViewOnlyAccount, - }, + accounts::{predicate::Predicate, wallet::WalletUnlocked, Account, ViewOnlyAccount}, core::constants::BASE_ASSET_ID, prelude::Result, test_helpers::{setup_single_asset_coins, setup_test_provider}, @@ -191,7 +189,8 @@ mod tests { // ANCHOR: transfer_multiple_transaction let mut tb = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default()); - wallet_1.sign_transaction(&mut tb); + tb.add_signer(wallet_1.clone())?; + let tx = tb.build(&provider).await?; provider.send_transaction_and_await_commit(tx).await?; @@ -296,9 +295,9 @@ 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_add_signer + tb.add_signer(hot_wallet.clone())?; + // ANCHOR_END: custom_tx_add_signer // ANCHOR: custom_tx_adjust hot_wallet.adjust_for_fee(&mut tb, 100).await?; diff --git a/examples/predicates/src/lib.rs b/examples/predicates/src/lib.rs index a07385389..b91d7927a 100644 --- a/examples/predicates/src/lib.rs +++ b/examples/predicates/src/lib.rs @@ -2,14 +2,13 @@ mod tests { use fuels::{ accounts::{predicate::Predicate, Account}, + crypto::{Message, SecretKey}, prelude::*, types::B512, }; #[tokio::test] async fn predicate_example() -> Result<()> { - use fuels::accounts::fuel_crypto::SecretKey; - // ANCHOR: predicate_wallets let secret_key1: SecretKey = "0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301" @@ -53,22 +52,10 @@ mod tests { }); // ANCHOR_END: predicate_coins - let data_to_sign = [0; 32]; - let signature1: B512 = wallet - .sign_message(data_to_sign) - .await? - .as_ref() - .try_into()?; - let signature2: B512 = wallet2 - .sign_message(data_to_sign) - .await? - .as_ref() - .try_into()?; - let signature3: B512 = wallet3 - .sign_message(data_to_sign) - .await? - .as_ref() - .try_into()?; + let data_to_sign = Message::new([0; 32]); + let signature1: B512 = wallet.sign(data_to_sign).await?.as_ref().try_into()?; + let signature2: B512 = wallet2.sign(data_to_sign).await?.as_ref().try_into()?; + let signature3: B512 = wallet3.sign(data_to_sign).await?.as_ref().try_into()?; let signatures = [signature1, signature2, signature3]; diff --git a/examples/providers/src/lib.rs b/examples/providers/src/lib.rs index 7b9758d1f..f8e9f9d2a 100644 --- a/examples/providers/src/lib.rs +++ b/examples/providers/src/lib.rs @@ -10,7 +10,7 @@ mod tests { // ANCHOR: connect_to_testnet use std::str::FromStr; - use fuels::{accounts::fuel_crypto::SecretKey, prelude::*}; + use fuels::{crypto::SecretKey, prelude::*}; // Create a provider pointing to the testnet. // This example will not work as the testnet does not support the new version of fuel-core diff --git a/examples/wallets/src/lib.rs b/examples/wallets/src/lib.rs index 9c05df30a..ec263f21f 100644 --- a/examples/wallets/src/lib.rs +++ b/examples/wallets/src/lib.rs @@ -23,7 +23,7 @@ mod tests { // ANCHOR: create_wallet_from_secret_key use std::str::FromStr; - use fuels::{accounts::fuel_crypto::SecretKey, prelude::*}; + use fuels::{crypto::SecretKey, prelude::*}; // Use the test helper to setup a test provider. let provider = setup_test_provider(vec![], vec![], None, None).await?; diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index 1c0b1fceb..54061a593 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -2,9 +2,6 @@ use std::{collections::HashMap, fmt::Display}; use async_trait::async_trait; use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest}; -#[doc(no_inline)] -pub use fuel_crypto; -use fuel_crypto::Signature; use fuel_tx::{Output, Receipt, TxId, TxPointer, UtxoId}; use fuel_types::{AssetId, Bytes32, ContractId, Nonce}; use fuels_core::{ @@ -29,23 +26,6 @@ use crate::{ provider::{Provider, ResourceFilter}, }; -/// Trait for signing transactions and messages -/// -/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait Signer: std::fmt::Debug + Send + Sync { - type Error: std::error::Error + Send + Sync; - - async fn sign_message>( - &self, - message: S, - ) -> std::result::Result; - - /// Signs the transaction - fn sign_transaction(&self, message: &mut impl TransactionBuilder); -} - #[derive(Debug)] pub struct AccountError(String); @@ -192,7 +172,9 @@ pub trait Account: ViewOnlyAccount { } // Add signatures to the builder if the underlying account is a wallet - fn add_witnessses(&self, _tb: &mut Tb) {} + fn add_witnesses(&self, _tb: &mut Tb) -> Result<()> { + Ok(()) + } /// Transfer funds from this account to another `Address`. /// Fails if amount for asset ID is larger than address's spendable coins. @@ -212,7 +194,7 @@ pub trait Account: ViewOnlyAccount { let mut tx_builder = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_policies); - self.add_witnessses(&mut tx_builder); + self.add_witnesses(&mut tx_builder)?; let used_base_amount = if asset_id == AssetId::BASE { amount } else { 0 }; self.adjust_for_fee(&mut tx_builder, used_base_amount) @@ -274,8 +256,9 @@ pub trait Account: ViewOnlyAccount { tx_policies, ); - self.add_witnessses(&mut tb); + self.add_witnesses(&mut tb)?; self.adjust_for_fee(&mut tb, balance).await?; + let tx = tb.build(provider).await?; let tx_id = tx.id(provider.chain_id()); @@ -308,8 +291,9 @@ pub trait Account: ViewOnlyAccount { tx_policies, ); - self.add_witnessses(&mut tb); + self.add_witnesses(&mut tb)?; self.adjust_for_fee(&mut tb, amount).await?; + let tx = tb.build(provider).await?; let tx_id = tx.id(provider.chain_id()); @@ -328,9 +312,12 @@ pub trait Account: ViewOnlyAccount { mod tests { use std::str::FromStr; - use fuel_crypto::{Message, SecretKey}; + use fuel_crypto::{Message, SecretKey, Signature}; use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction}; - use fuels_core::types::{transaction::Transaction, transaction_builders::DryRunner}; + use fuels_core::{ + traits::Signer, + types::{transaction::Transaction, transaction_builders::DryRunner}, + }; use rand::{rngs::StdRng, RngCore, SeedableRng}; use super::*; @@ -351,15 +338,13 @@ mod tests { // Create a wallet using the private key created above. let wallet = WalletUnlocked::new_from_private_key(secret, None); - let message = "my message"; - - let signature = wallet.sign_message(message).await?; + let message = Message::new("my message".as_bytes()); + let signature = wallet.sign(message).await?; // Check if signature is what we expect it to be assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?); // Recover address that signed the message - let message = Message::new(message); let recovered_address = signature.recover(&message)?; assert_eq!(wallet.address().hash(), recovered_address.hash()); @@ -371,6 +356,7 @@ mod tests { Ok(()) } + #[derive(Default)] struct MockDryRunner { c_param: ConsensusParameters, } @@ -390,7 +376,7 @@ mod tests { #[tokio::test] async fn sign_tx_and_verify() -> std::result::Result<(), Box> { - // ANCHOR: sign_tx + // ANCHOR: sign_tb let secret = SecretKey::from_str( "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1", )?; @@ -421,15 +407,11 @@ mod tests { ) }; - // Sign the transaction - wallet.sign_transaction(&mut tb); // Add the private key to the transaction builder - // ANCHOR_END: sign_tx + // Add `Signer` to the transaction builder + tb.add_signer(wallet.clone())?; + // ANCHOR_END: sign_tb - 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())?; @@ -437,7 +419,7 @@ mod tests { // Sign the transaction manually let message = Message::from_bytes(*tx.id(0.into())); - let signature = Signature::sign(&wallet.private_key, &message); + let signature = wallet.sign(message).await?; // Check if the signatures are the same assert_eq!(signature, tx_signature); diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 8262bf802..ec68195ef 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -221,7 +221,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..b17eef2a8 100644 --- a/packages/fuels-accounts/src/wallet.rs +++ b/packages/fuels-accounts/src/wallet.rs @@ -4,12 +4,15 @@ use async_trait::async_trait; use elliptic_curve::rand_core; use eth_keystore::KeystoreError; use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; -use fuels_core::types::{ - bech32::{Bech32Address, FUEL_BECH32_HRP}, - errors::{Error, Result}, - input::Input, - transaction_builders::TransactionBuilder, - AssetId, +use fuels_core::{ + traits::Signer, + types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + AssetId, + }, }; use rand::{CryptoRng, Rng}; use thiserror::Error; @@ -17,7 +20,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ provider::{Provider, ProviderError}, - Account, AccountError, AccountResult, Signer, ViewOnlyAccount, + Account, AccountError, AccountResult, ViewOnlyAccount, }; pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'"; @@ -224,6 +227,10 @@ impl WalletUnlocked { .expect("Decrypted key should have a correct size"); Ok(Self::new_from_private_key(secret_key, provider)) } + + pub fn address(&self) -> &Bech32Address { + &self.address + } } impl ViewOnlyAccount for WalletUnlocked { @@ -255,26 +262,24 @@ impl Account for WalletUnlocked { .collect::>()) } - fn add_witnessses(&self, tb: &mut Tb) { - self.sign_transaction(tb); + fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { + tb.add_signer(self.clone())?; + + Ok(()) } } #[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 { - let message = Message::new(message); + async fn sign(&self, message: Message) -> Result { 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 address(&self) -> &Bech32Address { + &self.address } } @@ -314,15 +319,15 @@ mod tests { let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?; // sign a message using the above key. - let message = "Hello there!"; - let signature = wallet.sign_message(message).await?; + let message = Message::new("Hello there!".as_bytes()); + let signature = wallet.sign(message).await?; // Read from the encrypted JSON keystore and decrypt it. let path = Path::new(dir.path()).join(uuid); let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?; // Sign the same message as before and assert that the signature is the same. - let signature2 = recovered_wallet.sign_message(message).await?; + let signature2 = recovered_wallet.sign(message).await?; assert_eq!(signature, signature2); // Remove tempdir. diff --git a/packages/fuels-core/Cargo.toml b/packages/fuels-core/Cargo.toml index 0bfaafa6b..48531b88c 100644 --- a/packages/fuels-core/Cargo.toml +++ b/packages/fuels-core/Cargo.toml @@ -29,10 +29,10 @@ serde_json = { workspace = true, default-features = true } sha2 = { workspace = true } thiserror = { workspace = true, default-features = false } uint = { workspace = true, default-features = false } -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/traits.rs b/packages/fuels-core/src/traits.rs index fa9fe98b4..0d394ba76 100644 --- a/packages/fuels-core/src/traits.rs +++ b/packages/fuels-core/src/traits.rs @@ -1,5 +1,7 @@ mod parameterize; +mod signer; mod tokenizable; pub use parameterize::*; +pub use signer::*; pub use tokenizable::*; diff --git a/packages/fuels-core/src/traits/signer.rs b/packages/fuels-core/src/traits/signer.rs new file mode 100644 index 000000000..6172dbc14 --- /dev/null +++ b/packages/fuels-core/src/traits/signer.rs @@ -0,0 +1,14 @@ +use async_trait::async_trait; +use fuel_crypto::{Message, Signature}; + +use crate::types::{bech32::Bech32Address, errors::Result}; + +/// Trait for signing transactions and messages +/// +/// Implement this trait to support different signing modes, e.g. hardware wallet, hosted etc. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Signer: 'static { + async fn sign(&self, message: Message) -> Result; + fn address(&self) -> &Bech32Address; +} 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..9eab467aa 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -1,10 +1,14 @@ #![cfg(feature = "std")] -use std::{collections::HashMap, iter::repeat_with}; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + iter::repeat, +}; use async_trait::async_trait; use fuel_asm::{op, GTFArgs, RegId}; -use fuel_crypto::{Message as CryptoMessage, SecretKey, Signature}; +use fuel_crypto::{Message as CryptoMessage, Signature}; use fuel_tx::{ field::{Inputs, WitnessLimit, Witnesses}, policies::{Policies, PolicyType}, @@ -13,11 +17,12 @@ use fuel_tx::{ Witness, }; use fuel_types::{bytes::padded_len_usize, canonical::Serialize, Bytes32, ChainId, Salt}; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use itertools::Itertools; use crate::{ constants::{BASE_ASSET_ID, SIGNATURE_WITNESS_SIZE, WITNESS_STATIC_SIZE, WORD_SIZE}, offsets, + traits::Signer, types::{ bech32::Bech32Address, coin::Coin, @@ -56,11 +61,9 @@ impl DryRunner for &T { } } -#[derive(Debug, Clone, Default, Zeroize, ZeroizeOnDrop)] -struct UnresolvedSignatures { - #[zeroize(skip)] - addr_idx_offset_map: HashMap, - secret_keys: Vec, +#[derive(Debug, Clone, Default)] +struct UnresolvedWitnessIndexes { + owner_to_idx_offset: HashMap, } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -68,6 +71,11 @@ pub trait BuildableTransaction { type TxType: Transaction; async fn build(self, provider: &impl DryRunner) -> Result; + + /// Building without signatures will set the witness indexes of signed coins in the + /// order as they appear in the inputs. Multiple coins with the same owner will have + /// the same witness index. Make sure you sign the built transaction in the expected order. + async fn build_without_signatures(self, provider: &impl DryRunner) -> Result; } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -77,6 +85,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_signers = Default::default(); + + self.build(provider).await + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -86,13 +101,20 @@ 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_signers = Default::default(); + + self.build(provider).await + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait TransactionBuilder: BuildableTransaction + Send + Clone { +pub trait TransactionBuilder: BuildableTransaction + Send { type TxType: Transaction; - fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey); + fn add_signer(&mut self, signer: impl Signer + Send + Sync) -> Result<&mut Self>; async fn fee_checked_from_tx( &self, provider: &impl DryRunner, @@ -115,19 +137,45 @@ macro_rules! impl_tx_trait { impl TransactionBuilder for $ty { type TxType = $tx_ty; - 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 - .insert(owner, index_offset); + fn add_signer(&mut self, signer: impl Signer + Send + Sync) -> Result<&mut Self> { + let address = signer.address(); + if self + .unresolved_witness_indexes + .owner_to_idx_offset + .contains_key(address) + { + return Err(error!( + InvalidData, + "Already added `Signer` with address: `{address}`" + )); + } + + let index_offset = self.unresolved_signers.len() as u64; + self.unresolved_witness_indexes + .owner_to_idx_offset + .insert(address.clone(), index_offset); + self.unresolved_signers.push(Box::new(signer)); + + Ok(self) } async fn fee_checked_from_tx( &self, provider: &impl DryRunner, ) -> Result> { - let mut tx = BuildableTransaction::build(self.clone(), provider).await?; + let mut fee_estimation_tb = self.clone_without_signers(); + + // Add a temporary witness for every `Signer` to include them in the fee + // estimation. + let witness: Witness = Signature::default().as_ref().into(); + fee_estimation_tb + .witnesses_mut() + .extend(repeat(witness).take(self.unresolved_signers.len())); + + let mut tx = + BuildableTransaction::build_without_signatures(fee_estimation_tb, provider) + .await?; + let consensus_parameters = provider.consensus_parameters(); if tx.is_using_predicates() { @@ -141,8 +189,10 @@ macro_rules! impl_tx_trait { )) } - fn with_tx_policies(self, tx_policies: TxPolicies) -> Self { - self.with_tx_policies(tx_policies) + fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { + self.tx_policies = tx_policies; + + self } fn with_inputs(mut self, inputs: Vec) -> Self { @@ -186,6 +236,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()); @@ -214,7 +279,7 @@ macro_rules! impl_tx_trait { fn num_witnesses(&self) -> Result { let num_witnesses = self.witnesses().len(); - if num_witnesses + self.unresolved_signatures.secret_keys.len() > 256 { + if num_witnesses + self.unresolved_signers.len() > 256 { return Err(error!( InvalidData, "tx can not have more than 256 witnesses" @@ -226,8 +291,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) } @@ -235,7 +300,15 @@ macro_rules! impl_tx_trait { }; } -#[derive(Debug, Clone, Default)] +impl Debug for dyn Signer + Send + Sync { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signer") + .field("address", &self.address()) + .finish() + } +} + +#[derive(Debug, Default)] pub struct ScriptTransactionBuilder { pub script: Vec, pub script_data: Vec, @@ -244,10 +317,11 @@ pub struct ScriptTransactionBuilder { pub witnesses: Vec, pub tx_policies: TxPolicies, pub gas_estimation_tolerance: f32, - unresolved_signatures: UnresolvedSignatures, + unresolved_witness_indexes: UnresolvedWitnessIndexes, + unresolved_signers: Vec>, } -#[derive(Debug, Clone, Default)] +#[derive(Default)] pub struct CreateTransactionBuilder { pub bytecode_length: u64, pub bytecode_witness_index: u8, @@ -257,7 +331,8 @@ pub struct CreateTransactionBuilder { pub witnesses: Vec, pub tx_policies: TxPolicies, pub salt: Salt, - unresolved_signatures: UnresolvedSignatures, + unresolved_witness_indexes: UnresolvedWitnessIndexes, + unresolved_signers: Vec>, } impl_tx_trait!(ScriptTransactionBuilder, ScriptTransaction); @@ -272,10 +347,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, @@ -285,10 +357,12 @@ impl ScriptTransactionBuilder { // When dry running a tx with `utxo_validation` off, the node will not validate signatures. // 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. + // This function will create witnesses from a default `Signature` such that the total length matches the expected one. + // Using a `Signature` ensures that the calculated fee includes the fee generated by the witnesses. fn create_dry_run_witnesses(&self, num_witnesses: u8) -> Vec { - let unresolved_witnesses_len = self.unresolved_signatures.addr_idx_offset_map.len(); - repeat_with(Default::default) + let unresolved_witnesses_len = self.unresolved_witness_indexes.owner_to_idx_offset.len(); + let witness: Witness = Signature::default().as_ref().into(); + repeat(witness) .take(num_witnesses as usize + unresolved_witnesses_len) .collect() } @@ -353,12 +427,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