Skip to content
This repository has been archived by the owner on Aug 2, 2024. It is now read-only.

Commit

Permalink
estimate fee for bulk transactions (#1240)
Browse files Browse the repository at this point in the history
Co-authored-by: 0xevolve <Artevolve@yahoo.com>
  • Loading branch information
lana-shanghai and EvolveArt authored Nov 4, 2023
1 parent 1050db5 commit 79b1e71
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 90 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- chore: update starknet-js version in faucet-setup docs
- dev(compilation): add incremental compilation
- feat(rpc): add support for bulk estimate fee

## v0.5.0

Expand Down
36 changes: 21 additions & 15 deletions crates/client/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,27 +619,33 @@ where
let best_block_hash = self.client.info().best_hash;
let chain_id = Felt252Wrapper(self.chain_id()?.0);

let mut estimates = vec![];
let mut transactions = vec![];
for tx in request {
let tx = tx.try_into().map_err(|e| {
error!("Failed to convert BroadcastedTransaction to UserTransaction: {e}");
StarknetRpcApiError::InternalServerError
})?;
let (actual_fee, gas_usage) = self
.client
.runtime_api()
.estimate_fee(substrate_block_hash, tx, is_query)
.map_err(|e| {
error!("Request parameters error: {e}");
StarknetRpcApiError::InternalServerError
})?
.map_err(|e| {
error!("Failed to call function: {:#?}", e);
StarknetRpcApiError::ContractError
})?;

estimates.push(FeeEstimate { gas_price: 0, gas_consumed: gas_usage, overall_fee: actual_fee });
transactions.push(tx);
}

let fee_estimates = self
.client
.runtime_api()
.estimate_fee(substrate_block_hash, transactions)
.map_err(|e| {
error!("Request parameters error: {e}");
StarknetRpcApiError::InternalServerError
})?
.map_err(|e| {
error!("Failed to call function: {:#?}", e);
StarknetRpcApiError::ContractError
})?;

let estimates = fee_estimates
.into_iter()
.map(|x| FeeEstimate { gas_price: 0, gas_consumed: x.1, overall_fee: x.0 })
.collect();

Ok(estimates)
}

Expand Down
110 changes: 63 additions & 47 deletions crates/pallets/starknet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ mod offchain_worker;

