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

[cli] Add gas estimate feature #17322

Merged
merged 17 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
226 changes: 219 additions & 7 deletions crates/sui/src/client_commands.rs
stefan-mysten marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ macro_rules! serialize_or_execute {
}};
}

/// Only to be used within CLI
pub const MAX_GAS_BUDGET: u64 = 50_000_000_000;
pub const GAS_SAFE_OVERHEAD: u64 = 1000;

#[derive(Parser)]
#[clap(rename_all = "kebab-case")]
pub enum SuiClientCommands {
Expand Down Expand Up @@ -613,9 +617,12 @@ pub enum SuiClientCommands {
/// Global options for most transaction execution related commands
#[derive(Args, Debug)]
pub struct Opts {
/// Gas budget for this transaction (in MIST)
/// An optional gas budget for this transaction (in MIST). If gas budget is not provided, the
/// tool will first perform a dry run to estimate the gas cost, and then it will execute the
/// transaction. Please note that this incurs a small cost in performance due to the additional
/// dry run call.
#[arg(long)]
pub gas_budget: u64,
pub gas_budget: Option<u64>,
/// Perform a dry run of the transaction, without executing it.
#[arg(long)]
pub dry_run: bool,
Expand Down Expand Up @@ -647,7 +654,7 @@ impl Opts {
/// Uses the passed gas_budget for the gas budget variable and sets all other flags to false.
pub fn for_testing(gas_budget: u64) -> Self {
Self {
gas_budget,
gas_budget: Some(gas_budget),
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
Expand All @@ -657,7 +664,7 @@ impl Opts {
/// and sets all other flags to false.
pub fn for_testing_dry_run(gas_budget: u64) -> Self {
Self {
gas_budget,
gas_budget: Some(gas_budget),
dry_run: true,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
Expand Down Expand Up @@ -934,11 +941,27 @@ impl SuiClientCommands {
)
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
sender,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};

let data = client
.transaction_builder()
.tx_data(
sender,
tx_kind,
tx_kind.clone(),
gas_budget,
gas_price,
gas.into_iter().collect(),
Expand Down Expand Up @@ -1040,6 +1063,21 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
sender,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};

let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1265,6 +1303,20 @@ impl SuiClientCommands {
)
.await;
}
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1320,6 +1372,20 @@ impl SuiClientCommands {
)
.await;
}
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
from,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1371,6 +1437,20 @@ impl SuiClientCommands {
)
.await;
}
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
from,
tx_kind.clone(),
gas_price,
Some(vec![object_id]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1455,6 +1535,20 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
from,
kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1529,6 +1623,20 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
kind.clone(),
gas_price,
Some(input_coins.clone()),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1582,6 +1690,20 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
Some(input_coins.clone()),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(signer, tx_kind, gas_budget, gas_price, input_coins, None)
Expand Down Expand Up @@ -1734,6 +1856,21 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};

let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1787,6 +1924,21 @@ impl SuiClientCommands {
)
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -2997,15 +3149,19 @@ fn format_balance(
}

/// Helper function to reduce code duplication for executing dry run
async fn execute_dry_run(
pub async fn execute_dry_run(
context: &mut WalletContext,
signer: SuiAddress,
kind: TransactionKind,
gas_budget: u64,
gas_budget: Option<u64>,
gas_price: u64,
gas_payment: Option<Vec<ObjectID>>,
sponsor: Option<SuiAddress>,
) -> Result<SuiClientCommandResult, anyhow::Error> {
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => max_gas_budget(context).await?,
};
let dry_run_tx_data = context
.get_client()
.await?
Expand All @@ -3021,3 +3177,59 @@ async fn execute_dry_run(
.map_err(|e| anyhow!("Dry run failed: {e}"))?;
Ok(SuiClientCommandResult::DryRun(response))
}

/// Call a dry run with the transaction data to estimate the gas budget.
/// The estimated gas budget is computed as following:
/// * the maximum between A and B, where:
/// A = computation cost + GAS_SAFE_OVERHEAD * reference gas price
/// B = computation cost + storage cost - storage rebate + GAS_SAFE_OVERHEAD * reference gas price
/// overhead
///
/// This gas estimate is computed exactly as in the TypeScript SDK (sha 3c43692). Check the
/// function `prepare` in the TransactionBlock.ts file and look for safeOverhead variable
/// <https://github.com/MystenLabs/sui/blob/main/sdk/typescript/src/transactions/TransactionBlock.ts>
stefan-mysten marked this conversation as resolved.
Show resolved Hide resolved
pub async fn estimate_gas_budget(
context: &mut WalletContext,
signer: SuiAddress,
kind: TransactionKind,
gas_price: u64,
gas_payment: Option<Vec<ObjectID>>,
sponsor: Option<SuiAddress>,
) -> Result<u64, anyhow::Error> {
let client = context.get_client().await?;
let dry_run =
execute_dry_run(context, signer, kind, None, gas_price, gas_payment, sponsor).await;
if let Ok(SuiClientCommandResult::DryRun(dry_run)) = dry_run {
let safe_overhead = GAS_SAFE_OVERHEAD * client.read_api().get_reference_gas_price().await?;
let computation_cost_with_overhead =
(dry_run.effects.gas_cost_summary().computation_cost + safe_overhead) as i64;
let gas_budget = dry_run.effects.gas_cost_summary().net_gas_usage() + safe_overhead as i64;
let gas_estimate = std::cmp::max(computation_cost_with_overhead, gas_budget);
let gas_estimate: u64 = gas_estimate.try_into().map_err(|e| {
anyhow!(
"Could not convert gas estimate to u64: {e}.\n Please use the --gas-budget flag to\
provide a gas budget."
)
})?;
Ok(gas_estimate)
} else {
bail!("Dry run failed, could not automatically determine the gas budget. Please use the --gas-budget flag to provide a gas budget.")
}
stefan-mysten marked this conversation as resolved.
Show resolved Hide resolved
}

/// Queries the protocol config for the maximum gas allowed in a transaction.
pub async fn max_gas_budget(context: &mut WalletContext) -> Result<u64, anyhow::Error> {
let cfg = context
.get_client()
.await?
.read_api()
.get_protocol_config(None)
.await?;
Ok(match cfg.attributes.get("max_tx_gas") {
Some(Some(sui_json_rpc_types::SuiProtocolConfigValue::U64(y))) => *y,
_ => bail!(
"Could not automatically find the maximum gas allowed in a transaction from the \
protocol config. Please provide a gas budget with the --gas-budget flag."
),
})
}
2 changes: 1 addition & 1 deletion crates/sui/src/client_ptb/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub struct ProgramMetadata {
pub gas_object_id: Option<Spanned<ObjectID>>,
pub json_set: bool,
pub dry_run_set: bool,
pub gas_budget: Spanned<u64>,
pub gas_budget: Option<Spanned<u64>>,
}

/// A parsed module access consisting of the address, module name, and function name.
Expand Down
7 changes: 3 additions & 4 deletions crates/sui/src/client_ptb/displays/ptb_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ impl<'a> Display for PTBPreview<'a> {
}
// index of horizontal line to draw after commands
let line_index = builder.count_rows();
builder.push_record([
GAS_BUDGET,
self.program_metadata.gas_budget.value.to_string().as_str(),
]);
if let Some(gas_budget) = self.program_metadata.gas_budget {
builder.push_record([GAS_BUDGET, gas_budget.value.to_string().as_str()]);
}
if let Some(gas_coin_id) = self.program_metadata.gas_object_id {
builder.push_record([GAS_COIN, gas_coin_id.value.to_string().as_str()]);
}
Expand Down
10 changes: 1 addition & 9 deletions crates/sui/src/client_ptb/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,6 @@ impl<'a, I: Iterator<Item = &'a str>> ProgramParser<'a, I> {
.push(err!(sp, "Trailing {tok} found after the last command",));
}

let Some(gas_budget) = self.state.gas_budget else {
self.state.errors.push(err!(
sp => help: { "Use --gas-budget <u64> to set a gas budget" },
"Gas budget not set."
));
return Err(self.state.errors);
};

if self.state.errors.is_empty() {
Ok((
A::Program {
Expand All @@ -215,7 +207,7 @@ impl<'a, I: Iterator<Item = &'a str>> ProgramParser<'a, I> {
gas_object_id: self.state.gas_object_id,
json_set: self.state.json_set,
dry_run_set: self.state.dry_run_set,
gas_budget,
gas_budget: self.state.gas_budget,
},
))
} else {
Expand Down
Loading
Loading