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

Add verification on transaction dry_run that they don't spend more than block gas limit #2151

Merged
merged 22 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Added
- [2135](https://github.com/FuelLabs/fuel-core/pull/2135): Added metrics logging for number of blocks served over the p2p req/res protocol.
- [2151](https://github.com/FuelLabs/fuel-core/pull/2151): Added limitations on gas used during dry_run in API.

### Changed

Expand Down
55 changes: 55 additions & 0 deletions crates/fuel-core/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,59 @@ mod tests {
));
}

#[test]
fn transaction_consuming_too_much_gas_are_skipped() {
// Gather the gas consumption of the transaction
let mut executor = create_executor(Default::default(), Default::default());
let block: PartialFuelBlock = PartialFuelBlock {
header: Default::default(),
transactions: vec![TransactionBuilder::script(vec![], vec![])
.max_fee_limit(100_000_000)
.add_random_fee_input()
.script_gas_limit(0)
.tip(123)
.finalize_as_transaction()],
};

// When
let ExecutionResult { tx_status, .. } =
executor.produce_and_commit(block).unwrap();
let tx_gas_usage = tx_status[0].result.total_gas();

// Given
let mut txs = vec![];
for i in 0..10 {
let tx = TransactionBuilder::script(vec![], vec![])
.max_fee_limit(100_000_000)
.add_random_fee_input()
.script_gas_limit(0)
.tip(i * 100)
.finalize_as_transaction();
txs.push(tx);
}
let mut config: Config = Default::default();
// Each TX consumes `tx_gas_usage` gas and so set the block gas limit to execute only 9 transactions.
config
.consensus_parameters
.set_block_gas_limit(tx_gas_usage * 9);
let mut executor = create_executor(Default::default(), config);

let block = PartialFuelBlock {
header: Default::default(),
transactions: txs,
};

// When
let ExecutionResult {
skipped_transactions,
..
} = executor.produce_and_commit(block).unwrap();

// Then
assert_eq!(skipped_transactions.len(), 1);
assert_eq!(skipped_transactions[0].1, ExecutorError::GasOverflow);
}

