diff --git a/.changes/added/2840.md b/.changes/added/2840.md new file mode 100644 index 00000000000..cb9e5557d03 --- /dev/null +++ b/.changes/added/2840.md @@ -0,0 +1,3 @@ +Added a new CLI arguments: +- `assemble-tx-dry-run-limit` - The max number how many times script can be executed during `assemble_tx` GraphQL request. Default value is `3` times. +- `assemble-tx-estimate-predicates-limit` - The max number how many times predicates can be estimated during `assemble_tx` GraphQL request. Default values is `10` times. \ No newline at end of file diff --git a/.changes/breaking/2840.md b/.changes/breaking/2840.md new file mode 100644 index 00000000000..37ed90ad5d2 --- /dev/null +++ b/.changes/breaking/2840.md @@ -0,0 +1,3 @@ +CLI argument `vm-backtrace` is deprecated and does nothing. It will be removed in a future version of `fuel-core`. +The `extra_tx_checks` field was renamed into `forbid_fake_coins` that affects JSON based serialization/deserialization. +Renamed `extra_tx_checks_default` field into `forbid_fake_coins_default`. \ No newline at end of file diff --git a/.changes/changed/2840.md b/.changes/changed/2840.md new file mode 100644 index 00000000000..242fa222745 --- /dev/null +++ b/.changes/changed/2840.md @@ -0,0 +1 @@ +Removed `log_backtrace` logic from the executor. It is not needed anymore with the existence of the local debugger for the transactions. \ No newline at end of file diff --git a/.changes/fixed/2840.md b/.changes/fixed/2840.md new file mode 100644 index 00000000000..b984cd1e6d6 --- /dev/null +++ b/.changes/fixed/2840.md @@ -0,0 +1 @@ +Fixed `fuel-core-client` receipt deserialization in the case if the `ContractId` is zero. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0b344e63891..e23b5486fc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3655,7 +3655,6 @@ dependencies = [ "fuel-core-storage", "fuel-core-trace", "fuel-core-types 0.41.7", - "hex", "parking_lot", "serde", "tracing", diff --git a/bin/fuel-core/src/cli/run.rs b/bin/fuel-core/src/cli/run.rs index d7f152e6c7e..25e3d9083be 100644 --- a/bin/fuel-core/src/cli/run.rs +++ b/bin/fuel-core/src/cli/run.rs @@ -33,7 +33,6 @@ use fuel_core::{ Config, DbType, RelayerConsensusConfig, - VMConfig, }, state::rocks_db::{ ColumnsPolicy, @@ -197,6 +196,7 @@ pub struct Command { /// Enable logging of backtraces from vm errors #[arg(long = "vm-backtrace", env)] + #[deprecated] pub vm_backtrace: bool, /// Enable full utxo stateful validation @@ -307,6 +307,7 @@ pub struct Command { impl Command { pub async fn get_config(self) -> anyhow::Result { + #[allow(deprecated)] let Command { service_name: name, max_database_cache_size, @@ -319,7 +320,7 @@ impl Command { db_prune, snapshot, continue_on_error, - vm_backtrace, + vm_backtrace: _, debug, historical_execution, utxo_validation, @@ -628,6 +629,9 @@ impl Command { max_concurrent_queries: graphql.graphql_max_concurrent_queries, request_body_bytes_limit: graphql.graphql_request_body_bytes_limit, api_request_timeout: graphql.api_request_timeout.into(), + assemble_tx_dry_run_limit: graphql.assemble_tx_dry_run_limit, + assemble_tx_estimate_predicates_limit: graphql + .assemble_tx_estimate_predicates_limit, query_log_threshold_time: graphql.query_log_threshold_time.into(), costs: Costs { balance_query: graphql.costs.balance_query, @@ -671,9 +675,6 @@ impl Command { executor_number_of_cores, block_production: trigger, predefined_blocks_path, - vm: VMConfig { - backtrace: vm_backtrace, - }, txpool: TxPoolConfig { max_txs_chain_count: tx_max_chain_count, max_txs_ttl: tx_pool_ttl, diff --git a/bin/fuel-core/src/cli/run/graphql.rs b/bin/fuel-core/src/cli/run/graphql.rs index a71bbc379e9..cba56429342 100644 --- a/bin/fuel-core/src/cli/run/graphql.rs +++ b/bin/fuel-core/src/cli/run/graphql.rs @@ -66,6 +66,20 @@ pub struct GraphQLArgs { #[clap(long = "api-request-timeout", default_value = "30s", env)] pub api_request_timeout: humantime::Duration, + /// The max number how many times script can be executed + /// during `assemble_tx` GraphQL request. + #[clap(long = "assemble-tx-dry-run-limit", default_value = "3", env)] + pub assemble_tx_dry_run_limit: usize, + + /// The max number how many times predicate can be estimated + /// during `assemble_tx` GraphQL request. + #[clap( + long = "assemble-tx-estimate-predicates-limit", + default_value = "10", + env + )] + pub assemble_tx_estimate_predicates_limit: usize, + /// Maximum allowed block lag for GraphQL fuel block height requests. /// The client waits for the node to catch up if it's behind by no more blocks than /// this tolerance. diff --git a/crates/client/src/client/schema/tx/transparent_receipt.rs b/crates/client/src/client/schema/tx/transparent_receipt.rs index e82980690d3..d8500a7ebb2 100644 --- a/crates/client/src/client/schema/tx/transparent_receipt.rs +++ b/crates/client/src/client/schema/tx/transparent_receipt.rs @@ -72,11 +72,14 @@ impl TryFrom for fuel_tx::Receipt { fn try_from(schema: Receipt) -> Result { Ok(match schema.receipt_type { ReceiptType::Call => fuel_tx::Receipt::Call { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), to: schema .to - .ok_or_else(|| MissingField("to".to_string()))? - .into(), + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), amount: schema .amount .ok_or_else(|| MissingField("amount".to_string()))? @@ -107,7 +110,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::Return => fuel_tx::Receipt::Return { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), val: schema .val .ok_or_else(|| MissingField("val".to_string()))? @@ -122,7 +128,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::ReturnData => fuel_tx::Receipt::ReturnData { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), ptr: schema .ptr .ok_or_else(|| MissingField("ptr".to_string()))? @@ -151,7 +160,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::Panic => fuel_tx::Receipt::Panic { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), reason: schema .reason .ok_or_else(|| MissingField("reason".to_string()))? @@ -167,7 +179,10 @@ impl TryFrom for fuel_tx::Receipt { contract_id: schema.contract_id.map(Into::into), }, ReceiptType::Revert => fuel_tx::Receipt::Revert { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), ra: schema .ra .ok_or_else(|| MissingField("ra".to_string()))? @@ -182,7 +197,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::Log => fuel_tx::Receipt::Log { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), ra: schema .ra .ok_or_else(|| MissingField("ra".to_string()))? @@ -209,7 +227,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::LogData => fuel_tx::Receipt::LogData { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), ra: schema .ra .ok_or_else(|| MissingField("ra".to_string()))? @@ -246,7 +267,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::Transfer => fuel_tx::Receipt::Transfer { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), to: schema .to .ok_or_else(|| MissingField("to".to_string()))? @@ -269,7 +293,10 @@ impl TryFrom for fuel_tx::Receipt { .into(), }, ReceiptType::TransferOut => fuel_tx::Receipt::TransferOut { - id: schema.id.map(|id| id.into()).unwrap_or_default(), + id: schema + .id + .map(Into::into) + .unwrap_or(fuel_tx::ContractId::zeroed()), to: schema .to_address .ok_or_else(|| MissingField("to_address".to_string()))? diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 6a31bdaf72c..80555fbd934 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -171,10 +171,8 @@ mod tests { /// The executor already has these parameters, and this field allows us /// to override the existing value. pub consensus_parameters: ConsensusParameters, - /// Print execution backtraces if transaction execution reverts. - pub backtrace: bool, - /// Default mode for utxo_validation - pub utxo_validation_default: bool, + /// Default mode for `forbid_fake_coins` in the executor. + pub forbid_fake_coins_default: bool, } #[derive(Clone, Debug)] @@ -216,8 +214,7 @@ mod tests { config: Config, ) -> Executor { let executor_config = fuel_core_upgradable_executor::config::Config { - backtrace: config.backtrace, - utxo_validation_default: config.utxo_validation_default, + forbid_fake_coins_default: config.forbid_fake_coins_default, native_executor_version: None, allow_historical_execution: true, }; @@ -825,7 +822,7 @@ mod tests { let mut validator = create_executor( Default::default(), Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, ..Default::default() }, ); @@ -1158,7 +1155,7 @@ mod tests { // setup executors with utxo-validation enabled let config = Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }; let producer = create_executor(Database::default(), config.clone()); @@ -1286,7 +1283,7 @@ mod tests { let mut executor = create_executor( Database::default(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -1354,7 +1351,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -1423,7 +1420,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -1472,7 +1469,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -1667,7 +1664,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, ..Default::default() }, ); @@ -1722,7 +1719,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, ..Default::default() }, ); @@ -1824,7 +1821,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, ..Default::default() }, ); @@ -1930,9 +1927,8 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, consensus_parameters: consensus_parameters.clone(), - ..Default::default() }, ); @@ -2091,7 +2087,7 @@ mod tests { let mut executor = create_executor( db.clone(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -2380,7 +2376,7 @@ mod tests { create_executor( database, Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ) @@ -2807,7 +2803,7 @@ mod tests { let mut executor = create_executor( database.clone(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -2871,7 +2867,7 @@ mod tests { let mut executor = create_executor( database.clone(), Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, ..Default::default() }, ); @@ -2893,9 +2889,8 @@ mod tests { let consensus_parameters = ConsensusParameters::default(); let config = Config { - utxo_validation_default: true, + forbid_fake_coins_default: true, consensus_parameters: consensus_parameters.clone(), - ..Default::default() }; let mut tx = TransactionBuilder::script( @@ -3088,7 +3083,7 @@ mod tests { .finalize(); let config = Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, ..Default::default() }; let (sender, mut receiver) = tokio::sync::mpsc::channel(2); @@ -3152,7 +3147,7 @@ mod tests { .finalize(); let config = Config { - utxo_validation_default: false, + forbid_fake_coins_default: false, ..Default::default() }; let (sender, mut receiver) = tokio::sync::mpsc::channel(2); diff --git a/crates/fuel-core/src/graphql_api.rs b/crates/fuel-core/src/graphql_api.rs index 37d4bd60bad..0761fda3197 100644 --- a/crates/fuel-core/src/graphql_api.rs +++ b/crates/fuel-core/src/graphql_api.rs @@ -25,7 +25,6 @@ pub struct Config { pub utxo_validation: bool, pub debug: bool, pub historical_execution: bool, - pub vm_backtrace: bool, pub max_tx: usize, pub max_gas: u64, pub max_size: usize, @@ -54,6 +53,8 @@ pub struct ServiceConfig { /// Time to wait after submitting a query before debug info will be logged about query. pub query_log_threshold_time: Duration, pub api_request_timeout: Duration, + pub assemble_tx_dry_run_limit: usize, + pub assemble_tx_estimate_predicates_limit: usize, /// Configurable cost parameters to limit graphql queries complexity pub costs: Costs, } diff --git a/crates/fuel-core/src/schema/node_info.rs b/crates/fuel-core/src/schema/node_info.rs index 4015dcaf412..5d56efa51f9 100644 --- a/crates/fuel-core/src/schema/node_info.rs +++ b/crates/fuel-core/src/schema/node_info.rs @@ -110,7 +110,7 @@ impl NodeQuery { let read_view = db.view()?; Ok(NodeInfo { utxo_validation: config.utxo_validation, - vm_backtrace: config.vm_backtrace, + vm_backtrace: false, max_tx: (config.max_tx as u64).into(), max_gas: config.max_gas.into(), max_size: (config.max_size as u64).into(), diff --git a/crates/fuel-core/src/schema/tx.rs b/crates/fuel-core/src/schema/tx.rs index 817ad6fdc49..349ebd7cad4 100644 --- a/crates/fuel-core/src/schema/tx.rs +++ b/crates/fuel-core/src/schema/tx.rs @@ -357,6 +357,7 @@ impl TxQuery { let exclude: Exclude = exclude_input.into(); let gas_price = ctx.estimate_gas_price(Some(block_horizon.into()))?; + let config = &ctx.data_unchecked::().config; let tx = FuelTx::from_bytes(&tx.0)?; @@ -372,6 +373,8 @@ impl TxQuery { reserve_gas, consensus_parameters, gas_price, + dry_run_limit: config.assemble_tx_dry_run_limit, + estimate_predicates_limit: config.assemble_tx_estimate_predicates_limit, block_producer, read_view, shared_memory_pool, @@ -403,9 +406,11 @@ impl TxQuery { .await? .into_iter() .next() - .ok_or(anyhow::anyhow!( - "Failed to do the final `dry_run` of the assembled transaction" - ))?; + .ok_or_else(|| { + anyhow::anyhow!( + "Failed to do the final `dry_run` of the assembled transaction" + ) + })?; let result = AssembleTransactionResult { tx_id: status.id, diff --git a/crates/fuel-core/src/schema/tx/assemble_tx.rs b/crates/fuel-core/src/schema/tx/assemble_tx.rs index eeff68510a1..44e14d275dd 100644 --- a/crates/fuel-core/src/schema/tx/assemble_tx.rs +++ b/crates/fuel-core/src/schema/tx/assemble_tx.rs @@ -56,7 +56,10 @@ use fuel_core_types::{ Script, Transaction, }, - fuel_types::canonical::Serialize, + fuel_types::{ + canonical::Serialize, + ContractId, + }, fuel_vm::{ checked_transaction::CheckPredicateParams, interpreter::ExecutableTransaction, @@ -83,6 +86,8 @@ pub struct AssembleArguments<'a> { pub reserve_gas: u64, pub consensus_parameters: Arc, pub gas_price: u64, + pub dry_run_limit: usize, + pub estimate_predicates_limit: usize, pub read_view: Arc, pub block_producer: &'a BlockProducer, pub shared_memory_pool: &'a SharedMemoryPool, @@ -119,7 +124,8 @@ impl<'a> AssembleArguments<'a> { .into_iter() .next(); - let result = result.ok_or(anyhow::anyhow!("No result for the coins to spend"))?; + let result = + result.ok_or_else(|| anyhow::anyhow!("No result for the coins to spend"))?; Ok(result) } @@ -135,7 +141,7 @@ impl<'a> AssembleArguments<'a> { .await? .into_iter() .next() - .ok_or(anyhow::anyhow!("No result for the dry run")) + .ok_or_else(|| anyhow::anyhow!("No result for the dry run")) } } @@ -145,6 +151,7 @@ pub struct AssembleTx<'a, Tx> { signature_witness_indexes: HashMap, change_output_policies: HashMap, set_change_outputs: HashSet, + set_contracts: HashSet, // The amount of the base asset that is reserved for the application logic base_asset_reserved: u64, has_predicates: bool, @@ -153,6 +160,7 @@ pub struct AssembleTx<'a, Tx> { original_witness_limit: u64, fee_payer_account: Account, estimated_predicates_count: usize, + dry_run_count: usize, } impl<'a, Tx> AssembleTx<'a, Tx> @@ -194,6 +202,7 @@ where let mut has_predicates = false; let inputs = tx.inputs(); let mut unique_used_asset = HashSet::new(); + let mut set_contracts = HashSet::new(); for input in inputs { if let Some(utxo_id) = input.utxo_id() { arguments.exclude.exclude(CoinId::Utxo(*utxo_id)); @@ -225,8 +234,8 @@ where }) => { signature_witness_indexes.insert(*owner, *witness_index); } - Input::Contract(_) => { - // Do nothing + Input::Contract(c) => { + set_contracts.insert(c.contract_id); } Input::CoinPredicate(_) @@ -329,6 +338,7 @@ where signature_witness_indexes, change_output_policies, set_change_outputs, + set_contracts, base_asset_reserved: base_asset_reserved.expect("Set above; qed"), has_predicates, index_of_first_fake_variable_output: None, @@ -336,6 +346,7 @@ where original_witness_limit, fee_payer_account, estimated_predicates_count: 0, + dry_run_count: 0, }) } @@ -647,9 +658,11 @@ where return Ok(self) } - if self.estimated_predicates_count > 10 { + if self.estimated_predicates_count >= self.arguments.estimate_predicates_limit { return Err(anyhow::anyhow!( - "The transaction estimation requires running of predicate more than 10 times" + "The transaction estimation requires running \ + of predicate more than {} times", + self.arguments.estimate_predicates_limit )); } @@ -677,8 +690,10 @@ where self.tx = estimated_tx; - self.estimated_predicates_count = - self.estimated_predicates_count.saturating_add(1); + self.estimated_predicates_count = self + .estimated_predicates_count + .checked_add(1) + .ok_or_else(|| anyhow::anyhow!("estimated predicates count overflow"))?; Ok(self) } @@ -769,6 +784,14 @@ where let fee_params = self.arguments.consensus_parameters.fee_params(); loop { + if self.dry_run_count > self.arguments.dry_run_limit { + return Err(anyhow::anyhow!( + "The transaction script estimation \ + requires running of script more than {} times", + self.arguments.dry_run_limit + )); + } + if !has_spendable_input { script.inputs_mut().push(fake_input()); } @@ -783,6 +806,12 @@ where *script.script_gas_limit_mut() = max_gas_limit; let (updated_tx, new_status) = self.arguments.dry_run(script).await?; + + self.dry_run_count = self + .dry_run_count + .checked_add(1) + .ok_or_else(|| anyhow::anyhow!("dry run count overflow"))?; + let Transaction::Script(updated_script) = updated_tx else { return Err(anyhow::anyhow!( "During script gas limit estimation, \ @@ -802,7 +831,7 @@ where match &status.result { TransactionExecutionResult::Success { .. } => break, TransactionExecutionResult::Failed { receipts, .. } => { - for receipt in receipts.iter().rev() { + for receipt in receipts.iter() { if let Receipt::Panic { reason, contract_id, @@ -825,6 +854,10 @@ where } for contract_id in contracts_not_in_inputs { + if !self.set_contracts.insert(contract_id) { + continue + } + let inptus = script.inputs_mut(); let contract_idx = u16::try_from(inptus.len()).unwrap_or(u16::MAX); @@ -888,9 +921,11 @@ where if asset_id == &base_asset_id && &fee_payer_account.owner() == owner { total_base_asset = - total_base_asset.checked_add(amount).ok_or(anyhow::anyhow!( + total_base_asset.checked_add(amount).ok_or_else(|| { + anyhow::anyhow!( "The total base asset amount used by the transaction is too big" - ))?; + ) + })?; } } diff --git a/crates/fuel-core/src/service.rs b/crates/fuel-core/src/service.rs index 0bd72da3f48..4ef0df9cf9a 100644 --- a/crates/fuel-core/src/service.rs +++ b/crates/fuel-core/src/service.rs @@ -11,7 +11,6 @@ pub use config::{ Config, DbType, RelayerConsensusConfig, - VMConfig, }; use fuel_core_chain_config::{ ConsensusConfig, diff --git a/crates/fuel-core/src/service/adapters/producer.rs b/crates/fuel-core/src/service/adapters/producer.rs index b7119a59971..d2bf6259b00 100644 --- a/crates/fuel-core/src/service/adapters/producer.rs +++ b/crates/fuel-core/src/service/adapters/producer.rs @@ -129,10 +129,10 @@ impl fuel_core_producer::ports::DryRunner for ExecutorAdapter { fn dry_run( &self, block: Components>, - utxo_validation: Option, + forbid_fake_coins: Option, at_height: Option, ) -> ExecutorResult> { - self.executor.dry_run(block, utxo_validation, at_height) + self.executor.dry_run(block, forbid_fake_coins, at_height) } } diff --git a/crates/fuel-core/src/service/config.rs b/crates/fuel-core/src/service/config.rs index 60dad28dfda..36efae52689 100644 --- a/crates/fuel-core/src/service/config.rs +++ b/crates/fuel-core/src/service/config.rs @@ -68,7 +68,6 @@ pub struct Config { pub executor_number_of_cores: NonZeroUsize, pub block_production: Trigger, pub predefined_blocks_path: Option, - pub vm: VMConfig, pub txpool: TxPoolConfig, pub tx_status_manager: TxStatusManagerConfig, pub block_producer: fuel_core_producer::Config, @@ -167,6 +166,8 @@ impl Config { request_body_bytes_limit: 16 * 1024 * 1024, query_log_threshold_time: Duration::from_secs(2), api_request_timeout: Duration::from_secs(60), + assemble_tx_dry_run_limit: 3, + assemble_tx_estimate_predicates_limit: 5, costs: Default::default(), required_fuel_block_height_tolerance: 10, required_fuel_block_height_timeout: Duration::from_secs(30), @@ -182,7 +183,6 @@ impl Config { snapshot_reader, block_production: Trigger::Instant, predefined_blocks_path: None, - vm: Default::default(), txpool: TxPoolConfig { utxo_validation, max_txs_ttl: MAX_TXS_TTL, @@ -262,11 +262,6 @@ impl From<&Config> for fuel_core_poa::Config { } } -#[derive(Clone, Debug, Default)] -pub struct VMConfig { - pub backtrace: bool, -} - #[derive( Clone, Copy, Debug, Display, Eq, PartialEq, EnumString, EnumVariantNames, ValueEnum, )] diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index 407d7dfc5e4..38564a8b76d 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -143,8 +143,7 @@ pub fn init_sub_services( } let upgradable_executor_config = fuel_core_upgradable_executor::config::Config { - backtrace: config.vm.backtrace, - utxo_validation_default: config.utxo_validation, + forbid_fake_coins_default: config.utxo_validation, native_executor_version: config.native_executor_version, allow_historical_execution: config.historical_execution, }; @@ -398,7 +397,6 @@ pub fn init_sub_services( utxo_validation: config.utxo_validation, debug: config.debug, historical_execution: config.historical_execution, - vm_backtrace: config.vm.backtrace, max_tx: config.txpool.pool_limits.max_txs, max_gas: config.txpool.pool_limits.max_gas, max_size: config.txpool.pool_limits.max_bytes_size, diff --git a/crates/services/executor/Cargo.toml b/crates/services/executor/Cargo.toml index 084caa057f7..22568f1310f 100644 --- a/crates/services/executor/Cargo.toml +++ b/crates/services/executor/Cargo.toml @@ -17,7 +17,6 @@ fuel-core-storage = { workspace = true, default-features = false, features = [ fuel-core-types = { workspace = true, default-features = false, features = [ "alloc", ] } -hex = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } tracing = { workspace = true } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index ba7111f1870..a0a32d87fca 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -53,10 +53,7 @@ use fuel_core_types::{ contract::ContractUtxoInfo, RelayedTransaction, }, - fuel_asm::{ - RegId, - Word, - }, + fuel_asm::Word, fuel_merkle::binary::root_calculator::MerkleRootCalculator, fuel_tx::{ field::{ @@ -118,11 +115,9 @@ use fuel_core_types::{ CheckedMetadata as CheckedMetadataTrait, ExecutableTransaction, InterpreterParams, - Memory, MemoryInstance, }, state::StateTransition, - Backtrace as FuelBacktrace, Interpreter, ProgramState, }, @@ -164,6 +159,17 @@ use alloc::{ vec, vec::Vec, }; +use fuel_core_types::{ + fuel_asm::{ + op, + PanicInstruction, + }, + fuel_tx::PanicReason, + fuel_vm::{ + interpreter::NotSupportedEcal, + verification, + }, +}; /// The maximum amount of transactions that can be included in a block, /// excluding the mint transaction. @@ -324,13 +330,25 @@ impl ExecutionData { /// Per-block execution options #[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)] pub struct ExecutionOptions { - // TODO: This is a bad name and the real motivation for this field should be specified - /// UTXO Validation flag, when disabled the executor skips signature and UTXO existence checks - pub extra_tx_checks: bool, + /// The flag allows the usage of fake coins in the inputs of the transaction. + /// When `false` the executor skips signature and UTXO existence checks. + pub forbid_fake_coins: bool, /// Print execution backtraces if transaction execution reverts. + /// + /// Deprecated field. Do nothing. This fields exists for serialization and + /// deserialization compatibility. pub backtrace: bool, } +/// Per-block execution options +#[derive(Clone, Default, Debug)] +struct ExecutionOptionsInner { + /// The flag allows the usage of fake coins in the inputs of the transaction. + /// When `false` the executor skips signature and UTXO existence checks. + pub forbid_fake_coins: bool, + pub dry_run: bool, +} + /// The executor instance performs block production and validation. Given a block, it will execute all /// the transactions contained in the block and persist changes to the underlying database as needed. #[derive(Clone, Debug)] @@ -370,16 +388,14 @@ where consensus_params_version, new_tx_waiter, preconfirmation_sender, + dry_run, )?; #[cfg(feature = "fault-proving")] let chain_id = block_executor.consensus_params.chain_id(); - let (partial_block, execution_data) = if dry_run { - block_executor.dry_run_block(components, storage_tx).await? - } else { - block_executor.produce_block(components, storage_tx).await? - }; + let (partial_block, execution_data) = + block_executor.execute(components, storage_tx).await?; let ExecutionData { message_ids, @@ -429,6 +445,7 @@ where consensus_params_version, TimeoutOnlyTxWaiter, TransparentPreconfirmationSender, + false, )?; let ExecutionData { @@ -458,6 +475,7 @@ where consensus_params_version: ConsensusParametersVersion, new_tx_waiter: N, preconfirmation_sender: P, + dry_run: bool, ) -> ExecutorResult<(BlockExecutor, StorageTransaction)> { let storage_tx = self .database @@ -476,6 +494,7 @@ where consensus_params, new_tx_waiter, preconfirmation_sender, + dry_run, )?; Ok((executor, storage_tx)) } @@ -488,7 +507,7 @@ type TxStorageTransaction<'a, T> = StorageTransaction<&'a mut BlockStorageTransa pub struct BlockExecutor { relayer: R, consensus_params: ConsensusParameters, - options: ExecutionOptions, + options: ExecutionOptionsInner, new_tx_waiter: TxWaiter, preconfirmation_sender: PreconfirmationSender, } @@ -502,11 +521,15 @@ impl consensus_params: ConsensusParameters, new_tx_waiter: TxWaiter, preconfirmation_sender: PreconfirmationSender, + dry_run: bool, ) -> ExecutorResult { Ok(Self { relayer, consensus_params, - options, + options: ExecutionOptionsInner { + forbid_fake_coins: options.forbid_fake_coins, + dry_run, + }, new_tx_waiter, preconfirmation_sender, }) @@ -519,6 +542,22 @@ where N: NewTxWaiterPort, P: PreconfirmationSenderPort, { + async fn execute( + self, + components: Components, + block_storage_tx: BlockStorageTransaction, + ) -> ExecutorResult<(PartialFuelBlock, ExecutionData)> + where + TxSource: TransactionsSource, + D: KeyValueInspect, + { + if self.options.dry_run { + self.dry_run_block(components, block_storage_tx).await + } else { + self.produce_block(components, block_storage_tx).await + } + } + #[tracing::instrument(skip_all)] /// Produce the fuel block with specified components async fn produce_block( @@ -1364,7 +1403,7 @@ where let input = mint.input_contract().clone(); let mut input = Input::Contract(input); - if self.options.extra_tx_checks { + if self.options.forbid_fake_coins { self.verify_inputs_exist_and_values_match( storage_tx, core::slice::from_ref(&input), @@ -1409,7 +1448,7 @@ where { let tx_id = checked_tx.id(); - if self.options.extra_tx_checks { + if self.options.forbid_fake_coins { checked_tx = self.extra_tx_checks(checked_tx, header, storage_tx, memory)?; } @@ -1798,22 +1837,78 @@ where Some(*header.height()), )?; - let mut vm = Interpreter::<_, _, _>::with_storage( - memory, - vm_db, - InterpreterParams::new(gas_price, &self.consensus_params), - ); + let mut reverted; - let vm_result: StateTransition<_> = vm - .transact(ready_tx) - .map_err(|error| ExecutorError::VmExecution { - error: error.to_string(), - transaction_id: tx_id, - })? - .into(); - let reverted = vm_result.should_revert(); + let (state, mut tx, receipts) = if !self.options.dry_run { + let mut vm = Interpreter::<_, _, _, NotSupportedEcal, + verification::Normal>::with_storage( + memory, + vm_db, + InterpreterParams::new(gas_price, &self.consensus_params), + ); + + let vm_result: StateTransition<_> = vm + .transact(ready_tx) + .map_err(|error| ExecutorError::VmExecution { + error: error.to_string(), + transaction_id: tx_id, + })? + .into(); + reverted = vm_result.should_revert(); + + vm_result.into_inner() + } else { + let mut vm = Interpreter::< + _, + _, + _, + NotSupportedEcal, + verification::AttemptContinue, + >::with_storage( + memory, + vm_db, + InterpreterParams::new(gas_price, &self.consensus_params), + ); + + let vm_result: StateTransition<_> = vm + .transact(ready_tx) + .map_err(|error| ExecutorError::VmExecution { + error: error.to_string(), + transaction_id: tx_id, + })? + .into(); + + reverted = vm_result.should_revert(); + + let (state, tx, mut receipts) = vm_result.into_inner(); + + // If transaction requires contract ids, then extend receipts with + // `PanicReason::ContractNotInInputs` for each missing contract id. + // The data like `$pc` or `$is` is not available in this case, + // because panic generated outside of the execution. + if !vm.verifier().missing_contract_inputs.is_empty() { + debug_assert!(self.options.dry_run); + reverted = true; + + let reason = PanicInstruction::error( + PanicReason::ContractNotInInputs, + op::noop().into(), + ); + + for contract_id in &vm.verifier().missing_contract_inputs { + receipts.push(Receipt::Panic { + id: ContractId::zeroed(), + reason, + pc: 0, + is: 0, + contract_id: Some(*contract_id), + }); + } + } + + (state, tx, receipts) + }; - let (state, mut tx, receipts): (_, Tx, _) = vm_result.into_inner(); #[cfg(debug_assertions)] { tx.precompute(&self.consensus_params.chain_id())?; @@ -1828,7 +1923,6 @@ where // only commit state changes if execution was a success if !reverted { - self.log_backtrace(&vm, &receipts); let changes = sub_block_db_commit.into_changes(); storage_tx.commit_changes(changes)?; } @@ -2055,7 +2149,7 @@ where }) => { let contract = ContractRef::new(db, *contract_id); let utxo_info = - contract.validated_utxo(self.options.extra_tx_checks)?; + contract.validated_utxo(self.options.forbid_fake_coins)?; *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); *balance_root = contract.balance_root()?; @@ -2117,7 +2211,7 @@ where where T: KeyValueInspect, { - if self.options.extra_tx_checks { + if self.options.forbid_fake_coins { db.storage::() .get(&utxo_id)? .ok_or(ExecutorError::TransactionValidity( @@ -2137,38 +2231,6 @@ where } } - /// Log a VM backtrace if configured to do so - fn log_backtrace( - &self, - vm: &Interpreter, Tx, Ecal>, - receipts: &[Receipt], - ) where - M: Memory, - { - if self.options.backtrace { - if let Some(backtrace) = receipts - .iter() - .find_map(Receipt::result) - .copied() - .map(|result| FuelBacktrace::from_vm_error(vm, result)) - { - let sp = usize::try_from(backtrace.registers()[RegId::SP]).expect( - "The `$sp` register points to the memory of the VM. \ - Because the VM's memory is limited by the `usize` of the system, \ - it is impossible to lose higher bits during truncation.", - ); - warn!( - target = "vm", - "Backtrace on contract: 0x{:x}\nregisters: {:?}\ncall_stack: {:?}\nstack\n: {}", - backtrace.contract(), - backtrace.registers(), - backtrace.call_stack(), - hex::encode(backtrace.memory().read(0usize, sp).expect("`SP` always within stack")), // print stack - ); - } - } - } - fn persist_output_utxos( &self, block_height: BlockHeight, diff --git a/crates/services/producer/src/ports.rs b/crates/services/producer/src/ports.rs index 349f0408e55..650f8e499d0 100644 --- a/crates/services/producer/src/ports.rs +++ b/crates/services/producer/src/ports.rs @@ -105,12 +105,12 @@ pub trait BlockProducer: Send + Sync { pub trait DryRunner: Send + Sync { /// Executes the block without committing it to the database. During execution collects the - /// receipts to return them. The `utxo_validation` field can be used to disable the validation + /// receipts to return them. The `forbid_fake_coins` field can be used to enable/disable the validation /// of utxos during execution. The `at_height` field can be used to dry run on top of a past block. fn dry_run( &self, block: Components>, - utxo_validation: Option, + forbid_fake_coins: Option, at_height: Option, ) -> ExecutorResult>; } diff --git a/crates/services/upgradable-executor/src/config.rs b/crates/services/upgradable-executor/src/config.rs index ada809dcf8a..cb1e329b533 100644 --- a/crates/services/upgradable-executor/src/config.rs +++ b/crates/services/upgradable-executor/src/config.rs @@ -3,10 +3,8 @@ use fuel_core_types::blockchain::header::StateTransitionBytecodeVersion; #[derive(Clone, Debug, Default)] pub struct Config { - /// Print execution backtraces if transaction execution reverts. - pub backtrace: bool, - /// Default mode for utxo_validation - pub utxo_validation_default: bool, + /// Default mode for `forbid_fake_coins` in `ExecutionOptions`. + pub forbid_fake_coins_default: bool, /// The version of the native executor to determine usage of native vs WASM executor. /// If it is `None`, the `Executor::VERSION` is used. /// @@ -21,8 +19,8 @@ pub struct Config { impl From<&Config> for ExecutionOptions { fn from(value: &Config) -> Self { Self { - extra_tx_checks: value.utxo_validation_default, - backtrace: value.backtrace, + forbid_fake_coins: value.forbid_fake_coins_default, + backtrace: false, } } } diff --git a/crates/services/upgradable-executor/src/executor.rs b/crates/services/upgradable-executor/src/executor.rs index dc366438cdc..9386bb44b34 100644 --- a/crates/services/upgradable-executor/src/executor.rs +++ b/crates/services/upgradable-executor/src/executor.rs @@ -390,7 +390,7 @@ where pub fn dry_run( &self, component: Components>, - utxo_validation: Option, + forbid_fake_coins: Option, at_height: Option, ) -> ExecutorResult> { if at_height.is_some() && !self.config.allow_historical_execution { @@ -400,12 +400,12 @@ where } // fallback to service config value if no utxo_validation override is provided - let utxo_validation = - utxo_validation.unwrap_or(self.config.utxo_validation_default); + let forbid_fake_coins = + forbid_fake_coins.unwrap_or(self.config.forbid_fake_coins_default); let options = ExecutionOptions { - extra_tx_checks: utxo_validation, - backtrace: self.config.backtrace, + forbid_fake_coins, + backtrace: false, }; let component = Components { diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index fd2cdd2e906..e021bd278c2 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -76,6 +76,7 @@ pub mod fuel_vm { BlobData, }, util, + verification, }; } diff --git a/tests/tests/assemble_tx.rs b/tests/tests/assemble_tx.rs index 9fa5e82e419..5a138432202 100644 --- a/tests/tests/assemble_tx.rs +++ b/tests/tests/assemble_tx.rs @@ -1,6 +1,7 @@ use fuel_core::{ chain_config::{ ChainConfig, + ContractConfig, StateConfig, TESTNET_WALLET_SECRETS, }, @@ -15,13 +16,22 @@ use fuel_core_client::client::{ ChangePolicy, RequiredBalance, }, + primitives::{ + Bytes32, + ContractId, + }, CoinType, + TransactionStatus, }, FuelClient, }; use fuel_core_types::{ blockchain::transaction::TransactionExt, - fuel_asm::op, + fuel_asm::{ + op, + GTFArgs, + RegId, + }, fuel_crypto::SecretKey, fuel_tx::{ policies::Policies, @@ -32,7 +42,10 @@ use fuel_core_types::{ Transaction, TransactionBuilder, TxPointer, + Word, }, + fuel_types::canonical::Serialize, + fuel_vm::consts::WORD_SIZE, services::executor::TransactionExecutionResult, }; use test_helpers::{ @@ -351,3 +364,143 @@ async fn assemble_transaction__adds_change_output_for_non_required_non_base_bala assert!(outputs[1].is_change()); assert_eq!(outputs[1].asset_id(), Some(&base_asset_id)); } + +const NUMBER_OF_CONTRACT: usize = 250; +const CALL_SIZE: usize = ContractId::LEN + WORD_SIZE * 2; + +#[tokio::test] +async fn assemble_transaction__adds_automatically_250_contracts() { + let mut state_config = StateConfig::local_testnet(); + let chain_config = ChainConfig::local_testnet(); + const CONTRACT_ID_REGISTER: RegId = RegId::new(0x10); + const OFFSET_REGISTER: RegId = RegId::new(0x11); + + // Given + let mut script = vec![]; + let mut script_data = vec![]; + + for i in 0..NUMBER_OF_CONTRACT { + let contract_id = ContractId::new([(i + 1) as u8; 32]); + let config = ContractConfig { + contract_id, + code: vec![op::ret(1)].into_iter().collect(), + tx_id: Bytes32::new([i as u8; 32]), + output_index: Default::default(), + tx_pointer_block_height: Default::default(), + tx_pointer_tx_idx: Default::default(), + states: Default::default(), + balances: Default::default(), + }; + state_config.contracts.push(config); + + let call_ith_contract = vec![ + op::movi(OFFSET_REGISTER, CALL_SIZE.try_into().unwrap()), + op::muli(OFFSET_REGISTER, OFFSET_REGISTER, i.try_into().unwrap()), + // Get pointer to the start of script data + op::gtf_args(CONTRACT_ID_REGISTER, 0x00, GTFArgs::ScriptData), + // Shift pointer to i'th contract in the script data. + op::add(CONTRACT_ID_REGISTER, CONTRACT_ID_REGISTER, OFFSET_REGISTER), + op::call(CONTRACT_ID_REGISTER, RegId::ZERO, 0x11, RegId::CGAS), + ]; + let script_data_to_call_ith_contract = contract_id + .to_bytes() + .into_iter() + .chain(Word::MIN.to_be_bytes()) + .chain(Word::MIN.to_be_bytes()); + + script.extend(call_ith_contract); + script_data.extend(script_data_to_call_ith_contract); + } + // Return success at the end of the script + script.push(op::ret(1)); + + let mut config = Config::local_node_with_configs(chain_config, state_config); + config.utxo_validation = true; + config.gas_price_config.min_exec_gas_price = 1000; + + let service = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(service.bound_address); + + // When + let status = client + .run_script( + script.into_iter().collect(), + script_data, + default_signing_wallet(), + ) + .await + .unwrap(); + + // Then + assert!( + matches!(status, TransactionStatus::Success { .. }), + "{:?}", + status + ); +} + +#[tokio::test] +async fn assemble_transaction__adds_automatically_contracts__the_same_contract_twice() { + let mut state_config = StateConfig::local_testnet(); + let chain_config = ChainConfig::local_testnet(); + const CONTRACT_ID_REGISTER: RegId = RegId::new(0x10); + + // Given + let contract_id = ContractId::new([1; 32]); + let config = ContractConfig { + contract_id, + code: vec![op::ret(1)].into_iter().collect(), + tx_id: Bytes32::new([123; 32]), + output_index: Default::default(), + tx_pointer_block_height: Default::default(), + tx_pointer_tx_idx: Default::default(), + states: Default::default(), + balances: Default::default(), + }; + state_config.contracts.push(config); + + let mut script = vec![]; + let mut script_data = vec![]; + + for _ in 0..2 { + let call_ith_contract = vec![ + // Get pointer to the start of script data + op::gtf_args(CONTRACT_ID_REGISTER, 0x00, GTFArgs::ScriptData), + op::call(CONTRACT_ID_REGISTER, RegId::ZERO, 0x11, RegId::CGAS), + ]; + let script_data_to_call_ith_contract = contract_id + .to_bytes() + .into_iter() + .chain(Word::MIN.to_be_bytes()) + .chain(Word::MIN.to_be_bytes()); + + script.extend(call_ith_contract); + script_data.extend(script_data_to_call_ith_contract); + } + // Return success at the end of the script + script.push(op::ret(1)); + + let mut config = Config::local_node_with_configs(chain_config, state_config); + config.utxo_validation = true; + config.gas_price_config.min_exec_gas_price = 1000; + + let service = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(service.bound_address); + + // When + let status = client + .run_script( + script.into_iter().collect(), + script_data, + default_signing_wallet(), + ) + .await + .unwrap(); + + // Then + assert!( + matches!(status, TransactionStatus::Success { .. }), + "{:?}", + status + ); +} diff --git a/tests/tests/node_info.rs b/tests/tests/node_info.rs index b21a3673540..9982d528d10 100644 --- a/tests/tests/node_info.rs +++ b/tests/tests/node_info.rs @@ -19,14 +19,12 @@ async fn node_info() { let NodeInfo { utxo_validation, - vm_backtrace, max_depth, max_tx, .. } = client.node_info().await.unwrap(); assert_eq!(utxo_validation, node_config.utxo_validation); - assert_eq!(vm_backtrace, node_config.vm.backtrace); assert_eq!(max_depth, node_config.txpool.max_txs_chain_count as u64); assert_eq!(max_tx, node_config.txpool.pool_limits.max_txs as u64); }