use blockifier::execution::entry_point::{CallEntryPoint, CallType, EntryPointExecutionContext};
use blockifier::state::cached_state::ContractStorageKey;
use blockifier::state::state_api::State;
use blockifier::transaction::objects::{TransactionExecutionInfo, TransactionExecutionResult};
use starknet_api::state::StorageKey;
use starknet_api::transaction::{Calldata, Event as StarknetEvent, Fee};
Expand Down Expand Up @@ -85,7 +84,6 @@ use mp_fee::INITIAL_GAS;
use mp_felt::Felt252Wrapper;
use mp_hashers::HasherT;
use mp_sequencer_address::{InherentError, InherentType, DEFAULT_SEQUENCER_ADDRESS, INHERENT_IDENTIFIER};
use mp_state::{FeeConfig, StateChanges};
use mp_storage::{StarknetStorageSchemaVersion, PALLET_STARKNET_SCHEMA};
use mp_transactions::execution::{Execute, Validate};
use mp_transactions::{
Expand Down Expand Up @@ -1092,64 +1090,82 @@ impl<T: Config> Pallet<T> {
}

/// Estimate the fee associated with transaction
pub fn estimate_fee(transaction: UserTransaction, is_query: bool) -> Result<(u64, u64), DispatchError> {
pub fn estimate_fee(transactions: Vec<UserTransaction>) -> Result<Vec<(u64, u64)>, DispatchError> {
let chain_id = Self::chain_id();

fn execute_tx_and_rollback<S: State + StateChanges + FeeConfig>(
tx: impl Execute,
state: &mut S,
fn execute_txs_and_rollback<T: pallet::Config>(
txs: Vec<UserTransaction>,
block_context: &BlockContext,
disable_nonce_validation: bool,
) -> TransactionExecutionResult<TransactionExecutionInfo> {
// TODO: initialization can probably be skiped by using mem::MaybeUninit
let mut execution_result = Ok(Default::default());
chain_id: Felt252Wrapper,
) -> Vec<TransactionExecutionResult<TransactionExecutionInfo>> {
let mut execution_results = vec![];
let _: Result<_, DispatchError> = storage::transactional::with_transaction(|| {
execution_result = tx.execute(state, block_context, true, disable_nonce_validation);
for tx in txs {
let result = match tx {
UserTransaction::Declare(tx, contract_class) => {
let executable = tx
.try_into_executable::<T::SystemHash>(chain_id, contract_class, true)
.map_err(|_| Error::<T>::InvalidContractClass)
.expect("Contract class should be valid");
executable.execute(
&mut BlockifierStateAdapter::<T>::default(),
block_context,
true,
disable_nonce_validation,
)
}
UserTransaction::DeployAccount(tx) => {
let executable = tx.into_executable::<T::SystemHash>(chain_id, true);
executable.execute(
&mut BlockifierStateAdapter::<T>::default(),
block_context,
true,
disable_nonce_validation,
)
}
UserTransaction::Invoke(tx) => {
let executable = tx.into_executable::<T::SystemHash>(chain_id, true);
executable.execute(
&mut BlockifierStateAdapter::<T>::default(),
block_context,
true,
disable_nonce_validation,
)
}
};
execution_results.push(result);
}
storage::TransactionOutcome::Rollback(Ok(()))
});
execution_result
execution_results
}

let mut blockifier_state_adapter = BlockifierStateAdapter::<T>::default();
let block_context = Self::get_block_context();
let disable_nonce_validation = T::DisableNonceValidation::get();

let execution_result = match transaction {
UserTransaction::Declare(tx, contract_class) => execute_tx_and_rollback(
tx.try_into_executable::<T::SystemHash>(chain_id, contract_class, is_query)
.map_err(|_| Error::<T>::InvalidContractClass)?,
&mut blockifier_state_adapter,
&block_context,
disable_nonce_validation,
),
UserTransaction::DeployAccount(tx) => execute_tx_and_rollback(
tx.into_executable::<T::SystemHash>(chain_id, is_query),
&mut blockifier_state_adapter,
&block_context,
disable_nonce_validation,
),
UserTransaction::Invoke(tx) => execute_tx_and_rollback(
tx.into_executable::<T::SystemHash>(chain_id, is_query),
&mut blockifier_state_adapter,
&block_context,
disable_nonce_validation,
),
};
let execution_results = execute_txs_and_rollback::<T>(
transactions,
&Self::get_block_context(),
T::DisableNonceValidation::get(),
chain_id,
);

match execution_result {
Ok(tx_exec_info) => {
log!(debug, "Successfully estimated fee: {:?}", tx_exec_info);
if let Some(gas_usage) = tx_exec_info.actual_resources.0.get("l1_gas_usage") {
Ok((tx_exec_info.actual_fee.0 as u64, *gas_usage as u64))
} else {
Err(Error::<T>::TransactionExecutionFailed.into())
let mut results = vec![];
for res in execution_results {
match res {
Ok(tx_exec_info) => {
log!(info, "Successfully estimated fee: {:?}", tx_exec_info);
if let Some(l1_gas_usage) = tx_exec_info.actual_resources.0.get("l1_gas_usage") {
results.push((tx_exec_info.actual_fee.0 as u64, *l1_gas_usage as u64));
} else {
return Err(Error::<T>::TransactionExecutionFailed.into());
}
}
Err(e) => {
log!(info, "Failed to estimate fee: {:?}", e);
return Err(Error::<T>::TransactionExecutionFailed.into());
}
}
Err(e) => {
log!(error, "Failed to estimate fee: {:?}", e);
Err(Error::<T>::TransactionExecutionFailed.into())
}
}
Ok(results)
}

pub fn emit_and_store_tx_and_fees_events(
Expand Down
2 changes: 1 addition & 1 deletion crates/pallets/starknet/src/runtime_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ sp_api::decl_runtime_apis! {
/// Returns the chain id.
fn chain_id() -> Felt252Wrapper;
/// Returns fee estimate
fn estimate_fee(transaction: UserTransaction, is_query: bool) -> Result<(u64, u64), DispatchError>;
fn estimate_fee(transactions: Vec<UserTransaction>) -> Result<Vec<(u64, u64)>, DispatchError>;
/// Filters extrinsic transactions to return only Starknet transactions
///
/// To support runtime upgrades, the client must be unaware of the specific extrinsic
Expand Down
35 changes: 21 additions & 14 deletions crates/pallets/starknet/src/tests/query_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@ fn estimates_tx_fee_successfully_no_validate() {
new_test_ext::<MockRuntime>().execute_with(|| {
basic_test_setup(2);

let tx = get_invoke_dummy(Felt252Wrapper::ZERO);
let tx = UserTransaction::Invoke(tx.into());
let tx_1: mp_transactions::InvokeTransactionV1 = get_storage_read_write_dummy();
let tx_1 = UserTransaction::Invoke(tx_1.into());

let (actual, l1_gas_usage) = Starknet::estimate_fee(tx, true).unwrap();
assert!(actual > 0, "actual fee is missing");
assert!(l1_gas_usage == 0, "this should not be charged any l1_gas as it does not store nor send messages");
let tx_2 = get_invoke_dummy(Felt252Wrapper::ONE);
let tx_2 = UserTransaction::Invoke(tx_2.into());

let tx = get_storage_read_write_dummy();
let tx = UserTransaction::Invoke(tx.into());
let txs = vec![tx_1, tx_2];

let fees = Starknet::estimate_fee(txs).expect("estimate should not fail");

let (actual, l1_gas_usage) = Starknet::estimate_fee(tx, true).unwrap();
let (actual, l1_gas_usage) = fees[0];
assert!(actual > 0, "actual fee is missing");
assert!(l1_gas_usage > 0, "this should be charged l1_gas as it store a value to storage");

let (actual, l1_gas_usage) = fees[1];
assert!(actual > 0, "actual fee is missing");
assert!(l1_gas_usage == 0, "this should not be charged any l1_gas as it does not store nor send messages");
});
}

Expand All @@ -39,7 +43,9 @@ fn estimates_tx_fee_with_query_version() {
let pre_storage = Starknet::pending().len();
let tx = UserTransaction::Invoke(tx.into());

assert_ok!(Starknet::estimate_fee(tx, true));
let tx_vec = vec![tx];

assert_ok!(Starknet::estimate_fee(tx_vec));

assert!(pre_storage == Starknet::pending().len(), "estimate should not add a tx to pending");
});
Expand All @@ -55,11 +61,10 @@ fn executable_tx_should_not_be_estimable() {
let tx_hash = tx.compute_hash::<<MockRuntime as Config>::SystemHash>(chain_id, false);
tx.signature = sign_message_hash(tx_hash);

let tx_vec = vec![UserTransaction::Invoke(tx.clone().into())];

// it should not be valid for estimate calls
assert_err!(
Starknet::estimate_fee(UserTransaction::Invoke(tx.clone().into()), true),
Error::<MockRuntime>::TransactionExecutionFailed
);
assert_err!(Starknet::estimate_fee(tx_vec), Error::<MockRuntime>::TransactionExecutionFailed);

// it should be executable
assert_ok!(Starknet::invoke(RuntimeOrigin::none(), tx.clone().into()));
Expand All @@ -76,8 +81,10 @@ fn query_tx_should_not_be_executable() {
let tx_hash = tx.compute_hash::<<MockRuntime as Config>::SystemHash>(chain_id, true);
tx.signature = sign_message_hash(tx_hash);

let tx_vec = vec![UserTransaction::Invoke(tx.clone().into())];

// it should be valid for estimate calls
assert_ok!(Starknet::estimate_fee(UserTransaction::Invoke(tx.clone().into()), true),);
assert_ok!(Starknet::estimate_fee(tx_vec));

// it should not be executable
assert_err!(
Expand Down
4 changes: 2 additions & 2 deletions crates/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ impl_runtime_apis! {
Starknet::chain_id()
}

fn estimate_fee(transaction: UserTransaction, is_query: bool) -> Result<(u64, u64), DispatchError> {
Starknet::estimate_fee(transaction, is_query)
fn estimate_fee(transactions: Vec<UserTransaction>) -> Result<Vec<(u64, u64)>, DispatchError> {
Starknet::estimate_fee(transactions)
}

fn get_starknet_events_and_their_associated_tx_hash(block_extrinsics: Vec<<Block as BlockT>::Extrinsic>, chain_id: Felt252Wrapper) -> Vec<(Felt252Wrapper, StarknetEvent)> {
Expand Down
27 changes: 16 additions & 11 deletions starknet-rpc-test/estimate_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ async fn fail_if_one_txn_cannot_be_executed(madara: &ThreadSafeMadaraClient) ->
async fn works_ok(madara: &ThreadSafeMadaraClient) -> Result<(), anyhow::Error> {
let rpc = madara.get_starknet_client().await;

// from mainnet tx: 0x000c52079f33dcb44a58904fac3803fd908ac28d6632b67179ee06f2daccb4b5
// https://starkscan.co/tx/0x000c52079f33dcb44a58904fac3803fd908ac28d6632b67179ee06f2daccb4b5
let invoke_transaction = BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction {
let tx = BroadcastedInvokeTransaction {
max_fee: FieldElement::ZERO,
signature: vec![],
nonce: FieldElement::ZERO,
Expand All @@ -99,18 +97,25 @@ async fn works_ok(madara: &ThreadSafeMadaraClient) -> Result<(), anyhow::Error>
FieldElement::from_hex_be("494196e88ce16bff11180d59f3c75e4ba3475d9fba76249ab5f044bcd25add6").unwrap(),
],
is_query: true,
});
};

// from mainnet tx: 0x000c52079f33dcb44a58904fac3803fd908ac28d6632b67179ee06f2daccb4b5
// https://starkscan.co/tx/0x000c52079f33dcb44a58904fac3803fd908ac28d6632b67179ee06f2daccb4b5
let invoke_transaction = BroadcastedTransaction::Invoke(tx.clone());

let invoke_transaction_2 =
BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction { nonce: FieldElement::ONE, ..tx });

let estimate =
rpc.estimate_fee(&vec![invoke_transaction.clone(), invoke_transaction], BlockId::Tag(BlockTag::Latest)).await?;
let estimates =
rpc.estimate_fee(&vec![invoke_transaction, invoke_transaction_2], BlockId::Tag(BlockTag::Latest)).await?;

// TODO: instead execute the tx and check that the actual fee are the same as the estimated ones
assert_eq!(estimate.len(), 2);
assert_eq!(estimate[0].overall_fee, 410);
assert_eq!(estimate[1].overall_fee, 410);
assert_eq!(estimates.len(), 2);
assert_eq!(estimates[0].overall_fee, 410);
assert_eq!(estimates[1].overall_fee, 410);
// https://starkscan.co/block/5
assert_eq!(estimate[0].gas_consumed, 0);
assert_eq!(estimate[1].gas_consumed, 0);
assert_eq!(estimates[0].gas_consumed, 0);
assert_eq!(estimates[1].gas_consumed, 0);

Ok(())
}

0 comments on commit 79b1e71

Please sign in to comment.