From 35829bb2fce20c8942a747000fec1ba406741acf Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Wed, 9 Apr 2025 14:48:32 -0600 Subject: [PATCH 01/13] Fix compiler errors from new version of fuel-vm --- Cargo.lock | 1 - crates/compression/src/decompress.rs | 49 +++++++- crates/fuel-core/src/schema/tx/assemble_tx.rs | 6 +- crates/fuel-core/src/schema/tx/input.rs | 107 +++++++++++++++++- crates/services/executor/src/executor.rs | 31 +++-- crates/services/txpool_v2/src/config.rs | 3 + .../txpool_v2/src/extracted_outputs.rs | 3 +- .../services/txpool_v2/src/storage/graph.rs | 37 ++++-- 8 files changed, 211 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d891d6bc56e..fc56e932758 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4381,7 +4381,6 @@ dependencies = [ "strum 0.24.1", "substrate-bn", "tai64", - "tracing", ] [[package]] diff --git a/crates/compression/src/decompress.rs b/crates/compression/src/decompress.rs index 5273945aef0..9c7de09b762 100644 --- a/crates/compression/src/decompress.rs +++ b/crates/compression/src/decompress.rs @@ -67,7 +67,10 @@ pub mod fault_proving { #[cfg(feature = "fault-proving")] use fault_proving::DecompressDb; - +use fuel_core_types::fuel_tx::input::coin::{ + UnverifiedCoin, + UnverifiedDataCoin, +}; #[cfg(not(feature = "fault-proving"))] use not_fault_proving::DecompressDb; @@ -254,6 +257,50 @@ where } } +impl DecompressibleBy> for UnverifiedCoin +where + D: DecompressDb, +{ + async fn decompress_with( + c: ::Compressed, + ctx: &DecompressCtx, + ) -> anyhow::Result { + let utxo_id = UtxoId::decompress_with(c.utxo_id, ctx).await?; + let coin_info = ctx.db.coin(utxo_id)?; + let tx_pointer = Default::default(); + Ok(Self { + utxo_id, + owner: coin_info.owner, + amount: coin_info.amount, + asset_id: coin_info.asset_id, + tx_pointer, + }) + } +} + +impl DecompressibleBy> for UnverifiedDataCoin +where + D: DecompressDb, +{ + async fn decompress_with( + c: ::Compressed, + ctx: &DecompressCtx, + ) -> anyhow::Result { + let utxo_id = UtxoId::decompress_with(c.utxo_id, ctx).await?; + let coin_info = ctx.db.coin(utxo_id)?; + let tx_pointer = Default::default(); + let data = c.data.decompress(ctx).await?; + Ok(Self { + utxo_id, + owner: coin_info.owner, + amount: coin_info.amount, + asset_id: coin_info.asset_id, + tx_pointer, + data, + }) + } +} + impl DecompressibleBy> for Message where D: DecompressDb, diff --git a/crates/fuel-core/src/schema/tx/assemble_tx.rs b/crates/fuel-core/src/schema/tx/assemble_tx.rs index 686bf6f22c1..916d755e0b8 100644 --- a/crates/fuel-core/src/schema/tx/assemble_tx.rs +++ b/crates/fuel-core/src/schema/tx/assemble_tx.rs @@ -249,6 +249,9 @@ where set_contracts.insert(c.contract_id); } + Input::ReadOnly(_) => { + // Doesn't require witness + } Input::CoinPredicate(_) | Input::DataCoinPredicate(_) | Input::MessageCoinPredicate(_) @@ -753,7 +756,8 @@ where | Input::DataCoinPredicate(_) | Input::MessageCoinSigned(_) | Input::MessageCoinPredicate(_) => true, - Input::MessageDataSigned(_) + Input::ReadOnly(_) + | Input::MessageDataSigned(_) | Input::MessageDataPredicate(_) | Input::Contract(_) => false, }); diff --git a/crates/fuel-core/src/schema/tx/input.rs b/crates/fuel-core/src/schema/tx/input.rs index 08808dbfae1..7205d84ad94 100644 --- a/crates/fuel-core/src/schema/tx/input.rs +++ b/crates/fuel-core/src/schema/tx/input.rs @@ -3,8 +3,6 @@ use async_graphql::{ Union, }; -use fuel_core_types::fuel_tx; - use crate::schema::scalars::{ Address, AssetId, @@ -17,6 +15,10 @@ use crate::schema::scalars::{ U16, U64, }; +use fuel_core_types::{ + fuel_tx, + fuel_tx::input::coin::UnverifiedCoin, +}; #[derive(Union)] pub enum Input { @@ -24,6 +26,8 @@ pub enum Input { DataCoin(InputDataCoin), Contract(InputContract), Message(InputMessage), + ReadOnlyCoin(InputUnverifiedCoin), + ReadOnlyDataCoin(InputUnverifiedDataCoin), } pub struct InputCoin { @@ -133,6 +137,73 @@ impl InputDataCoin { } } +pub struct InputUnverifiedCoin { + utxo_id: UtxoId, + owner: Address, + amount: U64, + asset_id: AssetId, + tx_pointer: TxPointer, +} + +#[Object] +impl InputUnverifiedCoin { + async fn utxo_id(&self) -> UtxoId { + self.utxo_id + } + + async fn owner(&self) -> Address { + self.owner + } + + async fn amount(&self) -> U64 { + self.amount + } + + async fn asset_id(&self) -> AssetId { + self.asset_id + } + + async fn tx_pointer(&self) -> TxPointer { + self.tx_pointer + } +} + +#[Object] +impl InputUnverifiedDataCoin { + async fn utxo_id(&self) -> UtxoId { + self.utxo_id + } + + async fn owner(&self) -> Address { + self.owner + } + + async fn amount(&self) -> U64 { + self.amount + } + + async fn asset_id(&self) -> AssetId { + self.asset_id + } + + async fn tx_pointer(&self) -> TxPointer { + self.tx_pointer + } + + async fn data(&self) -> HexString { + self.data.clone() + } +} + +pub struct InputUnverifiedDataCoin { + utxo_id: UtxoId, + owner: Address, + amount: U64, + asset_id: AssetId, + tx_pointer: TxPointer, + data: HexString, +} + pub struct InputContract { utxo_id: UtxoId, balance_root: Bytes32, @@ -302,6 +373,38 @@ impl From<&fuel_tx::Input> for Input { predicate_data: HexString(predicate_data.clone()), data: HexString(Default::default()), }), + fuel_tx::Input::ReadOnly(inner) => match inner { + fuel_tx::input::ReadOnly::Coin(UnverifiedCoin { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + }) => Input::ReadOnlyCoin(InputUnverifiedCoin { + utxo_id: UtxoId(*utxo_id), + owner: Address(*owner), + amount: (*amount).into(), + asset_id: AssetId(*asset_id), + tx_pointer: TxPointer(*tx_pointer), + }), + fuel_tx::input::ReadOnly::DataCoin( + fuel_tx::input::coin::UnverifiedDataCoin { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + data, + }, + ) => Input::ReadOnlyDataCoin(InputUnverifiedDataCoin { + utxo_id: UtxoId(*utxo_id), + owner: Address(*owner), + amount: (*amount).into(), + asset_id: AssetId(*asset_id), + tx_pointer: TxPointer(*tx_pointer), + data: HexString(data.clone()), + }), + }, fuel_tx::Input::Contract(contract) => Input::Contract(contract.into()), fuel_tx::Input::MessageCoinSigned( fuel_tx::input::message::MessageCoinSigned { diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 274ec51ff16..0e075d3eef8 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -16,6 +16,16 @@ use tracing::{ warn, }; +use crate::{ + ports::{ + MaybeCheckedTransaction, + NewTxWaiterPort, + PreconfirmationSenderPort, + RelayerPort, + TransactionsSource, + }, + refs::ContractRef, +}; use fuel_core_storage::{ column::Column, kv_store::KeyValueInspect, @@ -85,6 +95,8 @@ use fuel_core_types::{ CoinSigned, DataCoinPredicate, DataCoinSigned, + UnverifiedCoin, + UnverifiedDataCoin, }, message::{ MessageCoinPredicate, @@ -92,6 +104,7 @@ use fuel_core_types::{ MessageDataPredicate, MessageDataSigned, }, + ReadOnly, }, output, Address, @@ -162,17 +175,6 @@ use fuel_core_types::{ }, }; -use crate::{ - ports::{ - MaybeCheckedTransaction, - NewTxWaiterPort, - PreconfirmationSenderPort, - RelayerPort, - TransactionsSource, - }, - refs::ContractRef, -}; - /// The maximum amount of transactions that can be included in a block, /// excluding the mint transaction. #[cfg(not(feature = "limited-tx-count"))] @@ -1961,7 +1963,12 @@ where Input::CoinSigned(CoinSigned { utxo_id, .. }) | Input::DataCoinSigned(DataCoinSigned { utxo_id, .. }) | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) - | Input::DataCoinPredicate(DataCoinPredicate { utxo_id, .. }) => { + | Input::DataCoinPredicate(DataCoinPredicate { utxo_id, .. }) + | Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { + utxo_id, + .. + })) + | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { utxo_id, .. })) => { if let Some(coin) = db.storage::().get(utxo_id)? { if !coin.matches_input(input).unwrap_or_default() { return Err( diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index ef0c644067a..f0523ede693 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -77,6 +77,9 @@ impl BlackList { return Err(BlacklistedError::BlacklistedOwner(*owner)); } } + Input::ReadOnly(_) => { + // No reason to prevent reading from read-only input + } Input::Contract(contract) => { if self.contracts.contains(&contract.contract_id) { return Err(BlacklistedError::BlacklistedContract( diff --git a/crates/services/txpool_v2/src/extracted_outputs.rs b/crates/services/txpool_v2/src/extracted_outputs.rs index c05882b3700..82de5ea3862 100644 --- a/crates/services/txpool_v2/src/extracted_outputs.rs +++ b/crates/services/txpool_v2/src/extracted_outputs.rs @@ -133,7 +133,8 @@ impl ExtractedOutputs { coins.remove(&utxo_id.output_index()); }); } - Input::Contract(_) + Input::ReadOnly(_) + | Input::Contract(_) | Input::MessageCoinPredicate(_) | Input::MessageCoinSigned(_) | Input::MessageDataPredicate(_) diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 5e43e3214aa..7653b06d0e4 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -7,6 +7,11 @@ use std::{ time::SystemTime, }; +use super::{ + RemovedTransactions, + Storage, + StorageData, +}; use crate::{ error::{ DependencyError, @@ -28,6 +33,8 @@ use fuel_core_types::{ CoinSigned, DataCoinPredicate, DataCoinSigned, + UnverifiedCoin, + UnverifiedDataCoin, }, contract::Contract, message::{ @@ -36,6 +43,7 @@ use fuel_core_types::{ MessageDataPredicate, MessageDataSigned, }, + ReadOnly, }, ContractId, Input, @@ -53,12 +61,6 @@ use petgraph::{ prelude::StableDiGraph, }; -use super::{ - RemovedTransactions, - Storage, - StorageData, -}; - pub struct GraphStorage { /// The configuration of the graph config: GraphConfig, @@ -420,7 +422,12 @@ impl GraphStorage { Input::CoinSigned(CoinSigned { utxo_id, .. }) | Input::DataCoinSigned(DataCoinSigned { utxo_id, .. }) | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) - | Input::DataCoinPredicate(DataCoinPredicate { utxo_id, .. }) => { + | Input::DataCoinPredicate(DataCoinPredicate { utxo_id, .. }) + | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { utxo_id, .. })) + | Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { + utxo_id, + .. + })) => { if let Some(node_id) = self.coins_creators.get(utxo_id) { direct_dependencies.insert(*node_id); @@ -740,7 +747,21 @@ impl Storage for GraphStorage { amount, asset_id, .. - }) => { + }) + | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { + utxo_id, + owner, + amount, + asset_id, + .. + })) + | Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { + utxo_id, + owner, + amount, + asset_id, + .. + })) => { if let Some(node_id) = self.coins_creators.get(utxo_id) { let Some(node) = self.graph.node_weight(*node_id) else { return Err(InputValidationErrorType::Inconsistency( From e42050adc5554b6ad5cc8f7adb8c01ec5b1dfbed Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Wed, 9 Apr 2025 15:16:10 -0600 Subject: [PATCH 02/13] Add test for reading from read-only input --- crates/fuel-core/src/executor.rs | 135 +++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index e6965e1c606..2e43bec0cf7 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -3619,6 +3619,141 @@ mod tests { assert!(result.is_ok(), "{result:?}") } + #[test] + fn validate_predicate_succeeds_if_read_only_input_matches_output_data() { + let mut rng = StdRng::seed_from_u64(2322u64); + + // given + let predicate: Vec = + predicates_checking_input_data_coin_data_matches_output_data_coin_data(); + let owner = Input::predicate_owner(&predicate); + let amount = 1000; + let other_amount = 123; + + let consensus_parameters = ConsensusParameters::default(); + let config = Config { + forbid_fake_coins_default: true, + consensus_parameters: consensus_parameters.clone(), + }; + let data = vec![99u8; 100]; + let predicate_data = data.clone(); + let true_predicate = vec![op::ret(0x01)].into_iter().collect(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + + let mut tx = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(amount) + .add_input(Input::data_coin_predicate( + rng.gen(), + owner, + other_amount, + AssetId::BASE, + rng.gen(), + 0, + predicate, + predicate_data, + data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + true_predicate_owner, + amount, + AssetId::BASE, + rng.gen(), + 0, + true_predicate, + vec![], + )) + .add_output(Output::DataCoin { + to: Default::default(), + amount: 100, + asset_id: AssetId::BASE, + data: data.clone(), + }) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + tx.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); + let db = &mut Database::default(); + + // insert data coin into state + if let Input::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = tx.inputs()[0] + { + let coin = CompressedCoin::V2(CompressedCoinV2 { + owner, + amount, + asset_id, + tx_pointer, + data, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + // insert coin to cover output value + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = tx.inputs()[1] + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + let producer = create_executor(db.clone(), config.clone()); + + // when + let ExecutionResult { + block, + skipped_transactions, + .. + } = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: PartialBlockHeader::default(), + transactions_source: OnceTransactionsSource::new(vec![tx.into()]), + coinbase_recipient: Default::default(), + gas_price: 1, + }) + .unwrap() + .into_result(); + + // then + assert!(skipped_transactions.is_empty(), "{skipped_transactions:?}"); + + let validator = create_executor(db.clone(), config); + let result = validator.validate(&block); + assert!(result.is_ok(), "{result:?}") + } + #[test] fn verifying_during_production_consensus_parameters_version_works() { let mut rng = StdRng::seed_from_u64(2322u64); From 4e4ed7b9b5220525fe4a93f4ffa1f474d1079c8e Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Wed, 9 Apr 2025 15:55:40 -0600 Subject: [PATCH 03/13] Fix test to actually use read-only data coin --- crates/fuel-core/src/executor.rs | 16 ++++++------- crates/types/src/entities/coins/coin.rs | 32 +++++++++++++++++++------ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 2e43bec0cf7..147b42d0865 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -100,9 +100,11 @@ mod tests { CoinPredicate, CoinSigned, DataCoinPredicate, + UnverifiedDataCoin, }, contract, Input, + ReadOnly, }, policies::PolicyType, Bytes32, @@ -3620,7 +3622,7 @@ mod tests { } #[test] - fn validate_predicate_succeeds_if_read_only_input_matches_output_data() { + fn validate__predicate_succeeds_if_read_only_input_matches_output_data() { let mut rng = StdRng::seed_from_u64(2322u64); // given @@ -3636,7 +3638,6 @@ mod tests { consensus_parameters: consensus_parameters.clone(), }; let data = vec![99u8; 100]; - let predicate_data = data.clone(); let true_predicate = vec![op::ret(0x01)].into_iter().collect(); let true_predicate_owner = Input::predicate_owner(&true_predicate); @@ -3644,16 +3645,13 @@ mod tests { vec![op::ret(RegId::ONE)].into_iter().collect(), vec![], ) - .max_fee_limit(amount) - .add_input(Input::data_coin_predicate( + .max_fee_limit(100) + .add_input(Input::read_only_data_coin( rng.gen(), owner, other_amount, AssetId::BASE, rng.gen(), - 0, - predicate, - predicate_data, data.clone(), )) .add_input(Input::coin_predicate( @@ -3687,14 +3685,14 @@ mod tests { let db = &mut Database::default(); // insert data coin into state - if let Input::DataCoinPredicate(DataCoinPredicate { + if let Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { utxo_id, owner, amount, asset_id, tx_pointer, .. - }) = tx.inputs()[0] + })) = tx.inputs()[0] { let coin = CompressedCoin::V2(CompressedCoinV2 { owner, diff --git a/crates/types/src/entities/coins/coin.rs b/crates/types/src/entities/coins/coin.rs index a09abe23b79..030866edd61 100644 --- a/crates/types/src/entities/coins/coin.rs +++ b/crates/types/src/entities/coins/coin.rs @@ -3,11 +3,16 @@ use crate::{ fuel_asm::Word, fuel_tx::{ - input::coin::{ - CoinPredicate, - CoinSigned, - DataCoinPredicate, - DataCoinSigned, + input::{ + coin::{ + CoinPredicate, + CoinSigned, + DataCoinPredicate, + DataCoinSigned, + UnverifiedCoin, + UnverifiedDataCoin, + }, + ReadOnly, }, Input, TxPointer, @@ -316,7 +321,13 @@ impl CompressedCoin { amount, asset_id, .. - }) => match self { + }) + | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { + owner, + amount, + asset_id, + .. + })) => match self { CompressedCoin::V1(coin) => Some( owner == &coin.owner && amount == &coin.amount @@ -337,7 +348,14 @@ impl CompressedCoin { asset_id, data, .. - }) => match self { + }) + | Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { + owner, + amount, + asset_id, + data, + .. + })) => match self { CompressedCoin::V2(coin) => Some( owner == &coin.owner && amount == &coin.amount From 2f4616307bbe93e7764fd59478ba35247e6c7317 Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Thu, 10 Apr 2025 14:30:43 -0600 Subject: [PATCH 04/13] Get compiling with verified read-only coins --- crates/fuel-core/src/schema/tx/input.rs | 168 +++++++++++++++++- crates/services/executor/src/executor.rs | 10 +- .../services/txpool_v2/src/storage/graph.rs | 22 +++ 3 files changed, 196 insertions(+), 4 deletions(-) diff --git a/crates/fuel-core/src/schema/tx/input.rs b/crates/fuel-core/src/schema/tx/input.rs index 7205d84ad94..07994f7f08b 100644 --- a/crates/fuel-core/src/schema/tx/input.rs +++ b/crates/fuel-core/src/schema/tx/input.rs @@ -17,7 +17,14 @@ use crate::schema::scalars::{ }; use fuel_core_types::{ fuel_tx, - fuel_tx::input::coin::UnverifiedCoin, + fuel_tx::input::{ + coin::{ + CoinPredicate, + DataCoinPredicate, + UnverifiedCoin, + }, + ReadOnly, + }, }; #[derive(Union)] @@ -28,6 +35,8 @@ pub enum Input { Message(InputMessage), ReadOnlyCoin(InputUnverifiedCoin), ReadOnlyDataCoin(InputUnverifiedDataCoin), + ReadOnlyPredicateCoin(InputVerifiedCoin), + ReadOnlyPredicateDataCoin(InputVerifiedDataCoin), } pub struct InputCoin { @@ -204,6 +213,113 @@ pub struct InputUnverifiedDataCoin { data: HexString, } +pub struct InputVerifiedCoin { + utxo_id: UtxoId, + owner: Address, + amount: U64, + asset_id: AssetId, + tx_pointer: TxPointer, + witness_index: u16, + predicate_gas_used: U64, + predicate: HexString, + predicate_data: HexString, +} + +#[Object] +impl InputVerifiedCoin { + async fn utxo_id(&self) -> UtxoId { + self.utxo_id + } + + async fn owner(&self) -> Address { + self.owner + } + + async fn amount(&self) -> U64 { + self.amount + } + + async fn asset_id(&self) -> AssetId { + self.asset_id + } + + async fn tx_pointer(&self) -> TxPointer { + self.tx_pointer + } + + async fn witness_index(&self) -> U16 { + self.witness_index.into() + } + + async fn predicate_gas_used(&self) -> U64 { + self.predicate_gas_used + } + + async fn predicate(&self) -> HexString { + self.predicate.clone() + } + + async fn predicate_data(&self) -> HexString { + self.predicate_data.clone() + } +} + +pub struct InputVerifiedDataCoin { + utxo_id: UtxoId, + owner: Address, + amount: U64, + asset_id: AssetId, + tx_pointer: TxPointer, + witness_index: u16, + predicate_gas_used: U64, + predicate: HexString, + predicate_data: HexString, + data: HexString, +} + +#[Object] +impl InputVerifiedDataCoin { + async fn utxo_id(&self) -> UtxoId { + self.utxo_id + } + + async fn owner(&self) -> Address { + self.owner + } + + async fn amount(&self) -> U64 { + self.amount + } + + async fn asset_id(&self) -> AssetId { + self.asset_id + } + + async fn tx_pointer(&self) -> TxPointer { + self.tx_pointer + } + + async fn witness_index(&self) -> U16 { + self.witness_index.into() + } + + async fn predicate_gas_used(&self) -> U64 { + self.predicate_gas_used + } + + async fn predicate(&self) -> HexString { + self.predicate.clone() + } + + async fn predicate_data(&self) -> HexString { + self.predicate_data.clone() + } + + async fn data(&self) -> HexString { + self.data.clone() + } +} + pub struct InputContract { utxo_id: UtxoId, balance_root: Bytes32, @@ -356,10 +472,11 @@ impl From<&fuel_tx::Input> for Input { amount, asset_id, tx_pointer, + witness_index: _, predicate, predicate_data, predicate_gas_used, - .. + data, }, ) => Input::DataCoin(InputDataCoin { utxo_id: UtxoId(*utxo_id), @@ -371,7 +488,7 @@ impl From<&fuel_tx::Input> for Input { predicate_gas_used: (*predicate_gas_used).into(), predicate: HexString(predicate.to_vec()), predicate_data: HexString(predicate_data.clone()), - data: HexString(Default::default()), + data: HexString(data.clone()), }), fuel_tx::Input::ReadOnly(inner) => match inner { fuel_tx::input::ReadOnly::Coin(UnverifiedCoin { @@ -404,7 +521,52 @@ impl From<&fuel_tx::Input> for Input { tx_pointer: TxPointer(*tx_pointer), data: HexString(data.clone()), }), + ReadOnly::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + witness_index: _, + predicate_gas_used, + predicate, + predicate_data, + }) => Input::ReadOnlyPredicateCoin(InputVerifiedCoin { + utxo_id: UtxoId(*utxo_id), + owner: Address(*owner), + amount: (*amount).into(), + asset_id: AssetId(*asset_id), + tx_pointer: TxPointer(*tx_pointer), + witness_index: Default::default(), + predicate_gas_used: (*predicate_gas_used).into(), + predicate: HexString(predicate.to_vec()), + predicate_data: HexString(predicate_data.clone()), + }), + ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + witness_index: _, + predicate_gas_used, + predicate, + predicate_data, + data, + }) => Input::ReadOnlyPredicateDataCoin(InputVerifiedDataCoin { + utxo_id: UtxoId(*utxo_id), + owner: Address(*owner), + amount: (*amount).into(), + asset_id: AssetId(*asset_id), + tx_pointer: TxPointer(*tx_pointer), + witness_index: Default::default(), + predicate_gas_used: (*predicate_gas_used).into(), + predicate: HexString(predicate.to_vec()), + predicate_data: HexString(predicate_data.clone()), + data: HexString(data.clone()), + }), }, + fuel_tx::Input::Contract(contract) => Input::Contract(contract.into()), fuel_tx::Input::MessageCoinSigned( fuel_tx::input::message::MessageCoinSigned { diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 0e075d3eef8..79691aec2da 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1968,7 +1968,15 @@ where utxo_id, .. })) - | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { utxo_id, .. })) => { + | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { utxo_id, .. })) + | Input::ReadOnly(ReadOnly::CoinPredicate(CoinPredicate { + utxo_id, + .. + })) + | Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + .. + })) => { if let Some(coin) = db.storage::().get(utxo_id)? { if !coin.matches_input(input).unwrap_or_default() { return Err( diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 7653b06d0e4..f1fcc558af6 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -427,6 +427,14 @@ impl GraphStorage { | Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { utxo_id, .. + })) + | Input::ReadOnly(ReadOnly::CoinPredicate(CoinPredicate { + utxo_id, + .. + })) + | Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + .. })) => { if let Some(node_id) = self.coins_creators.get(utxo_id) { direct_dependencies.insert(*node_id); @@ -761,6 +769,20 @@ impl Storage for GraphStorage { amount, asset_id, .. + })) + | Input::ReadOnly(ReadOnly::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + .. + })) + | Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + .. })) => { if let Some(node_id) = self.coins_creators.get(utxo_id) { let Some(node) = self.graph.node_weight(*node_id) else { From 5455083d1fee0e665987e6b93945d0bada04ff03 Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Thu, 10 Apr 2025 15:43:50 -0600 Subject: [PATCH 05/13] Add verified read-only test --- crates/fuel-core/src/executor.rs | 149 +++++++++++++++++++++-- crates/services/executor/src/executor.rs | 11 ++ crates/types/src/entities/coins/coin.rs | 13 ++ 3 files changed, 166 insertions(+), 7 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 147b42d0865..7c9157ead10 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -3620,9 +3620,8 @@ mod tests { let result = validator.validate(&block); assert!(result.is_ok(), "{result:?}") } - #[test] - fn validate__predicate_succeeds_if_read_only_input_matches_output_data() { + fn validate__predicate_succeeds_if_read_only_input_data_matches_output_data() { let mut rng = StdRng::seed_from_u64(2322u64); // given @@ -3638,8 +3637,8 @@ mod tests { consensus_parameters: consensus_parameters.clone(), }; let data = vec![99u8; 100]; - let true_predicate = vec![op::ret(0x01)].into_iter().collect(); - let true_predicate_owner = Input::predicate_owner(&true_predicate); + // let true_predicate = vec![op::ret(0x01)].into_iter().collect(); + // let true_predicate_owner = Input::predicate_owner(&true_predicate); let mut tx = TransactionBuilder::script( vec![op::ret(RegId::ONE)].into_iter().collect(), @@ -3648,7 +3647,7 @@ mod tests { .max_fee_limit(100) .add_input(Input::read_only_data_coin( rng.gen(), - owner, + rng.gen(), other_amount, AssetId::BASE, rng.gen(), @@ -3656,12 +3655,12 @@ mod tests { )) .add_input(Input::coin_predicate( rng.gen(), - true_predicate_owner, + owner, amount, AssetId::BASE, rng.gen(), 0, - true_predicate, + predicate, vec![], )) .add_output(Output::DataCoin { @@ -3752,6 +3751,142 @@ mod tests { assert!(result.is_ok(), "{result:?}") } + #[test] + fn validate__predicate_succeeds_if_read_only_predicate_input_data_matches_output_data( + ) { + let mut rng = StdRng::seed_from_u64(2322u64); + + // given + let predicate: Vec = + predicates_checking_input_data_coin_data_matches_output_data_coin_data(); + let owner = Input::predicate_owner(&predicate); + let amount = 1000; + let other_amount = 123; + + let consensus_parameters = ConsensusParameters::default(); + let config = Config { + forbid_fake_coins_default: true, + consensus_parameters: consensus_parameters.clone(), + }; + let data = vec![99u8; 100]; + let true_predicate = vec![op::ret(0x01)].into_iter().collect(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + + let mut tx = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(100) + .add_input(Input::read_only_data_coin_predicate( + rng.gen(), + true_predicate_owner, + other_amount, + AssetId::BASE, + rng.gen(), + 0, + true_predicate, + vec![], + data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + amount, + AssetId::BASE, + rng.gen(), + 0, + predicate, + vec![], + )) + .add_output(Output::DataCoin { + to: Default::default(), + amount: 100, + asset_id: AssetId::BASE, + data: data.clone(), + }) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + tx.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); + let db = &mut Database::default(); + + // insert data coin into state + if let Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + data, + .. + })) = &tx.inputs()[0] + { + let coin = CompressedCoin::V2(CompressedCoinV2 { + owner: *owner, + amount: *amount, + asset_id: *asset_id, + tx_pointer: *tx_pointer, + data: data.clone(), + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + // insert coin to cover output value + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = tx.inputs()[1] + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + let producer = create_executor(db.clone(), config.clone()); + + // when + let ExecutionResult { + block, + skipped_transactions, + .. + } = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: PartialBlockHeader::default(), + transactions_source: OnceTransactionsSource::new(vec![tx.into()]), + coinbase_recipient: Default::default(), + gas_price: 1, + }) + .unwrap() + .into_result(); + + // then + assert!(skipped_transactions.is_empty(), "{skipped_transactions:?}"); + + let validator = create_executor(db.clone(), config); + let result = validator.validate(&block); + assert!(result.is_ok(), "{result:?}") + } + #[test] fn verifying_during_production_consensus_parameters_version_works() { let mut rng = StdRng::seed_from_u64(2322u64); diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 79691aec2da..6164b3e4e80 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1761,6 +1761,17 @@ where }) => { *predicate_gas_used = gas_used; } + Input::ReadOnly( + ReadOnly::CoinPredicate(CoinPredicate { + predicate_gas_used, .. + }) + | ReadOnly::DataCoinPredicate(DataCoinPredicate { + predicate_gas_used, + .. + }), + ) => { + *predicate_gas_used = gas_used; + } _ => { debug_assert!(false, "This error is not possible unless VM changes the order of inputs, \ or we added a new predicate inputs."); diff --git a/crates/types/src/entities/coins/coin.rs b/crates/types/src/entities/coins/coin.rs index 030866edd61..f091b03e7a4 100644 --- a/crates/types/src/entities/coins/coin.rs +++ b/crates/types/src/entities/coins/coin.rs @@ -327,6 +327,12 @@ impl CompressedCoin { amount, asset_id, .. + })) + | Input::ReadOnly(ReadOnly::CoinPredicate(CoinPredicate { + owner, + amount, + asset_id, + .. })) => match self { CompressedCoin::V1(coin) => Some( owner == &coin.owner @@ -355,6 +361,13 @@ impl CompressedCoin { asset_id, data, .. + })) + | Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + owner, + amount, + asset_id, + data, + .. })) => match self { CompressedCoin::V2(coin) => Some( owner == &coin.owner From 6439d0ed37aceb41a976a214afc307bfedc2e371 Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Fri, 11 Apr 2025 14:11:26 -0600 Subject: [PATCH 06/13] WIP debugging read-only tx_id bug --- crates/client/src/client.rs | 3 + crates/fuel-core/src/schema/tx.rs | 8 +- crates/services/executor/src/executor.rs | 6 +- tests/test-helpers/src/builder.rs | 25 ++- tests/tests/tx/predicates.rs | 266 +++++++++++++++++++++-- 5 files changed, 286 insertions(+), 22 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1c3e6757ec7..d40bdb256b8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -70,6 +70,7 @@ use fuel_core_types::{ Receipt, Transaction, TxId, + UniqueIdentifier, }, fuel_types::{ self, @@ -908,6 +909,8 @@ impl FuelClient { estimate_predicates: Option, ) -> io::Result { use cynic::SubscriptionBuilder; + tracing::debug!("submitting tx: {:?}", tx.id(&Default::default()).to_bytes()); + tracing::debug!("encoding tx: {:?}", tx); let tx = tx.clone().to_bytes(); let s = schema::tx::SubmitAndAwaitSubscription::build(TxWithEstimatedPredicatesArg { diff --git a/crates/fuel-core/src/schema/tx.rs b/crates/fuel-core/src/schema/tx.rs index 595dad1d8e5..248e8bdaf58 100644 --- a/crates/fuel-core/src/schema/tx.rs +++ b/crates/fuel-core/src/schema/tx.rs @@ -79,7 +79,10 @@ use fuel_core_types::{ }, fuel_types::{ self, - canonical::Deserialize, + canonical::{ + Deserialize, + Serialize, + }, }, fuel_vm::checked_transaction::{ CheckPredicateParams, @@ -778,6 +781,9 @@ async fn submit_and_await_status<'a>( let mut tx = FuelTx::from_bytes(&tx.0)?; let tx_id = tx.id(¶ms.chain_id()); + tracing::debug!("Inserting tx into txpool: {:?}", tx_id.to_bytes()); + tracing::debug!("decoded tx: {:?}", tx); + if estimate_predicates { let query = ctx.read_view()?.into_owned(); tx = ctx.estimate_predicates(tx, query).await?; diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 6164b3e4e80..a0a8efca00d 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -125,7 +125,10 @@ use fuel_core_types::{ UtxoId, }, fuel_types::{ - canonical::Deserialize, + canonical::{ + Deserialize, + Serialize, + }, BlockHeight, ContractId, MessageId, @@ -787,6 +790,7 @@ where for transaction in regular_tx_iter { let tx_id = transaction.id(&self.consensus_params.chain_id()); let tx_max_gas = transaction.max_gas(&self.consensus_params)?; + debug!("Processing transaction {:?}", tx_id.to_bytes()); if tx_max_gas > remaining_gas_limit { data.skipped_transactions.push(( tx_id, diff --git a/tests/test-helpers/src/builder.rs b/tests/test-helpers/src/builder.rs index 4addb57b8e2..c05f0e47a13 100644 --- a/tests/test-helpers/src/builder.rs +++ b/tests/test-helpers/src/builder.rs @@ -25,11 +25,16 @@ use fuel_core_types::{ fuel_asm::op, fuel_tx::{ field::Inputs, - input::coin::{ - CoinPredicate, - CoinSigned, - DataCoinPredicate, - DataCoinSigned, + input::{ + coin::{ + CoinPredicate, + CoinSigned, + DataCoinPredicate, + DataCoinSigned, + UnverifiedCoin, + UnverifiedDataCoin, + }, + ReadOnly, }, policies::Policies, *, @@ -185,7 +190,10 @@ impl TestSetupBuilder { utxo_id, tx_pointer, .. - }) = input + }) + | Input::ReadOnly(ReadOnly::Coin(UnverifiedCoin { utxo_id, owner, amount, asset_id, tx_pointer })) + | Input::ReadOnly(ReadOnly::CoinPredicate(CoinPredicate { utxo_id, owner, amount, asset_id, tx_pointer, .. })) + = input { Some( ConfigCoin { @@ -216,7 +224,10 @@ impl TestSetupBuilder { tx_pointer, data, .. - }) = input + }) + | Input::ReadOnly(ReadOnly::DataCoin(UnverifiedDataCoin { utxo_id, owner, amount, asset_id, tx_pointer, data })) + | Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { utxo_id, owner, amount, asset_id, tx_pointer, data, .. })) + = input { tracing::debug!("Adding data coin input: tx_id: {:?}, output_index: {}, owner: {:?}, amount: {}, asset_id: {:?}, data: {:?}", utxo_id.tx_id(), utxo_id.output_index(), owner, amount, asset_id, data); diff --git a/tests/tests/tx/predicates.rs b/tests/tests/tx/predicates.rs index e61cd357f96..66cb9eaa02c 100644 --- a/tests/tests/tx/predicates.rs +++ b/tests/tests/tx/predicates.rs @@ -1,4 +1,5 @@ // Tests related to the predicate execution feature +#![allow(unused_imports)] use crate::helpers::TestSetupBuilder; use fuel_core_storage::tables::Coins; @@ -6,6 +7,7 @@ use fuel_core_types::{ fuel_asm::*, fuel_tx::{ field::{ + ChargeableBody, Inputs, Outputs, }, @@ -401,20 +403,126 @@ async fn submit__tx_with_predicate_can_check_input_and_output_data_coins() { .finalize() .await; - use fuel_core_storage::StorageAsRef; + predicate_tx + .estimate_predicates( + &CheckPredicateParams::from( + &context + .srv + .shared + .config + .snapshot_reader + .chain_config() + .consensus_parameters, + ), + MemoryInstance::new(), + &EmptyStorage, + ) + .expect("Predicate check failed"); - let coin = context - .srv - .shared - .database - .on_chain() - .storage::() - .get(&predicate_tx.inputs()[0].utxo_id().unwrap()) - .expect("Failed to get coin from db"); + assert_ne!(predicate_tx.inputs()[0].predicate_gas_used().unwrap(), 0); - tracing::debug!("zzzzzz {:?}", coin); + // when + let predicate_tx = predicate_tx.into(); + context + .client + .submit_and_await_commit(&predicate_tx) + .await + .unwrap(); - assert_eq!(predicate_tx.inputs()[0].predicate_gas_used().unwrap(), 0); + // then + let transaction: Transaction = context + .client + .transaction(&predicate_tx.id(&ChainId::default())) + .await + .unwrap() + .unwrap() + .transaction + .try_into() + .unwrap(); + + if let Output::DataCoin { amount, .. } = transaction.as_script().unwrap().outputs()[0] + { + assert!( + amount == output_amount, + "Expected output amount to be {}, but got {}", + amount, + output_amount + ); + } else { + panic!("Expected output data coin"); + } + + if let Output::Change { amount, .. } = transaction.as_script().unwrap().outputs()[1] { + assert!( + amount == change_amount, + "Expected change amount to be {}, but got {}", + change_amount, + amount + ); + } else { + panic!("Expected change output"); + } +} + +#[tokio::test] +async fn submit__tx_with_predicate_can_check_read_only_input_predicate_data_coin_and_output_data_coins( +) { + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .try_init(); + + let mut rng = StdRng::seed_from_u64(2322); + + // given + let input_amount = 500; + let output_amount = 200; + let change_amount = input_amount - output_amount; + let limit = 1000; + let asset_id = rng.gen(); + let predicate = predicate_checking_output_data_matches_input_data_coin(); + let coin_data = vec![123; 100]; + let predicate_data = vec![1, 2, 3, 4, 5]; + let true_predicate = op::ret(RegId::ONE).to_bytes().to_vec(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + let owner = Input::predicate_owner(&predicate); + let mut predicate_tx = + TransactionBuilder::script(Default::default(), Default::default()) + .add_input(Input::read_only_data_coin_predicate( + rng.gen(), + true_predicate_owner, + 123, + asset_id, + Default::default(), + Default::default(), + true_predicate, + vec![], + coin_data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + input_amount, + asset_id, + Default::default(), + Default::default(), + predicate, + predicate_data, + )) + .add_output(Output::data_coin( + rng.gen(), + output_amount, + asset_id, + coin_data, + )) + .add_output(Output::change(rng.gen(), 0, asset_id)) + .script_gas_limit(limit) + .finalize(); + + // create test context with predicates disabled + let context = TestSetupBuilder::default() + .config_coin_inputs_from_transactions(&[&predicate_tx]) + .finalize() + .await; predicate_tx .estimate_predicates( @@ -434,18 +542,40 @@ async fn submit__tx_with_predicate_can_check_input_and_output_data_coins() { assert_ne!(predicate_tx.inputs()[0].predicate_gas_used().unwrap(), 0); + let chain_id = context.srv.shared.config.chain_id(); + let original_id = predicate_tx.id(&chain_id); + tracing::debug!("original tx id: {:?}", original_id.to_bytes()); + // when let predicate_tx = predicate_tx.into(); - context + let _status = context .client .submit_and_await_commit(&predicate_tx) .await .unwrap(); + // list all txs in the context db + let txs = context + .srv + .shared + .database + .on_chain() + .all_transactions(None, None) + .collect::>(); + + use fuel_core_types::fuel_types::canonical::Serialize; + for tx in txs.iter() { + let id = tx.as_ref().unwrap().id(&chain_id).to_bytes(); + tracing::debug!("db tx:{:?}", id); + } + // then + let id = predicate_tx.id(&chain_id); + tracing::debug!("tx id: {:?}", id.to_bytes()); + let transaction: Transaction = context .client - .transaction(&predicate_tx.id(&ChainId::default())) + .transaction(&id) .await .unwrap() .unwrap() @@ -476,3 +606,113 @@ async fn submit__tx_with_predicate_can_check_input_and_output_data_coins() { panic!("Expected change output"); } } + +#[allow(unused_variables)] +#[tokio::test] +async fn encoding_decoding_tx_id_roundtrip() { + use fuel_core_types::fuel_types::canonical::Serialize; + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .try_init(); + + let mut rng = StdRng::seed_from_u64(2322); + + // given + let input_amount = 500; + let output_amount = 200; + let limit = 1000; + let asset_id = rng.gen(); + let predicate = predicate_checking_output_data_matches_input_data_coin(); + let coin_data = vec![123; 100]; + let predicate_data = vec![1, 2, 3, 4, 5]; + let true_predicate = op::ret(RegId::ONE).to_bytes().to_vec(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + let owner = Input::predicate_owner(&predicate); + let mut predicate_tx = + TransactionBuilder::script(Default::default(), Default::default()) + .add_input(Input::read_only_data_coin_predicate( + rng.gen(), + true_predicate_owner, + 123, + asset_id, + Default::default(), + Default::default(), + true_predicate, + vec![], + coin_data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + input_amount, + asset_id, + Default::default(), + Default::default(), + predicate, + predicate_data, + )) + .add_output(Output::data_coin( + rng.gen(), + output_amount, + asset_id, + coin_data, + )) + .add_output(Output::change(rng.gen(), 0, asset_id)) + .script_gas_limit(limit) + .finalize(); + + // create test context with predicates disabled + let context = TestSetupBuilder::default() + .config_coin_inputs_from_transactions(&[&predicate_tx]) + .finalize() + .await; + + predicate_tx + .estimate_predicates( + &CheckPredicateParams::from( + &context + .srv + .shared + .config + .snapshot_reader + .chain_config() + .consensus_parameters, + ), + MemoryInstance::new(), + &EmptyStorage, + ) + .expect("Predicate check failed"); + + assert_ne!(predicate_tx.inputs()[0].predicate_gas_used().unwrap(), 0); + use fuel_core_types::fuel_types::canonical::Deserialize; + + let chain_id = context.srv.shared.config.chain_id(); + + // roundtrip tx_id: + // let encoded_tx = predicate_tx.clone().to_bytes(); + // let mut decoded_tx = Transaction::from_bytes(&encoded_tx).unwrap(); + // the fields of the tx match + // if let (original_script, Transaction::Script(ref mut roundtrip_script)) = + // (&predicate_tx, &mut decoded_tx) + // { + // use fuel_core_types::fuel_tx::field::{ + // Policies, + // Witnesses, + // }; + // assert_eq!(original_script.body(), roundtrip_script.body()); + // assert_eq!(original_script.policies(), roundtrip_script.policies()); + // assert_eq!(original_script.inputs(), roundtrip_script.inputs()); + // assert_eq!(original_script.outputs(), roundtrip_script.outputs()); + // assert_eq!(original_script.witnesses(), roundtrip_script.witnesses()); + // // make sure metadata matches + // // roundtrip_script.metadata = original_script.metadata().clone(); + // // assert_eq!(original_script.metadata(), roundtrip_script.metadata()); + // } else { + // panic!("Expected both transactions to be scripts"); + // } + let original_id = predicate_tx.id(&chain_id); + predicate_tx.metadata = None; + let mutated_id = predicate_tx.id(&chain_id); + + assert_eq!(original_id, mutated_id); +} From 5f6246f848cd2331667a89abdc363a75ffa47bdc Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Fri, 11 Apr 2025 21:50:56 -0600 Subject: [PATCH 07/13] Fix test --- tests/tests/tx/predicates.rs | 133 ----------------------------------- 1 file changed, 133 deletions(-) diff --git a/tests/tests/tx/predicates.rs b/tests/tests/tx/predicates.rs index 66cb9eaa02c..db5a93092be 100644 --- a/tests/tests/tx/predicates.rs +++ b/tests/tests/tx/predicates.rs @@ -467,10 +467,6 @@ async fn submit__tx_with_predicate_can_check_input_and_output_data_coins() { #[tokio::test] async fn submit__tx_with_predicate_can_check_read_only_input_predicate_data_coin_and_output_data_coins( ) { - let _ = tracing_subscriber::fmt() - .with_max_level(tracing::Level::DEBUG) - .try_init(); - let mut rng = StdRng::seed_from_u64(2322); // given @@ -543,8 +539,6 @@ async fn submit__tx_with_predicate_can_check_read_only_input_predicate_data_coin assert_ne!(predicate_tx.inputs()[0].predicate_gas_used().unwrap(), 0); let chain_id = context.srv.shared.config.chain_id(); - let original_id = predicate_tx.id(&chain_id); - tracing::debug!("original tx id: {:?}", original_id.to_bytes()); // when let predicate_tx = predicate_tx.into(); @@ -554,25 +548,8 @@ async fn submit__tx_with_predicate_can_check_read_only_input_predicate_data_coin .await .unwrap(); - // list all txs in the context db - let txs = context - .srv - .shared - .database - .on_chain() - .all_transactions(None, None) - .collect::>(); - - use fuel_core_types::fuel_types::canonical::Serialize; - for tx in txs.iter() { - let id = tx.as_ref().unwrap().id(&chain_id).to_bytes(); - tracing::debug!("db tx:{:?}", id); - } - // then let id = predicate_tx.id(&chain_id); - tracing::debug!("tx id: {:?}", id.to_bytes()); - let transaction: Transaction = context .client .transaction(&id) @@ -606,113 +583,3 @@ async fn submit__tx_with_predicate_can_check_read_only_input_predicate_data_coin panic!("Expected change output"); } } - -#[allow(unused_variables)] -#[tokio::test] -async fn encoding_decoding_tx_id_roundtrip() { - use fuel_core_types::fuel_types::canonical::Serialize; - let _ = tracing_subscriber::fmt() - .with_max_level(tracing::Level::DEBUG) - .try_init(); - - let mut rng = StdRng::seed_from_u64(2322); - - // given - let input_amount = 500; - let output_amount = 200; - let limit = 1000; - let asset_id = rng.gen(); - let predicate = predicate_checking_output_data_matches_input_data_coin(); - let coin_data = vec![123; 100]; - let predicate_data = vec![1, 2, 3, 4, 5]; - let true_predicate = op::ret(RegId::ONE).to_bytes().to_vec(); - let true_predicate_owner = Input::predicate_owner(&true_predicate); - let owner = Input::predicate_owner(&predicate); - let mut predicate_tx = - TransactionBuilder::script(Default::default(), Default::default()) - .add_input(Input::read_only_data_coin_predicate( - rng.gen(), - true_predicate_owner, - 123, - asset_id, - Default::default(), - Default::default(), - true_predicate, - vec![], - coin_data.clone(), - )) - .add_input(Input::coin_predicate( - rng.gen(), - owner, - input_amount, - asset_id, - Default::default(), - Default::default(), - predicate, - predicate_data, - )) - .add_output(Output::data_coin( - rng.gen(), - output_amount, - asset_id, - coin_data, - )) - .add_output(Output::change(rng.gen(), 0, asset_id)) - .script_gas_limit(limit) - .finalize(); - - // create test context with predicates disabled - let context = TestSetupBuilder::default() - .config_coin_inputs_from_transactions(&[&predicate_tx]) - .finalize() - .await; - - predicate_tx - .estimate_predicates( - &CheckPredicateParams::from( - &context - .srv - .shared - .config - .snapshot_reader - .chain_config() - .consensus_parameters, - ), - MemoryInstance::new(), - &EmptyStorage, - ) - .expect("Predicate check failed"); - - assert_ne!(predicate_tx.inputs()[0].predicate_gas_used().unwrap(), 0); - use fuel_core_types::fuel_types::canonical::Deserialize; - - let chain_id = context.srv.shared.config.chain_id(); - - // roundtrip tx_id: - // let encoded_tx = predicate_tx.clone().to_bytes(); - // let mut decoded_tx = Transaction::from_bytes(&encoded_tx).unwrap(); - // the fields of the tx match - // if let (original_script, Transaction::Script(ref mut roundtrip_script)) = - // (&predicate_tx, &mut decoded_tx) - // { - // use fuel_core_types::fuel_tx::field::{ - // Policies, - // Witnesses, - // }; - // assert_eq!(original_script.body(), roundtrip_script.body()); - // assert_eq!(original_script.policies(), roundtrip_script.policies()); - // assert_eq!(original_script.inputs(), roundtrip_script.inputs()); - // assert_eq!(original_script.outputs(), roundtrip_script.outputs()); - // assert_eq!(original_script.witnesses(), roundtrip_script.witnesses()); - // // make sure metadata matches - // // roundtrip_script.metadata = original_script.metadata().clone(); - // // assert_eq!(original_script.metadata(), roundtrip_script.metadata()); - // } else { - // panic!("Expected both transactions to be scripts"); - // } - let original_id = predicate_tx.id(&chain_id); - predicate_tx.metadata = None; - let mutated_id = predicate_tx.id(&chain_id); - - assert_eq!(original_id, mutated_id); -} From 59bc9aa953e43b7bfd9853e7e82af700f2f0a7ce Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Fri, 11 Apr 2025 23:50:19 -0600 Subject: [PATCH 08/13] Add other data read only test --- tests/tests/tx/predicates.rs | 113 ++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/tests/tx/predicates.rs b/tests/tests/tx/predicates.rs index db5a93092be..78308eb1a8a 100644 --- a/tests/tests/tx/predicates.rs +++ b/tests/tests/tx/predicates.rs @@ -429,7 +429,7 @@ async fn submit__tx_with_predicate_can_check_input_and_output_data_coins() { .await .unwrap(); - // then + let transaction: Transaction = context .client .transaction(&predicate_tx.id(&ChainId::default())) @@ -583,3 +583,114 @@ async fn submit__tx_with_predicate_can_check_read_only_input_predicate_data_coin panic!("Expected change output"); } } + + +#[tokio::test] +async fn submit__tx_with_predicate_can_check_read_only_input_data_coin_and_output_data_coins() { + let mut rng = StdRng::seed_from_u64(2322); + + // given + let input_amount = 500; + let output_amount = 200; + let change_amount = input_amount - output_amount; + let limit = 1000; + let asset_id = rng.gen(); + let predicate = predicate_checking_output_data_matches_input_data_coin(); + let coin_data = vec![123; 100]; + let owner = Input::predicate_owner(&predicate); + let mut predicate_tx = + TransactionBuilder::script(Default::default(), Default::default()) + .add_input(Input::read_only_data_coin( + rng.gen(), + owner, + input_amount, + asset_id, + Default::default(), + coin_data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + input_amount, + asset_id, + Default::default(), + Default::default(), + predicate, + vec![1,2,3], + )) + .add_output(Output::data_coin( + rng.gen(), + output_amount, + asset_id, + coin_data, + )) + .add_output(Output::change(rng.gen(), 0, asset_id)) + .script_gas_limit(limit) + .finalize(); + + // create test context with predicates disabled + let context = TestSetupBuilder::default() + .config_coin_inputs_from_transactions(&[&predicate_tx]) + .finalize() + .await; + + predicate_tx + .estimate_predicates( + &CheckPredicateParams::from( + &context + .srv + .shared + .config + .snapshot_reader + .chain_config() + .consensus_parameters, + ), + MemoryInstance::new(), + &EmptyStorage, + ) + .expect("Predicate check failed"); + + + // when + let predicate_tx = predicate_tx.into(); + context + .client + .submit_and_await_commit(&predicate_tx) + .await + .unwrap(); + + + let transaction: Transaction = context + .client + .transaction(&predicate_tx.id(&ChainId::default())) + .await + .unwrap() + .unwrap() + .transaction + .try_into() + .unwrap(); + + if let Output::DataCoin { amount, .. } = transaction.as_script().unwrap().outputs()[0] + { + assert!( + amount == output_amount, + "Expected output amount to be {}, but got {}", + amount, + output_amount + ); + } else { + panic!("Expected output data coin"); + } + + if let Output::Change { amount, .. } = transaction.as_script().unwrap().outputs()[1] { + assert!( + amount == change_amount, + "Expected change amount to be {}, but got {}", + change_amount, + amount + ); + } else { + panic!("Expected change output"); + } +} + From 34bb67b7fb932e1dd330ce21fc44231114f7b618 Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Thu, 17 Apr 2025 16:00:40 -0600 Subject: [PATCH 09/13] Add test for re-use of read-only --- crates/fuel-core/src/executor.rs | 208 +++++++++++++++++++++++++ crates/storage/src/blueprint/sparse.rs | 4 +- 2 files changed, 210 insertions(+), 2 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 7c9157ead10..6cb215aba49 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -15,6 +15,10 @@ mod tests { PreconfirmationSenderPort, }, }; + use fuel_core_storage::transactional::{ + Changes, + Modifiable, + }; #[cfg(not(feature = "wasm-executor"))] use fuel_core_types::services::preconfirmation::{ @@ -3887,6 +3891,210 @@ mod tests { assert!(result.is_ok(), "{result:?}") } + #[allow(unused_variables)] + #[test] + fn validate__can_include_read_only_input_in_multiple_txs() { + let mut rng = StdRng::seed_from_u64(2322u64); + + let true_predicate: Vec<_> = vec![op::ret(0x01)].into_iter().collect(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + let shared_read_only_input = Input::read_only_data_coin_predicate( + rng.gen(), + true_predicate_owner, + 1000, + AssetId::BASE, + rng.gen(), + 0, + true_predicate.clone(), + vec![], + vec![99u8; 100], + ); + + let input_one = Input::coin_predicate( + rng.gen(), + true_predicate_owner, + 10000, + AssetId::BASE, + rng.gen(), + 0, + true_predicate.clone(), + vec![1, 2, 3], + ); + + let input_two = Input::coin_predicate( + rng.gen(), + true_predicate_owner, + 10000, + AssetId::BASE, + rng.gen(), + 0, + true_predicate.clone(), + vec![4, 5, 6], + ); + + // include read only input and another input to cover costs + let mut tx_1 = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(100) + .add_input(shared_read_only_input.clone()) + .add_input(input_one.clone()) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + + // include read only input and another input to cover costs + let mut tx_2 = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(100) + .add_input(shared_read_only_input.clone()) + .add_input(input_two.clone()) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + + let consensus_parameters = ConsensusParameters::default(); + let config = Config { + forbid_fake_coins_default: true, + consensus_parameters: consensus_parameters.clone(), + }; + let db = &mut Database::default(); + // insert coins into state + if let Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + data, + .. + })) = shared_read_only_input + { + let coin = CompressedCoin::V2(CompressedCoinV2 { + owner, + amount, + asset_id, + tx_pointer, + data, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = input_one + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = input_two + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + assert_ne!( + tx_1.id(&consensus_parameters.chain_id()), + tx_2.id(&consensus_parameters.chain_id()) + ); + let producer = create_executor(db.clone(), config.clone()); + + // submit first tx + tx_1.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); + let ( + ExecutionResult { + block, + skipped_transactions, + .. + }, + changes, + ): (ExecutionResult, Changes) = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: PartialBlockHeader::default(), + transactions_source: OnceTransactionsSource::new(vec![tx_1.into()]), + coinbase_recipient: Default::default(), + gas_price: 1, + }) + .unwrap() + .into(); + tracing::debug!("skipped transactions: {:?}", skipped_transactions); + assert!(skipped_transactions.is_empty()); + // We need to commit the changes from the first tx to proceed + db.commit_changes(changes).unwrap(); + + // when + // submit second tx + tx_2.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); + let mut block_2_header = PartialBlockHeader::default(); + block_2_header.consensus.height = 1u32.into(); + + let ( + ExecutionResult { + block, + skipped_transactions, + .. + }, + changes, + ): (ExecutionResult, Changes) = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: block_2_header, + transactions_source: OnceTransactionsSource::new(vec![tx_2.into()]), + coinbase_recipient: Default::default(), + gas_price: 1, + }) + .unwrap() + .into(); + + // then + assert!(skipped_transactions.is_empty()); + } + #[test] fn verifying_during_production_consensus_parameters_version_works() { let mut rng = StdRng::seed_from_u64(2322u64); diff --git a/crates/storage/src/blueprint/sparse.rs b/crates/storage/src/blueprint/sparse.rs index e5f9de1fceb..e04c3c4064e 100644 --- a/crates/storage/src/blueprint/sparse.rs +++ b/crates/storage/src/blueprint/sparse.rs @@ -110,7 +110,7 @@ where let mut tree: MerkleTree = MerkleTree::load(storage, &root) .map_err(|err| StorageError::Other(anyhow::anyhow!("{err:?}")))?; - tree.update(MerkleTreeKey::new(key_bytes), value_bytes) + tree.insert(MerkleTreeKey::new(key_bytes), value_bytes) .map_err(|err| StorageError::Other(anyhow::anyhow!("{err:?}")))?; // Generate new metadata for the updated tree @@ -402,7 +402,7 @@ where .collect_vec(); for (key_bytes, value_bytes) in encoded_set.iter() { - tree.update(MerkleTreeKey::new(key_bytes), value_bytes) + tree.insert(MerkleTreeKey::new(key_bytes), value_bytes) .map_err(|err| StorageError::Other(anyhow::anyhow!("{err:?}")))?; } let root = tree.root(); From 103c019fa1ad41c8bc692b21f5c4066276c29f40 Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Thu, 17 Apr 2025 16:20:02 -0600 Subject: [PATCH 10/13] Add test that shows data coin being spent, fix missed case --- crates/fuel-core/src/executor.rs | 222 ++++++++++++++++++++++- crates/services/executor/src/executor.rs | 14 ++ 2 files changed, 231 insertions(+), 5 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 6cb215aba49..7f1d7af2d3e 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -3891,7 +3891,6 @@ mod tests { assert!(result.is_ok(), "{result:?}") } - #[allow(unused_variables)] #[test] fn validate__can_include_read_only_input_in_multiple_txs() { let mut rng = StdRng::seed_from_u64(2322u64); @@ -4044,7 +4043,6 @@ mod tests { .unwrap(); let ( ExecutionResult { - block, skipped_transactions, .. }, @@ -4074,14 +4072,228 @@ mod tests { let mut block_2_header = PartialBlockHeader::default(); block_2_header.consensus.height = 1u32.into(); + let ExecutionResult { + skipped_transactions, + .. + } = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: block_2_header, + transactions_source: OnceTransactionsSource::new(vec![tx_2.into()]), + coinbase_recipient: Default::default(), + gas_price: 1, + }) + .unwrap() + .into_result(); + + // then + assert!(skipped_transactions.is_empty()); + } + + #[test] + fn validate__can_not_include_read_only_input_if_spent() { + let mut rng = StdRng::seed_from_u64(2322u64); + + let true_predicate: Vec<_> = vec![op::ret(0x01)].into_iter().collect(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + let utxo_id = rng.gen(); + let owner = true_predicate_owner; + let amount = 1000; + let asset_id = AssetId::BASE; + let tx_pointer = rng.gen(); + let predicate_gas_used = 0; + let predicate = true_predicate.clone(); + let predicate_data = vec![]; + let data = vec![99u8; 100]; + let predicate_input = Input::data_coin_predicate( + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + predicate_gas_used, + predicate.clone(), + predicate_data.clone(), + data.clone(), + ); + let read_only_input = Input::read_only_data_coin_predicate( + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + predicate_gas_used, + predicate.clone(), + predicate_data, + data.clone(), + ); + + let input_one = Input::coin_predicate( + rng.gen(), + true_predicate_owner, + 10000, + AssetId::BASE, + rng.gen(), + 0, + true_predicate.clone(), + vec![1, 2, 3], + ); + + let input_two = Input::coin_predicate( + rng.gen(), + true_predicate_owner, + 10000, + AssetId::BASE, + rng.gen(), + 0, + true_predicate.clone(), + vec![4, 5, 6], + ); + + // include read only input and another input to cover costs + let mut tx_1 = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(100) + .add_input(predicate_input) + .add_input(input_one.clone()) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + + // include read only input and another input to cover costs + let mut tx_2 = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(100) + .add_input(read_only_input.clone()) + .add_input(input_two.clone()) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + + let consensus_parameters = ConsensusParameters::default(); + let config = Config { + forbid_fake_coins_default: true, + consensus_parameters: consensus_parameters.clone(), + }; + let db = &mut Database::default(); + // insert coins into state + if let Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + data, + .. + })) = read_only_input + { + let coin = CompressedCoin::V2(CompressedCoinV2 { + owner, + amount, + asset_id, + tx_pointer, + data, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = input_one + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = input_two + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + assert_ne!( + tx_1.id(&consensus_parameters.chain_id()), + tx_2.id(&consensus_parameters.chain_id()) + ); + let producer = create_executor(db.clone(), config.clone()); + + // submit first tx + tx_1.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); let ( ExecutionResult { - block, skipped_transactions, .. }, changes, ): (ExecutionResult, Changes) = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: PartialBlockHeader::default(), + transactions_source: OnceTransactionsSource::new(vec![tx_1.into()]), + coinbase_recipient: Default::default(), + gas_price: 1, + }) + .unwrap() + .into(); + tracing::debug!("skipped transactions: {:?}", skipped_transactions); + assert!(skipped_transactions.is_empty()); + // We need to commit the changes from the first tx to proceed + db.commit_changes(changes).unwrap(); + + // when + // submit second tx + tx_2.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); + let mut block_2_header = PartialBlockHeader::default(); + block_2_header.consensus.height = 1u32.into(); + + let ExecutionResult { + skipped_transactions, + .. + }: ExecutionResult = producer .produce_without_commit_with_source_direct_resolve(Components { header_to_produce: block_2_header, transactions_source: OnceTransactionsSource::new(vec![tx_2.into()]), @@ -4089,10 +4301,10 @@ mod tests { gas_price: 1, }) .unwrap() - .into(); + .into_result(); // then - assert!(skipped_transactions.is_empty()); + assert_eq!(skipped_transactions.len(), 1); } #[test] diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index a0a8efca00d..12e5a16520f 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -2070,6 +2070,20 @@ where amount, asset_id, .. + }) + | Input::DataCoinSigned(DataCoinSigned { + utxo_id, + owner, + amount, + asset_id, + .. + }) + | Input::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + .. }) => { // prune utxo from db let coin = db From b9c11240811fb239b0bbf826805202cba7de6f23 Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Tue, 22 Apr 2025 10:13:25 -0600 Subject: [PATCH 11/13] Fix tracing error --- crates/chain-config/src/config/coin.rs | 10 +++++----- tests/test-helpers/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/chain-config/src/config/coin.rs b/crates/chain-config/src/config/coin.rs index da227b7b747..0c3bf1bb805 100644 --- a/crates/chain-config/src/config/coin.rs +++ b/crates/chain-config/src/config/coin.rs @@ -228,11 +228,11 @@ impl From for TableEntry { } } }; - tracing::debug!( - "Created TableEntry: key={:?}, value={:?}", - &entry.key, - &entry.value, - ); + // tracing::debug!( + // "Created TableEntry: key={:?}, value={:?}", + // &entry.key, + // &entry.value, + // ); entry } } diff --git a/tests/test-helpers/Cargo.toml b/tests/test-helpers/Cargo.toml index a6b204e6bb3..61de6682e8c 100644 --- a/tests/test-helpers/Cargo.toml +++ b/tests/test-helpers/Cargo.toml @@ -38,4 +38,4 @@ reqwest = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tempfile = { workspace = true } -tracing = "0.1.41" +tracing = "0.1" From 292933364498d1b7e6807b73dea76b09a0e06e1f Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Wed, 30 Apr 2025 15:26:38 -0600 Subject: [PATCH 12/13] add test for fee checks on read-only inputs --- Cargo.toml | 2 + crates/fuel-core/src/executor.rs | 155 +++++++++++++++++++++- crates/fuel-core/src/schema/tx/receipt.rs | 3 +- 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3f10c14dbe4..6f95e79fc2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,3 +176,5 @@ url = "2.2" # add patch for fuel-vm [patch.crates-io] fuel-vm-private = { path = "../fuel-vm/fuel-vm", version = "0.60.0", package = "fuel-vm" } +fuel-tx = { path = "../fuel-vm/fuel-tx", version = "0.60.0" } +fuel-types = { path = "../fuel-vm/fuel-types", version = "0.60.0" } diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 7f1d7af2d3e..ed22445f282 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -3641,8 +3641,6 @@ mod tests { consensus_parameters: consensus_parameters.clone(), }; let data = vec![99u8; 100]; - // let true_predicate = vec![op::ret(0x01)].into_iter().collect(); - // let true_predicate_owner = Input::predicate_owner(&true_predicate); let mut tx = TransactionBuilder::script( vec![op::ret(RegId::ONE)].into_iter().collect(), @@ -3891,6 +3889,159 @@ mod tests { assert!(result.is_ok(), "{result:?}") } + #[test] + fn validate__predicate_fails_if_not_enough_to_cover_fee_but_read_only_has_enough() { + let mut rng = StdRng::seed_from_u64(2322u64); + + // given + let read_only_amount = 123; + let predicate: Vec = + predicates_checking_input_data_coin_data_matches_output_data_coin_data(); + let owner = Input::predicate_owner(&predicate); + let amount = 1; + + let consensus_parameters = ConsensusParameters::default(); + let config = Config { + forbid_fake_coins_default: true, + consensus_parameters: consensus_parameters.clone(), + }; + let data = vec![99u8; 100]; + let true_predicate = vec![op::ret(0x01)].into_iter().collect(); + let true_predicate_owner = Input::predicate_owner(&true_predicate); + + let mut tx = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .max_fee_limit(100) + .add_input(Input::read_only_data_coin_predicate( + rng.gen(), + true_predicate_owner, + read_only_amount, + AssetId::BASE, + rng.gen(), + 0, + true_predicate, + vec![], + data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + amount, + AssetId::BASE, + rng.gen(), + 0, + predicate, + vec![], + )) + .add_output(Output::DataCoin { + to: Default::default(), + amount: 100, + asset_id: AssetId::BASE, + data: data.clone(), + }) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: AssetId::BASE, + }) + .finalize(); + tx.estimate_predicates( + &consensus_parameters.clone().into(), + MemoryInstance::new(), + &EmptyStorage, + ) + .unwrap(); + let db = &mut Database::default(); + + // insert data coin into state + if let Input::ReadOnly(ReadOnly::DataCoinPredicate(DataCoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + data, + .. + })) = &tx.inputs()[0] + { + let coin = CompressedCoin::V2(CompressedCoinV2 { + owner: *owner, + amount: *amount, + asset_id: *asset_id, + tx_pointer: *tx_pointer, + data: data.clone(), + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + // insert coin to cover output value + if let Input::CoinPredicate(CoinPredicate { + utxo_id, + owner, + amount, + asset_id, + tx_pointer, + .. + }) = tx.inputs()[1] + { + let coin = CompressedCoin::V1(CompressedCoinV1 { + owner, + amount, + asset_id, + tx_pointer, + }); + db.storage::().insert(&utxo_id, &coin).unwrap(); + } else { + panic!("Expected a DataCoinPredicate"); + } + + let producer = create_executor(db.clone(), config.clone()); + + // when + let ExecutionResult { + // block, + skipped_transactions, + .. + } = producer + .produce_without_commit_with_source_direct_resolve(Components { + header_to_produce: PartialBlockHeader::default(), + transactions_source: OnceTransactionsSource::new(vec![tx.into()]), + coinbase_recipient: Default::default(), + gas_price: 1000000, + }) + .unwrap() + .into_result(); + + // then + let skipped_reason = skipped_transactions + .first() + .expect("Expected one skipped transaction") + .1 + .clone(); + assert_eq!( + skipped_transactions.len(), + 1, + "expected to have one skipped tx, found: {skipped_transactions:?}" + ); + assert!( + matches!( + skipped_reason, + fuel_core_types::services::executor::Error::InvalidTransaction( + CheckError::Validity(ValidityError::InsufficientFeeAmount { + provided, + .. + }) + ) + if provided == amount, + ), + "Expected InsufficientFeeAmount, found: {skipped_reason:?}" + ); + } + #[test] fn validate__can_include_read_only_input_in_multiple_txs() { let mut rng = StdRng::seed_from_u64(2322u64); diff --git a/crates/fuel-core/src/schema/tx/receipt.rs b/crates/fuel-core/src/schema/tx/receipt.rs index aabef434af4..9b0cdf27c4e 100644 --- a/crates/fuel-core/src/schema/tx/receipt.rs +++ b/crates/fuel-core/src/schema/tx/receipt.rs @@ -11,11 +11,12 @@ use async_graphql::{ Enum, Object, }; +#[cfg(feature = "test-helpers")] +use fuel_core_types::fuel_types::SubAssetId; use fuel_core_types::{ fuel_asm::Word, fuel_tx, fuel_types, - fuel_types::SubAssetId, }; #[derive( From f411f1f6de141fb2692a44f4fc35c785e46587fb Mon Sep 17 00:00:00 2001 From: Mitch Turner Date: Wed, 30 Apr 2025 15:28:17 -0600 Subject: [PATCH 13/13] Simplify test --- crates/fuel-core/src/executor.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index ed22445f282..b2d0b17c29f 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -4022,11 +4022,6 @@ mod tests { .expect("Expected one skipped transaction") .1 .clone(); - assert_eq!( - skipped_transactions.len(), - 1, - "expected to have one skipped tx, found: {skipped_transactions:?}" - ); assert!( matches!( skipped_reason,