Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure policies are respected #1423

Merged
merged 8 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions e2e/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1868,3 +1868,54 @@ async fn variable_output_estimation_is_optimized() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn contract_call_with_non_zero_base_asset_id_and_tip() -> Result<()> {
use fuels::prelude::*;
use fuels::tx::ConsensusParameters;

abigen!(Contract(
name = "MyContract",
abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
));

let asset_id = AssetId::new([1; 32]);

let mut consensus_parameters = ConsensusParameters::default();
consensus_parameters.set_base_asset_id(asset_id);

let config = ChainConfig {
consensus_parameters,
..Default::default()
};

let asset_base = AssetConfig {
id: asset_id,
num_coins: 1,
coin_amount: 10_000,
};

let wallet_config = WalletsConfig::new_multiple_assets(1, vec![asset_base]);
let wallets = launch_custom_provider_and_get_wallets(wallet_config, None, Some(config)).await?;
let wallet = wallets.first().expect("has wallet");

let contract_id = Contract::load_from(
"sway/contracts/contract_test/out/release/contract_test.bin",
LoadConfiguration::default(),
)?
.deploy(wallet, TxPolicies::default())
.await?;

let contract_instance = MyContract::new(contract_id, wallet.clone());

let response = contract_instance
.methods()
.initialize_counter(42)
.with_tx_policies(TxPolicies::default().with_tip(10))
.call()
.await?;

assert_eq!(42, response.value);

Ok(())
}
59 changes: 59 additions & 0 deletions e2e/tests/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,3 +1008,62 @@ async fn can_upload_executor_and_trigger_upgrade() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn tx_respects_policies() -> Result<()> {
setup_program_test!(
Wallets("wallet"),
Abigen(Contract(
name = "TestContract",
project = "e2e/sway/contracts/contract_test"
)),
Deploy(
name = "contract_instance",
contract = "TestContract",
wallet = "wallet"
),
);

let tip = 22;
let witness_limit = 1000;
let maturity = 4;
let max_fee = 10_000;
let script_gas_limit = 3000;
let tx_policies = TxPolicies::new(
Some(tip),
Some(witness_limit),
Some(maturity),
Some(max_fee),
Some(script_gas_limit),
);

// advance the block height to ensure the maturity is respected
let provider = wallet.try_provider()?;
provider.produce_blocks(4, None).await?;

// trigger a transaction that contains script code to verify
// that policies precede estimated values
let response = contract_instance
.methods()
.initialize_counter(42)
.with_tx_policies(tx_policies)
.call()
.await?;

let tx_response = provider
.get_transaction_by_id(&response.tx_id.unwrap())
.await?
.expect("tx should exist");
let script = match tx_response.transaction {
TransactionType::Script(tx) => tx,
_ => panic!("expected script transaction"),
};

assert_eq!(script.maturity(), maturity as u32);
assert_eq!(script.tip().unwrap(), tip);
assert_eq!(script.witness_limit().unwrap(), witness_limit);
assert_eq!(script.max_fee().unwrap(), max_fee);
assert_eq!(script.gas_limit(), script_gas_limit);

Ok(())
}
5 changes: 2 additions & 3 deletions e2e/tests/wallets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ async fn send_transfer_transactions() -> Result<()> {
// Configure transaction policies
let tip = 2;
let script_gas_limit = 500_000;
let expected_script_gas_limit = 0;
let maturity = 0;

let tx_policies = TxPolicies::default()
Expand All @@ -280,8 +279,8 @@ async fn send_transfer_transactions() -> Result<()> {
TransactionType::Script(tx) => tx,
_ => panic!("Received unexpected tx type!"),
};
// Transfer scripts have `script_gas_limit` set to `0`
assert_eq!(script.gas_limit(), expected_script_gas_limit);
// Transfer scripts uses set `script_gas_limit` despite not having script code
assert_eq!(script.gas_limit(), script_gas_limit);
assert_eq!(script.maturity(), maturity as u32);

let wallet_1_spendable_resources = wallet_1.get_spendable_resources(base_asset_id, 1).await?;
Expand Down
3 changes: 1 addition & 2 deletions examples/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ mod tests {
let response = contract_instance_1
.methods()
.initialize_counter(42)
.with_tx_policies(TxPolicies::default().with_script_gas_limit(1_000_000))
.call()
.await?;

Expand All @@ -259,11 +258,11 @@ mod tests {
let response = contract_instance_2
.methods()
.initialize_counter(42) // Build the ABI call
.with_tx_policies(TxPolicies::default().with_script_gas_limit(1_000_000))
.call()
.await?;

assert_eq!(42, response.value);

Ok(())
}

Expand Down
23 changes: 17 additions & 6 deletions packages/fuels-accounts/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,19 @@ impl Provider {
.collect())
}

pub async fn dry_run_no_validation(&self, tx: impl Transaction) -> Result<TxStatus> {
pub async fn dry_run_opt(
&self,
tx: impl Transaction,
utxo_validation: bool,
gas_price: Option<u64>,
) -> Result<TxStatus> {
let [tx_status] = self
.client
.dry_run_opt(Transactions::new().insert(tx).as_slice(), Some(false))
.dry_run_opt(
Transactions::new().insert(tx).as_slice(),
Some(utxo_validation),
gas_price,
)
.await?
.into_iter()
.map(Into::into)
Expand All @@ -323,13 +332,15 @@ impl Provider {
Ok(tx_status)
}

pub async fn dry_run_no_validation_multiple(
pub async fn dry_run_opt_multiple(
&self,
transactions: Transactions,
utxo_validation: bool,
gas_price: Option<u64>,
) -> Result<Vec<(TxId, TxStatus)>> {
Ok(self
.client
.dry_run_opt(transactions.as_slice(), Some(false))
.dry_run_opt(transactions.as_slice(), Some(utxo_validation), gas_price)
.await?
.into_iter()
.map(|execution_status| (execution_status.id, execution_status.into()))
Expand Down Expand Up @@ -639,7 +650,7 @@ impl Provider {
tx: T,
tolerance: f64,
) -> Result<u64> {
let receipts = self.dry_run_no_validation(tx).await?.take_receipts();
let receipts = self.dry_run_opt(tx, false, None).await?.take_receipts();
let gas_used = self.get_script_gas_used(&receipts);

Ok((gas_used as f64 * (1.0 + tolerance)) as u64)
Expand Down Expand Up @@ -707,7 +718,7 @@ impl DryRunner for Provider {
async fn dry_run(&self, tx: FuelTransaction) -> Result<DryRun> {
let [tx_execution_status] = self
.client
.dry_run_opt(&vec![tx], Some(false))
.dry_run_opt(&vec![tx], Some(false), Some(0))
.await?
.try_into()
.expect("should have only one element");
Expand Down
3 changes: 2 additions & 1 deletion packages/fuels-accounts/src/provider/retryable_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,9 @@ impl RetryableClient {
&self,
tx: &[Transaction],
utxo_validation: Option<bool>,
gas_price: Option<u64>,
) -> RequestResult<Vec<TransactionExecutionStatus>> {
self.wrap(|| self.client.dry_run_opt(tx, utxo_validation, None))
self.wrap(|| self.client.dry_run_opt(tx, utxo_validation, gas_price))
.await
}

Expand Down
46 changes: 31 additions & 15 deletions packages/fuels-core/src/types/transaction_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,16 @@ impl ScriptTransactionBuilder {
self.set_script_gas_limit(&mut script_dry_runner, &mut tx)
.await?;

Self::set_max_fee_policy(
&mut tx,
&dry_runner,
self.gas_price_estimation_block_horizon,
)
.await?;
if let Some(max_fee) = self.tx_policies.max_fee() {
tx.policies_mut().set(PolicyType::MaxFee, Some(max_fee));
} else {
Self::set_max_fee_policy(
&mut tx,
&dry_runner,
self.gas_price_estimation_block_horizon,
)
.await?;
}

self.set_witnesses(&mut tx, dry_runner).await?;

Expand All @@ -537,11 +541,11 @@ impl ScriptTransactionBuilder {
tx: &mut fuel_tx::Script,
) -> Result<()> {
let has_no_code = self.script.is_empty();
let script_gas_limit = if has_no_code {
0
} else if let Some(gas_limit) = self.tx_policies.script_gas_limit() {
let script_gas_limit = if let Some(gas_limit) = self.tx_policies.script_gas_limit() {
// Use the user defined value even if it makes the transaction revert.
gas_limit
} else if has_no_code {
0
} else {
let dry_run = if let Some(dry_run) = dry_runner.last_dry_run() {
// Even if the last dry run included variable outputs they only affect the transaction fee,
Expand Down Expand Up @@ -749,8 +753,12 @@ impl CreateTransactionBuilder {
self.witnesses,
);

Self::set_max_fee_policy(&mut tx, provider, self.gas_price_estimation_block_horizon)
.await?;
if let Some(max_fee) = self.tx_policies.max_fee() {
tx.policies_mut().set(PolicyType::MaxFee, Some(max_fee));
} else {
Self::set_max_fee_policy(&mut tx, &provider, self.gas_price_estimation_block_horizon)
.await?;
}

let missing_witnesses =
generate_missing_witnesses(tx.id(&chain_id), &self.unresolved_signers).await?;
Expand Down Expand Up @@ -847,8 +855,12 @@ impl UploadTransactionBuilder {
self.witnesses,
);

Self::set_max_fee_policy(&mut tx, provider, self.gas_price_estimation_block_horizon)
.await?;
if let Some(max_fee) = self.tx_policies.max_fee() {
tx.policies_mut().set(PolicyType::MaxFee, Some(max_fee));
} else {
Self::set_max_fee_policy(&mut tx, &provider, self.gas_price_estimation_block_horizon)
.await?;
}

let missing_witnesses =
generate_missing_witnesses(tx.id(&chain_id), &self.unresolved_signers).await?;
Expand Down Expand Up @@ -947,8 +959,12 @@ impl UpgradeTransactionBuilder {
self.witnesses,
);

Self::set_max_fee_policy(&mut tx, provider, self.gas_price_estimation_block_horizon)
.await?;
if let Some(max_fee) = self.tx_policies.max_fee() {
tx.policies_mut().set(PolicyType::MaxFee, Some(max_fee));
} else {
Self::set_max_fee_policy(&mut tx, &provider, self.gas_price_estimation_block_horizon)
.await?;
}

let missing_witnesses =
generate_missing_witnesses(tx.id(&chain_id), &self.unresolved_signers).await?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use fuel_crypto::Signature;
use fuel_tx::{
field::{Inputs, Outputs, ScriptGasLimit, WitnessLimit, Witnesses},
input::coin::{CoinPredicate, CoinSigned},
Chargeable, Input as FuelInput, TxPointer, Witness,
AssetId, Chargeable, Input as FuelInput, TxPointer, Witness,
};
use itertools::Itertools;

Expand Down Expand Up @@ -84,7 +84,11 @@ impl<R: DryRunner> ScriptDryRunner<R> {
}

fn add_fake_coins(&mut self, tx: &mut fuel_tx::Script) {
if let Some(fake_input) = Self::needs_fake_spendable_input(tx.inputs()) {
let consensus_params = self.dry_runner.consensus_parameters();

if let Some(fake_input) =
Self::needs_fake_base_input(tx.inputs(), consensus_params.base_asset_id())
{
tx.inputs_mut().push(fake_input);

// Add an empty `Witness` for the `coin_signed` we just added
Expand All @@ -93,18 +97,22 @@ impl<R: DryRunner> ScriptDryRunner<R> {
}
}

fn needs_fake_spendable_input(inputs: &[FuelInput]) -> Option<fuel_tx::Input> {
let has_spendable_input = inputs.iter().any(|i| {
matches!(
i,
FuelInput::CoinSigned(CoinSigned { .. })
| FuelInput::CoinPredicate(CoinPredicate { .. })
| FuelInput::MessageCoinSigned(_)
| FuelInput::MessageCoinPredicate(_)
)
fn needs_fake_base_input(
inputs: &[FuelInput],
base_asset_id: &AssetId,
) -> Option<fuel_tx::Input> {
let has_base_asset = inputs.iter().any(|i| match i {
FuelInput::CoinSigned(CoinSigned { asset_id, .. })
| FuelInput::CoinPredicate(CoinPredicate { asset_id, .. })
if asset_id == base_asset_id =>
{
true
}
FuelInput::MessageCoinSigned(_) | FuelInput::MessageCoinPredicate(_) => true,
_ => false,
});

if has_spendable_input {
if has_base_asset {
return None;
}

Expand All @@ -128,7 +136,7 @@ impl<R: DryRunner> ScriptDryRunner<R> {
Default::default(),
fake_owner,
1_000_000_000,
Default::default(),
*base_asset_id,
TxPointer::default(),
0,
))
Expand Down
Loading
Loading