#[test]
fn skipped_txs_not_affect_order() {
// `tx1` is invalid because it doesn't have inputs for max fee.
Expand Down Expand Up @@ -2883,6 +2936,8 @@ mod tests {
let expensive_consensus_parameters_version = 0;
let mut expensive_consensus_parameters = ConsensusParameters::default();
expensive_consensus_parameters.set_gas_costs(gas_costs.into());
// The block gas limit should cover `vm_initialization` cost
expensive_consensus_parameters.set_block_gas_limit(u64::MAX);
let config = Config {
consensus_parameters: expensive_consensus_parameters.clone(),
..Default::default()
Expand Down
16 changes: 12 additions & 4 deletions crates/fuel-core/src/schema/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use async_graphql::{
Object,
Subscription,
};
use fuel_core_executor::ports::TransactionExt;
use fuel_core_storage::{
iter::IterDirection,
Error as StorageError,
Expand Down Expand Up @@ -276,17 +277,24 @@ impl TxMutation {
gas_price: Option<U64>,
) -> async_graphql::Result<Vec<DryRunTransactionExecutionStatus>> {
let block_producer = ctx.data_unchecked::<BlockProducer>();
let params = ctx
let consensus_params = ctx
.data_unchecked::<ConsensusProvider>()
.latest_consensus_params();
let block_gas_limit = consensus_params.block_gas_limit();

let mut transactions = txs
.iter()
.map(|tx| FuelTx::from_bytes(&tx.0))
.collect::<Result<Vec<FuelTx>, _>>()?;
for transaction in &mut transactions {
transaction.precompute(&params.chain_id())?;
}
transactions.iter_mut().try_fold::<_, _, async_graphql::Result<u64>>(0u64, |acc, tx| {
let gas = tx.max_gas(&consensus_params)?;
let gas = gas.saturating_add(acc);
if gas > block_gas_limit {
return Err(anyhow::anyhow!("The sum of the gas usable by the transactions is greater than the block gas limit").into());
}
tx.precompute(&consensus_params.chain_id())?;
Ok(gas)
})?;

let tx_statuses = block_producer
.dry_run_txs(
Expand Down
28 changes: 12 additions & 16 deletions crates/services/executor/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ use std::borrow::Cow;
#[cfg(not(feature = "std"))]
use alloc::borrow::Cow;

use crate::ports::TransactionExt;
#[cfg(feature = "alloc")]
use alloc::{
format,
Expand Down Expand Up @@ -563,7 +564,7 @@ where
} = components;
let block_gas_limit = self.consensus_params.block_gas_limit();

let remaining_gas_limit = block_gas_limit.saturating_sub(data.used_gas);
let mut remaining_gas_limit = block_gas_limit.saturating_sub(data.used_gas);

let mut regular_tx_iter = l2_tx_source
.next(remaining_gas_limit)
Expand All @@ -572,6 +573,11 @@ where
while regular_tx_iter.peek().is_some() {
for transaction in regular_tx_iter {
let tx_id = transaction.id(&self.consensus_params.chain_id());
if transaction.max_gas(&self.consensus_params)? > remaining_gas_limit {
data.skipped_transactions
.push((tx_id, ExecutorError::GasOverflow));
continue;
}
match self.execute_transaction_and_commit(
block,
storage_tx,
Expand All @@ -586,12 +592,11 @@ where
data.skipped_transactions.push((tx_id, err));
}
}
remaining_gas_limit = block_gas_limit.saturating_sub(data.used_gas);
}

let new_remaining_gas_limit = block_gas_limit.saturating_sub(data.used_gas);

regular_tx_iter = l2_tx_source
.next(new_remaining_gas_limit)
.next(remaining_gas_limit)
.into_iter()
.peekable();
}
Expand Down Expand Up @@ -944,18 +949,9 @@ where
consensus_params: &ConsensusParameters,
) -> Result<(), ForcedTransactionFailure> {
let claimed_max_gas = relayed_tx.max_gas();
let gas_costs = consensus_params.gas_costs();
let fee_params = consensus_params.fee_params();
let actual_max_gas = match tx {
Transaction::Script(tx) => tx.max_gas(gas_costs, fee_params),
Transaction::Create(tx) => tx.max_gas(gas_costs, fee_params),
Transaction::Mint(_) => {
return Err(ForcedTransactionFailure::InvalidTransactionType)
}
Transaction::Upgrade(tx) => tx.max_gas(gas_costs, fee_params),
Transaction::Upload(tx) => tx.max_gas(gas_costs, fee_params),
Transaction::Blob(tx) => tx.max_gas(gas_costs, fee_params),
};
let actual_max_gas = tx
.max_gas(consensus_params)
.map_err(|_| ForcedTransactionFailure::InvalidTransactionType)?;
if actual_max_gas > claimed_max_gas {
return Err(ForcedTransactionFailure::InsufficientMaxGas {
claimed_max_gas,
Expand Down
65 changes: 62 additions & 3 deletions crates/services/executor/src/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ use fuel_core_types::{
header::ConsensusParametersVersion,
primitives::DaBlockHeight,
},
fuel_tx,
fuel_tx::{
self,
Chargeable,
ConsensusParameters,
Transaction,
TxId,
UniqueIdentifier,
},
fuel_types::ChainId,
fuel_vm::checked_transaction::CheckedTransaction,
services::relayer::Event,
services::{
executor::{
Error as ExecutorError,
Result as ExecutorResult,
},
relayer::Event,
},
};

#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use alloc::{
string::ToString,
vec::Vec,
};

/// The wrapper around either `Transaction` or `CheckedTransaction`.
pub enum MaybeCheckedTransaction {
Expand Down Expand Up @@ -54,6 +66,53 @@ impl MaybeCheckedTransaction {
}
}

pub trait TransactionExt {
fn max_gas(&self, consensus_params: &ConsensusParameters) -> ExecutorResult<u64>;
}

Comment on lines +69 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Since this is a one-method trait it would be easier to read the intention of it if it was named after the function: pub trait MaxGas.

impl TransactionExt for Transaction {
fn max_gas(&self, consensus_params: &ConsensusParameters) -> ExecutorResult<u64> {
let fee_params = consensus_params.fee_params();
let gas_costs = consensus_params.gas_costs();
match self {
Transaction::Script(tx) => Ok(tx.max_gas(gas_costs, fee_params)),
Transaction::Create(tx) => Ok(tx.max_gas(gas_costs, fee_params)),
Transaction::Mint(_) => Err(ExecutorError::Other(
"Mint transaction doesn't have max_gas".to_string(),
)),
Transaction::Upgrade(tx) => Ok(tx.max_gas(gas_costs, fee_params)),
Transaction::Upload(tx) => Ok(tx.max_gas(gas_costs, fee_params)),
Transaction::Blob(tx) => Ok(tx.max_gas(gas_costs, fee_params)),
}
}
}

impl TransactionExt for CheckedTransaction {
fn max_gas(&self, _: &ConsensusParameters) -> ExecutorResult<u64> {
match self {
CheckedTransaction::Script(tx) => Ok(tx.metadata().max_gas),
CheckedTransaction::Create(tx) => Ok(tx.metadata().max_gas),
CheckedTransaction::Mint(_) => Err(ExecutorError::Other(
"Mint transaction doesn't have max_gas".to_string(),
)),
CheckedTransaction::Upgrade(tx) => Ok(tx.metadata().max_gas),
CheckedTransaction::Upload(tx) => Ok(tx.metadata().max_gas),
CheckedTransaction::Blob(tx) => Ok(tx.metadata().max_gas),
}
}
}

impl TransactionExt for MaybeCheckedTransaction {
fn max_gas(&self, consensus_params: &ConsensusParameters) -> ExecutorResult<u64> {
match self {
MaybeCheckedTransaction::CheckedTransaction(tx, _) => {
tx.max_gas(consensus_params)
}
MaybeCheckedTransaction::Transaction(tx) => tx.max_gas(consensus_params),
}
}
}

pub trait TransactionsSource {
/// Returns the next batch of transactions to satisfy the `gas_limit`.
fn next(&self, gas_limit: u64) -> Vec<MaybeCheckedTransaction>;
Expand Down
8 changes: 8 additions & 0 deletions crates/types/src/services/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ impl TransactionExecutionResult {
}
}

/// Get the total gas used by the transaction.
pub fn total_gas(&self) -> &u64 {
match self {
TransactionExecutionResult::Success { total_gas, .. }
| TransactionExecutionResult::Failed { total_gas, .. } => total_gas,
}
}

#[cfg(feature = "std")]
/// Get the reason of the failed transaction execution.
pub fn reason(receipts: &[Receipt], state: &Option<ProgramState>) -> String {
Expand Down
36 changes: 36 additions & 0 deletions tests/tests/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,42 @@ async fn dry_run_create() {
assert_eq!(err.kind(), NotFound);
}

#[tokio::test]
async fn dry_run_above_block_gas_limit() {
let config = Config::local_node();
let srv = FuelService::new_node(config).await.unwrap();
let client = FuelClient::from(srv.bound_address);

// Given
let gas_limit = client
.consensus_parameters(0)
.await
.unwrap()
.unwrap()
.block_gas_limit();
let maturity = Default::default();

let script = [
op::addi(0x10, RegId::ZERO, 0xca),
op::addi(0x11, RegId::ZERO, 0xba),
op::log(0x10, 0x11, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
];
let script = script.into_iter().collect();

let tx = TransactionBuilder::script(script, vec![])
.script_gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.finalize_as_transaction();

// When
match client.dry_run(&[tx.clone()]).await {
Ok(_) => panic!("Expected error"),
Err(e) => assert_eq!(e.to_string(), "Response errors; The sum of the gas usable by the transactions is greater than the block gas limit".to_owned()),
}
}

#[tokio::test]
async fn submit() {
let srv = FuelService::new_node(Config::local_node()).await.unwrap();
Expand Down
Loading