Skip to content

Commit

Permalink
Allow signing forc-client transactions without prompting (FuelLabs#3075)
Browse files Browse the repository at this point in the history
We do not sign transactions while running the E2E tests, causing them to
have similar IDs and collide. Some randomness could be inserted to
prevent this, but signing the transactions already achieve the same
result.

The only way `forc-client` could do signing was through stdio prompts,
so support for signing with a provided key was also needed.

This PR:
- Moves tx related code used in `forc-client`'s `run` and `deploy`
commands into `tx_util.rs`.
- Adds support for signing transactions with a provided `SecretKey`
instead of prompting stdio.
- Signs transactions using `fuel-core`'s built-in key while testing.
  • Loading branch information
AlicanC authored Oct 21, 2022
1 parent faf55cb commit ea28b9c
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 356 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion forc-plugins/forc-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ description = "A `forc` plugin for interacting with a Fuel node."

[dependencies]
anyhow = "1"
async-trait = "0.1.58"
clap = { version = "3", features = ["derive", "env"] }
forc-pkg = { version = "0.26.0", path = "../../forc-pkg" }
forc-util = { version = "0.26.0", path = "../../forc-util" }
fuel-crypto = "0.6"
fuel-gql-client = { version = "0.10", default-features = false }
fuel-tx = "0.18"
fuel-tx = { version = "0.18", features = ["builder"] }
fuel-vm = "0.15"
fuels-core = "0.24"
fuels-signers = "0.24"
Expand Down
3 changes: 3 additions & 0 deletions forc-plugins/forc-client/src/ops/deploy/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::Parser;
use fuel_crypto::SecretKey;

#[derive(Debug, Default, Parser)]
#[clap(bin_name = "forc deploy", version)]
Expand Down Expand Up @@ -78,4 +79,6 @@ pub struct DeployCommand {
/// Set the transaction gas price. Defaults to 0.
#[clap(long)]
pub gas_price: Option<u64>,
/// Set the key to be used for signing.
pub signing_key: Option<SecretKey>,
}
229 changes: 38 additions & 191 deletions forc-plugins/forc-client/src/ops/deploy/op.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
use anyhow::{bail, Result};
use forc_pkg::{BuildOptions, Compiled, PackageManifestFile};
use fuel_crypto::Signature;
use forc_pkg::{BuildOptions, PackageManifestFile};
use fuel_gql_client::client::FuelClient;
use fuel_tx::{Output, Salt, StorageSlot, Transaction};
use fuel_tx::{Output, Salt, TransactionBuilder};
use fuel_vm::prelude::*;
use fuels_core::constants::BASE_ASSET_ID;
use fuels_signers::{provider::Provider, wallet::Wallet};
use fuels_types::bech32::Bech32Address;
use std::{io::Write, path::PathBuf, str::FromStr};
use std::path::PathBuf;
use sway_core::language::parsed::TreeType;
use sway_utils::constants::DEFAULT_NODE_URL;
use tracing::info;

use crate::ops::{deploy::cmd::DeployCommand, parameters::TxParameters};
use crate::ops::tx_util::{TransactionBuilderExt, TxParameters};

use super::cmd::DeployCommand;

pub async fn deploy(command: DeployCommand) -> Result<fuel_tx::ContractId> {
let curr_dir = if let Some(ref path) = command.path {
Expand All @@ -23,99 +21,46 @@ pub async fn deploy(command: DeployCommand) -> Result<fuel_tx::ContractId> {
let manifest = PackageManifestFile::from_dir(&curr_dir)?;
manifest.check_program_type(vec![TreeType::Contract])?;

let DeployCommand {
path,
print_ast,
print_finalized_asm,
print_intermediate_asm,
print_ir,
binary_outfile,
debug_outfile,
offline_mode,
terse_mode,
output_directory,
minify_json_abi,
minify_json_storage_slots,
locked,
url,
build_profile,
release,
time_phases,
unsigned,
gas_limit,
gas_price,
} = command;

let build_options = BuildOptions {
path,
print_ast,
print_finalized_asm,
print_intermediate_asm,
print_ir,
binary_outfile,
offline_mode,
debug_outfile,
terse_mode,
output_directory,
minify_json_abi,
minify_json_storage_slots,
locked,
build_profile,
release,
time_phases,
};

let compiled = forc_pkg::build_with_options(build_options)?;

let node_url = match &manifest.network {
Some(network) => &network.url,
_ => DEFAULT_NODE_URL,
};

let node_url = url.unwrap_or_else(|| node_url.to_string());

let node_url = command.url.unwrap_or_else(|| node_url.to_string());
let client = FuelClient::new(node_url)?;

let (mut tx, contract_id) = if unsigned {
create_contract_tx(
compiled.bytecode,
Vec::<fuel_tx::Input>::new(),
Vec::<fuel_tx::Output>::new(),
compiled.storage_slots,
)
} else {
let mut wallet_address = String::new();
print!(
"Please provide the address of the wallet you are going to sign this transaction with:"
);
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut wallet_address)?;
let address = Bech32Address::from_str(wallet_address.trim())?;
let locked_wallet = Wallet::from_address(address, Some(Provider::new(client.clone())));
let tx_parameters = TxParameters::new(gas_limit, gas_price);
create_signed_contract_tx(compiled, locked_wallet, tx_parameters).await?
let build_options = BuildOptions {
path: command.path,
print_ast: command.print_ast,
print_finalized_asm: command.print_finalized_asm,
print_intermediate_asm: command.print_intermediate_asm,
print_ir: command.print_ir,
binary_outfile: command.binary_outfile,
offline_mode: command.offline_mode,
debug_outfile: command.debug_outfile,
terse_mode: command.terse_mode,
output_directory: command.output_directory,
minify_json_abi: command.minify_json_abi,
minify_json_storage_slots: command.minify_json_storage_slots,
locked: command.locked,
build_profile: command.build_profile,
release: command.release,
time_phases: command.time_phases,
};
let compiled = forc_pkg::build_with_options(build_options)?;

if !unsigned {
// Ask for the signature and add it as a witness
let mut signature = String::new();
print!("Please provide the signature for this transaction:");
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut signature)?;

let signature = Signature::from_str(signature.trim())?;
let witness = vec![Witness::from(signature.as_ref())];

let mut witnesses: Vec<Witness> = tx.witnesses().to_vec();

match witnesses.len() {
0 => tx.set_witnesses(witness),
_ => {
witnesses.extend(witness);
tx.set_witnesses(witnesses)
}
}
}
let bytecode = compiled.bytecode.clone().into();
let salt = Salt::new([0; 32]);
let mut storage_slots = compiled.storage_slots;
storage_slots.sort();
let contract = Contract::from(compiled.bytecode.clone());
let root = contract.root();
let state_root = Contract::initial_state_root(storage_slots.iter());
let contract_id = contract.id(&salt, &root, &state_root);
let tx = TransactionBuilder::create(bytecode, salt, storage_slots.clone())
.params(TxParameters::new(command.gas_limit, command.gas_price))
.add_output(Output::contract_created(contract_id, state_root))
.finalize_signed(client.clone(), command.unsigned, command.signing_key)
.await?;

match client.submit(&tx).await {
Ok(logs) => {
Expand All @@ -125,101 +70,3 @@ pub async fn deploy(command: DeployCommand) -> Result<fuel_tx::ContractId> {
Err(e) => bail!("{e}"),
}
}

async fn create_signed_contract_tx(
compiled_contract: Compiled,
signer_wallet: Wallet,
tx_parameters: TxParameters,
) -> Result<(Transaction, fuel_tx::ContractId)> {
let maturity = 0;
let bytecode_witness_index = 0;
let witnesses = vec![compiled_contract.bytecode.clone().into()];

let salt = Salt::new([0; 32]);

let contract = Contract::from(compiled_contract.bytecode);
let root = contract.root();

let mut storage_slots = compiled_contract.storage_slots;
storage_slots.sort();
let state_root = Contract::initial_state_root(storage_slots.iter());
let contract_id = contract.id(&salt, &root, &state_root);
info!("Contract id: 0x{}", hex::encode(contract_id));

let outputs: Vec<Output> = vec![
Output::contract_created(contract_id, state_root),
// Note that the change will be computed by the node.
// Here we only have to tell the node who will own the change and its asset ID.
// For now we use the BASE_ASSET_ID constant
Output::change(signer_wallet.address().into(), 0, BASE_ASSET_ID),
];
let coin_witness_index = 1;

let inputs = signer_wallet
.get_asset_inputs_for_amount(AssetId::default(), 1_000_000, coin_witness_index)
.await?;
let tx = Transaction::create(
tx_parameters.gas_price,
tx_parameters.gas_limit,
maturity,
bytecode_witness_index,
salt,
storage_slots,
inputs,
outputs,
witnesses,
);

info!("Tx id to sign {}", tx.id());
Ok((tx, contract_id))
}

fn create_contract_tx(
compiled_contract: Vec<u8>,
inputs: Vec<Input>,
outputs: Vec<Output>,
storage_slots: Vec<StorageSlot>,
) -> (Transaction, fuel_tx::ContractId) {
let gas_price = 0;
let gas_limit = fuel_tx::ConsensusParameters::default().max_gas_per_tx;
let maturity = 0;
let bytecode_witness_index = 0;
let witnesses = vec![compiled_contract.clone().into()];

let salt = Salt::new([0; 32]);

let contract = Contract::from(compiled_contract);
let root = contract.root();

// The VM currently requires that storage slots are sorted but this shouldn't be neessary.
// Downstream tooling should do the sorting themselves.
// Ref: https://github.com/FuelLabs/fuel-tx/issues/153
let mut storage_slots = storage_slots;
storage_slots.sort();
let state_root = Contract::initial_state_root(storage_slots.iter());
let id = contract.id(&salt, &root, &state_root);
info!("Contract id: 0x{}", hex::encode(id));
let outputs = [
&[Output::ContractCreated {
contract_id: id,
state_root,
}],
&outputs[..],
]
.concat();

(
Transaction::create(
gas_price,
gas_limit,
maturity,
bytecode_witness_index,
salt,
storage_slots,
inputs,
outputs,
witnesses,
),
id,
)
}
2 changes: 1 addition & 1 deletion forc-plugins/forc-client/src/ops/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pub mod deploy;
pub mod parameters;
pub mod run;
pub mod tx_util;
25 changes: 0 additions & 25 deletions forc-plugins/forc-client/src/ops/parameters.rs

This file was deleted.

4 changes: 4 additions & 0 deletions forc-plugins/forc-client/src/ops/run/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::Parser;
use fuel_crypto::SecretKey;

/// Run script project.
/// Crafts a script transaction then sends it to a running node.
Expand Down Expand Up @@ -113,4 +114,7 @@ pub struct RunCommand {
/// Do not sign the transaction
#[clap(long)]
pub unsigned: bool,

/// Set the key to be used for signing.
pub signing_key: Option<SecretKey>,
}
Loading

0 comments on commit ea28b9c

Please sign in to comment.