Skip to content

Commit

Permalink
feat!: var output estimation optimization (#1393) (#1418)
Browse files Browse the repository at this point in the history
closes: #1380
The number of variable outputs is now determined by filling the dry-run
tx with as much variable outputs as the node will allow and counting the number of `TransferOut` receipts made
by the dry run.
---------

Co-authored-by: Ahmed Sagdati <[email protected]>
Co-authored-by: Oleksii Filonenko <[email protected]>
  • Loading branch information
3 people authored Jun 12, 2024
1 parent 35c3be4 commit 30221bd
Show file tree
Hide file tree
Showing 18 changed files with 517 additions and 339 deletions.
2 changes: 1 addition & 1 deletion docs/src/calling-contracts/tx-dependency-estimation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The following example uses a contract call that calls an external contract and l
{{#include ../../../examples/contracts/src/lib.rs:dependency_estimation_fail}}
```

As mentioned in previous chapters, you can specify the external contract with `.with_contracts()` and add an output variable with `append_variable_outputs()` to resolve this:
As mentioned in previous chapters, you can specify the external contract and add an output variable to resolve this:

```rust,ignore
{{#include ../../../examples/contracts/src/lib.rs:dependency_estimation_manual}}
Expand Down
12 changes: 6 additions & 6 deletions docs/src/calling-contracts/variable-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ Let's say you deployed a contract with the following method:
{{#include ../../../e2e/sway/contracts/token_ops/src/main.sw:variable_outputs}}
```

When calling `transfer_coins_to_output` with the SDK, you can specify the number of variable outputs by chaining `append_variable_outputs(amount)` to your call. Like this:
When calling `transfer_coins_to_output` with the SDK, you can specify the number of variable outputs:

```rust,ignore
{{#include ../../../examples/contracts/src/lib.rs:variable_outputs}}
```

<!-- This section should explain what the `append_variable_outputs` method does -->
<!-- append_variable_outputs:example:start -->
`append_variable_outputs` effectively appends a given amount of `Output::Variable`s to the transaction's list of outputs. This output type indicates that the amount and the owner may vary based on transaction execution.
<!-- append_variable_outputs:example:end -->
<!-- This section should explain what the `with_variable_output_policy` method does -->
<!-- with_variable_output_policy:example:start -->
`with_variable_output_policy` sets the policy regarding variable outputs. You can either set the number of variable outputs yourself by providing `VariableOutputPolicy::Exactly(n)` or let the SDK estimate it for you with `VariableOutputPolicy::EstimateMinimum`. A variable output indicates that the amount and the owner may vary based on transaction execution.
<!-- with_variable_output_policy:example:end -->

> **Note:** that the Sway `lib-std` function `mint_to_address` calls `transfer_to_address` under the hood, so you need to call `append_variable_outputs` in the Rust SDK tests like you would for `transfer_to_address`.
> **Note:** that the Sway `lib-std` function `mint_to_address` calls `transfer_to_address` under the hood, so you need to call `with_variable_output_policy` in the Rust SDK tests like you would for `transfer_to_address`.
1 change: 1 addition & 0 deletions e2e/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
'sway/contracts/storage',
'sway/contracts/token_ops',
'sway/contracts/transaction_block_height',
'sway/contracts/var_outputs',
'sway/logs/contract_logs',
'sway/logs/contract_logs_abi',
'sway/logs/contract_with_contract_logs',
Expand Down
5 changes: 5 additions & 0 deletions e2e/sway/contracts/var_outputs/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "var_outputs"
15 changes: 15 additions & 0 deletions e2e/sway/contracts/var_outputs/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
contract;

abi MyContract {
fn mint(coins: u64, recipient: Identity);
}

impl MyContract for Contract {
fn mint(coins: u64, recipient: Identity) {
let mut counter = 0;
while counter < coins {
counter += 1;
std::asset::mint_to(recipient, std::constants::ZERO_B256, 1);
}
}
}
96 changes: 46 additions & 50 deletions e2e/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use fuels::{
tx::ContractParameters,
types::{errors::transaction::Reason, Bits256, Identity},
};
use tokio::time::Instant;

#[tokio::test]
async fn test_multiple_args() -> Result<()> {
Expand Down Expand Up @@ -699,25 +700,11 @@ async fn test_output_variable_estimation() -> Result<()> {
));
}

{
// Should fail due to insufficient attempts (needs at least 3)
let response = contract_methods
.mint_to_addresses(amount, addresses)
.estimate_tx_dependencies(Some(2))
.await;

assert!(matches!(
response,
Err(Error::Transaction(Reason::Reverted { .. }))
));
}

{
// Should add 3 output variables automatically
let _ = contract_methods
.mint_to_addresses(amount, addresses)
.estimate_tx_dependencies(Some(3))
.await?
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await?;

Expand All @@ -730,35 +717,6 @@ async fn test_output_variable_estimation() -> Result<()> {
Ok(())
}

#[tokio::test]
async fn test_output_variable_estimation_default_attempts() -> Result<()> {
abigen!(Contract(
name = "MyContract",
abi = "e2e/sway/contracts/token_ops/out/release/token_ops-abi.json"
));

let (wallets, addresses, mint_asset_id, contract_id) =
setup_output_variable_estimation_test().await?;

let contract_instance = MyContract::new(contract_id, wallets[0].clone());
let contract_methods = contract_instance.methods();
let amount = 1000;

let _ = contract_methods
.mint_to_addresses(amount, addresses)
.estimate_tx_dependencies(None)
.await?
.call()
.await?;

for wallet in wallets.iter() {
let balance = wallet.get_asset_balance(&mint_asset_id).await?;
assert_eq!(balance, amount);
}

Ok(())
}

#[tokio::test]
async fn test_output_variable_estimation_multicall() -> Result<()> {
abigen!(Contract(
Expand Down Expand Up @@ -796,8 +754,7 @@ async fn test_output_variable_estimation_multicall() -> Result<()> {
multi_call_handler.add_call(call_handler);

let _ = multi_call_handler
.estimate_tx_dependencies(None)
.await?
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call::<((), (), ())>()
.await?;

Expand Down Expand Up @@ -932,7 +889,7 @@ async fn test_contract_set_estimation() -> Result<()> {
let res = contract_caller_instance
.methods()
.increment_from_contract(lib_contract_id, 42)
.estimate_tx_dependencies(None)
.determine_missing_contracts(None)
.await?
.call()
.await?;
Expand Down Expand Up @@ -995,7 +952,7 @@ async fn test_output_variable_contract_id_estimation_multicall() -> Result<()> {
multi_call_handler.add_call(call_handler);

let call_response = multi_call_handler
.estimate_tx_dependencies(None)
.determine_missing_contracts(None)
.await?
.call::<(u64, u64, u64, u64)>()
.await?;
Expand Down Expand Up @@ -1262,7 +1219,7 @@ async fn low_level_call() -> Result<()> {
Bytes(function_selector),
Bytes(call_data),
)
.estimate_tx_dependencies(None)
.determine_missing_contracts(None)
.await?
.call()
.await?;
Expand Down Expand Up @@ -1290,7 +1247,7 @@ async fn low_level_call() -> Result<()> {
Bytes(function_selector),
Bytes(call_data),
)
.estimate_tx_dependencies(None)
.determine_missing_contracts(None)
.await?
.call()
.await?;
Expand Down Expand Up @@ -1872,3 +1829,42 @@ async fn msg_sender_gas_estimation_issue() {
.await
.unwrap();
}

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

let contract_methods = contract_instance.methods();

let coins = 252;
let recipient = Identity::Address(wallet.address().into());
let start = Instant::now();
let _ = contract_methods
.mint(coins, recipient)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await?;

// using `fuel-core-lib` in debug builds is 20x slower so we won't validate in that case so we
// don't have to maintain two expectations
if !cfg!(all(debug_assertions, feature = "fuel-core-lib")) {
let elapsed = start.elapsed().as_secs();
let limit = 2;
if elapsed > limit {
panic!("Estimation took too long ({elapsed}). Limit is {limit}");
}
}

Ok(())
}
2 changes: 1 addition & 1 deletion e2e/tests/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ async fn test_amount_and_asset_forwarding() -> Result<()> {
// withdraw some tokens to wallet
contract_methods
.transfer(1_000_000, asset_id, address.into())
.append_variable_outputs(1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await?;

Expand Down
3 changes: 1 addition & 2 deletions e2e/tests/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ async fn test_output_variable_estimation() -> Result<()> {
let inputs = wallet.get_asset_inputs_for_amount(asset_id, amount).await?;
let _ = script_call
.with_inputs(inputs)
.estimate_tx_dependencies(None)
.await?
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await?;

Expand Down
9 changes: 5 additions & 4 deletions examples/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ mod tests {
// withdraw some tokens to wallet
let response = contract_methods
.transfer(1_000_000, asset_id, address.into())
.append_variable_outputs(1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await?;
// ANCHOR_END: variable_outputs
Expand Down Expand Up @@ -422,7 +422,7 @@ mod tests {
// ANCHOR: dependency_estimation_manual
let response = contract_methods
.mint_then_increment_from_contract(called_contract_id, amount, address.into())
.append_variable_outputs(1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.with_contract_ids(&[called_contract_id.into()])
.call()
.await?;
Expand All @@ -435,7 +435,8 @@ mod tests {
// ANCHOR: dependency_estimation
let response = contract_methods
.mint_then_increment_from_contract(called_contract_id, amount, address.into())
.estimate_tx_dependencies(Some(2))
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.determine_missing_contracts(Some(2))
.await?
.call()
.await?;
Expand Down Expand Up @@ -736,7 +737,7 @@ mod tests {
Bytes(function_selector),
Bytes(call_data),
)
.estimate_tx_dependencies(None)
.determine_missing_contracts(None)
.await?
.call()
.await?;
Expand Down
4 changes: 2 additions & 2 deletions examples/cookbook/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ mod tests {
contract_methods
.deposit(wallet.address().into())
.call_params(call_params)?
.append_variable_outputs(1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await?;
// ANCHOR_END: liquidity_deposit
Expand All @@ -86,7 +86,7 @@ mod tests {
contract_methods
.withdraw(wallet.address().into())
.call_params(call_params)?
.append_variable_outputs(1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await?;

Expand Down
13 changes: 10 additions & 3 deletions packages/fuels-accounts/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ mod tests {
use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction};
use fuels_core::{
traits::Signer,
types::{transaction::Transaction, transaction_builders::DryRunner},
types::{
transaction::Transaction,
transaction_builders::{DryRun, DryRunner},
},
};
use rand::{rngs::StdRng, RngCore, SeedableRng};

Expand Down Expand Up @@ -334,8 +337,12 @@ mod tests {

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl DryRunner for MockDryRunner {
async fn dry_run_and_get_used_gas(&self, _: FuelTransaction, _: f32) -> Result<u64> {
Ok(0)
async fn dry_run(&self, _: FuelTransaction) -> Result<DryRun> {
Ok(DryRun {
succeeded: true,
script_gas: 0,
variable_outputs: 0,
})
}

fn consensus_parameters(&self) -> &ConsensusParameters {
Expand Down
36 changes: 29 additions & 7 deletions packages/fuels-accounts/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ use fuel_core_client::client::{
gas_price::{EstimateGasPrice, LatestGasPrice},
},
};
use fuel_core_types::blockchain::header::LATEST_STATE_TRANSITION_VERSION;
use fuel_core_types::{
blockchain::header::LATEST_STATE_TRANSITION_VERSION,
services::executor::TransactionExecutionResult,
};
use fuel_tx::{
AssetId, ConsensusParameters, Receipt, Transaction as FuelTransaction, TxId, UtxoId,
};
Expand All @@ -37,7 +40,7 @@ use fuels_core::{
message_proof::MessageProof,
node_info::NodeInfo,
transaction::{Transaction, Transactions},
transaction_builders::DryRunner,
transaction_builders::{DryRun, DryRunner},
transaction_response::TransactionResponse,
tx_status::TxStatus,
},
Expand Down Expand Up @@ -637,12 +640,12 @@ impl Provider {
tolerance: f64,
) -> Result<u64> {
let receipts = self.dry_run_no_validation(tx).await?.take_receipts();
let gas_used = self.get_gas_used(&receipts);
let gas_used = self.get_script_gas_used(&receipts);

Ok((gas_used as f64 * (1.0 + tolerance)) as u64)
}

fn get_gas_used(&self, receipts: &[Receipt]) -> u64 {
fn get_script_gas_used(&self, receipts: &[Receipt]) -> u64 {
receipts
.iter()
.rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
Expand Down Expand Up @@ -701,17 +704,36 @@ impl Provider {

#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl DryRunner for Provider {
async fn dry_run_and_get_used_gas(&self, tx: FuelTransaction, tolerance: f32) -> Result<u64> {
async fn dry_run(&self, tx: FuelTransaction) -> Result<DryRun> {
let [tx_execution_status] = self
.client
.dry_run_opt(&vec![tx], Some(false))
.await?
.try_into()
.expect("should have only one element");

let gas_used = self.get_gas_used(tx_execution_status.result.receipts());
let receipts = tx_execution_status.result.receipts();
let script_gas = self.get_script_gas_used(receipts);

let variable_outputs = receipts
.iter()
.filter(
|receipt| matches!(receipt, Receipt::TransferOut { amount, .. } if *amount != 0),
)
.count();

let succeeded = matches!(
tx_execution_status.result,
TransactionExecutionResult::Success { .. }
);

let dry_run = DryRun {
succeeded,
script_gas,
variable_outputs,
};

Ok((gas_used as f64 * (1.0 + tolerance as f64)) as u64)
Ok(dry_run)
}

async fn estimate_gas_price(&self, block_horizon: u32) -> Result<u64> {
Expand Down
Loading

0 comments on commit 30221bd

Please sign in to comment.