From 6f5b3aa0d699f48539adab21536208ef45831478 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 15 Sep 2025 13:25:45 +0300 Subject: [PATCH 01/22] Use hash-based balance_root and hash_root fields --- Cargo.lock | 2 + crates/fuel-core/Cargo.toml | 1 + crates/fuel-core/src/executor.rs | 239 +++++------------- crates/services/executor/Cargo.toml | 1 + .../services/executor/src/contract_state.rs | 121 +++++++++ crates/services/executor/src/executor.rs | 99 ++++---- crates/services/executor/src/lib.rs | 3 + .../executor/src/storage_access_recorder.rs | 52 ++++ 8 files changed, 294 insertions(+), 224 deletions(-) create mode 100644 crates/services/executor/src/contract_state.rs create mode 100644 crates/services/executor/src/storage_access_recorder.rs diff --git a/Cargo.lock b/Cargo.lock index 61643c04677..fb87e38c063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3511,6 +3511,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha2 0.10.9", "strum 0.25.0", "strum_macros 0.25.3", "tempfile", @@ -3777,6 +3778,7 @@ dependencies = [ "fuel-core-types 0.46.0", "parking_lot", "serde", + "sha2 0.10.9", "tracing", ] diff --git a/crates/fuel-core/Cargo.toml b/crates/fuel-core/Cargo.toml index cc5377b900f..7b3e8405118 100644 --- a/crates/fuel-core/Cargo.toml +++ b/crates/fuel-core/Cargo.toml @@ -132,6 +132,7 @@ fuel-core-upgradable-executor = { workspace = true, features = [ "test-helpers", ] } proptest = { workspace = true } +sha2 = "0.10" test-case = { workspace = true } test-strategy = { workspace = true } tokio-test = "0.4.4" diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index bc0255fb62c..abd8b49e751 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -79,7 +79,6 @@ mod tests { }, fuel_tx::{ Bytes32, - Cacheable, ConsensusParameters, Create, DependentCost, @@ -108,7 +107,6 @@ mod tests { OutputContract, Outputs, Policies, - Script as ScriptField, TxPointer as TxPointerTraitTrait, }, input::{ @@ -168,6 +166,11 @@ mod tests { SeedableRng, prelude::StdRng, }; + use sha2::{ + Digest, + Sha256, + }; + #[derive(Clone, Debug, Default)] struct Config { @@ -493,15 +496,16 @@ mod tests { mint.tx_pointer(), &TxPointer::new(*block.header().height(), 1) ); + let empty_sha: [u8; 32] = Sha256::digest(&[]).into(); assert_eq!(mint.mint_asset_id(), &AssetId::BASE); assert_eq!(mint.mint_amount(), &expected_fee_amount_1); assert_eq!(mint.input_contract().contract_id, recipient); - assert_eq!(mint.input_contract().balance_root, Bytes32::zeroed()); - assert_eq!(mint.input_contract().state_root, Bytes32::zeroed()); + assert_eq!(mint.input_contract().balance_root, Bytes32::new(empty_sha)); + assert_eq!(mint.input_contract().state_root, Bytes32::new(empty_sha)); assert_eq!(mint.input_contract().utxo_id, UtxoId::default()); assert_eq!(mint.input_contract().tx_pointer, TxPointer::default()); - assert_ne!(mint.output_contract().balance_root, Bytes32::zeroed()); - assert_eq!(mint.output_contract().state_root, Bytes32::zeroed()); + assert_ne!(mint.output_contract().balance_root, Bytes32::new(empty_sha)); + assert_eq!(mint.output_contract().state_root, Bytes32::new(empty_sha)); assert_eq!(mint.output_contract().input_index, 0); first_mint = mint.clone(); } else { @@ -571,14 +575,6 @@ mod tests { assert_eq!(second_mint.mint_asset_id(), &AssetId::BASE); assert_eq!(second_mint.mint_amount(), &expected_fee_amount_2); assert_eq!(second_mint.input_contract().contract_id, recipient); - assert_eq!( - second_mint.input_contract().balance_root, - first_mint.output_contract().balance_root - ); - assert_eq!( - second_mint.input_contract().state_root, - first_mint.output_contract().state_root - ); assert_eq!( second_mint.input_contract().utxo_id, UtxoId::new(first_mint.id(&consensus_parameters.chain_id()), 0) @@ -587,14 +583,6 @@ mod tests { second_mint.input_contract().tx_pointer, TxPointer::new(1.into(), 1) ); - assert_ne!( - second_mint.output_contract().balance_root, - first_mint.output_contract().balance_root - ); - assert_eq!( - second_mint.output_contract().state_root, - first_mint.output_contract().state_root - ); assert_eq!(second_mint.output_contract().input_index, 0); } else { panic!("Invalid coinbase transaction"); @@ -1691,16 +1679,16 @@ mod tests { } = executor.produce_and_commit(block).unwrap(); // Assert the balance and state roots should be the same before and after execution. - let empty_state = (*sparse::empty_sum()).into(); let executed_tx = block.transactions()[1].as_script().unwrap(); assert!(matches!( tx_status[2].result, TransactionExecutionResult::Success { .. } )); - assert_eq!(executed_tx.inputs()[0].state_root(), Some(&empty_state)); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&empty_state)); - assert_eq!(executed_tx.outputs()[0].state_root(), Some(&empty_state)); - assert_eq!(executed_tx.outputs()[0].balance_root(), Some(&empty_state)); + let empty: [u8; 32] = Sha256::digest(&[]).into(); + assert_eq!(executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(empty))); + assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(empty))); + assert_eq!(executed_tx.outputs()[0].state_root(), Some(&Bytes32::new(empty))); + assert_eq!(executed_tx.outputs()[0].balance_root(), Some(&Bytes32::new(empty))); } #[test] @@ -1746,7 +1734,6 @@ mod tests { } = executor.produce_and_commit(block).unwrap(); // Assert the balance and state roots should be the same before and after execution. - let empty_state = (*sparse::empty_sum()).into(); let executed_tx = block.transactions()[1].as_script().unwrap(); assert!(matches!( tx_status[1].result, @@ -1760,8 +1747,11 @@ mod tests { executed_tx.inputs()[0].balance_root(), executed_tx.outputs()[0].balance_root() ); - assert_eq!(executed_tx.inputs()[0].state_root(), Some(&empty_state)); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&empty_state)); + + // Both balance and state roots are empty, and should match hash of empty data. + let empty: [u8; 32] = Sha256::digest(&[]).into(); + assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(empty))); + assert_eq!(executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(empty))); } #[test] @@ -1772,9 +1762,9 @@ mod tests { // Create a contract that modifies the state let (create, contract_id) = create_contract( + // Sets value 1 to slot matching the tx id vec![ - // Sets the state STATE[0x1; 32] = value of `RegId::PC`; - op::sww(0x1, 0x29, RegId::PC), + op::sww(RegId::ZERO, 0x29, RegId::ONE), op::ret(1), ] .into_iter() @@ -1846,152 +1836,57 @@ mod tests { let ExecutionResult { block, tx_status, .. } = executor.produce_and_commit(block).unwrap(); - - let empty_state = (*sparse::empty_sum()).into(); - let executed_tx = block.transactions()[1].as_script().unwrap(); assert!(matches!( tx_status[2].result, TransactionExecutionResult::Success { .. } )); - assert_eq!(executed_tx.inputs()[0].state_root(), Some(&empty_state)); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&empty_state)); - // Roots should be different - assert_ne!( - executed_tx.inputs()[0].state_root(), - executed_tx.outputs()[0].state_root() - ); - assert_ne!( - executed_tx.inputs()[0].balance_root(), - executed_tx.outputs()[0].balance_root() - ); - } - - #[test] - fn contracts_balance_and_state_roots_in_inputs_updated() { - // Values in inputs and outputs are random. If the execution of the transaction that - // modifies the state and the balance is successful, it should update roots. - // The first transaction updates the `balance_root` and `state_root`. - // The second transaction is empty. The executor should update inputs of the second - // transaction with the same value from `balance_root` and `state_root`. - let mut rng = StdRng::seed_from_u64(2322u64); - - // Create a contract that modifies the state - let (create, contract_id) = create_contract( - vec![ - // Sets the state STATE[0x1; 32] = value of `RegId::PC`; - op::sww(0x1, 0x29, RegId::PC), - op::ret(1), - ] - .into_iter() - .collect::>() - .as_slice(), - &mut rng, - ); - - let transfer_amount = 100 as Word; - let asset_id = AssetId::from([2; 32]); - let (script, data_offset) = script_with_data_offset!( - data_offset, - vec![ - // Set register `0x10` to `Call` - op::movi(0x10, data_offset + AssetId::LEN as u32), - // Set register `0x11` with offset to data that contains `asset_id` - op::movi(0x11, data_offset), - // Set register `0x12` with `transfer_amount` - op::movi(0x12, transfer_amount as u32), - op::call(0x10, 0x12, 0x11, RegId::CGAS), - op::ret(RegId::ONE), - ], - TxParameters::DEFAULT.tx_offset() - ); - - let script_data: Vec = [ - asset_id.as_ref(), - Call::new(contract_id, transfer_amount, data_offset as Word) - .to_bytes() - .as_ref(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - let modify_balance_and_state_tx = TxBuilder::new(2322) - .script_gas_limit(10000) - .coin_input(AssetId::zeroed(), 10000) - .start_script(script, script_data) - .contract_input(contract_id) - .coin_input(asset_id, transfer_amount) - .fee_input() - .contract_output(&contract_id) - .build() - .transaction() - .clone(); - let db = Database::default(); - - let consensus_parameters = ConsensusParameters::default(); - let mut executor = create_executor( - db.clone(), - Config { - forbid_fake_coins_default: false, - consensus_parameters: consensus_parameters.clone(), - }, - ); - - let block = PartialFuelBlock { - header: PartialBlockHeader { - consensus: ConsensusHeader { - height: 1.into(), - ..Default::default() - }, - ..Default::default() - }, - transactions: vec![create.into(), modify_balance_and_state_tx.into()], - }; - - let ExecutionResult { block, .. } = executor.produce_and_commit(block).unwrap(); let executed_tx = block.transactions()[1].as_script().unwrap(); - let state_root = executed_tx.outputs()[0].state_root(); - let balance_root = executed_tx.outputs()[0].balance_root(); - - let mut new_tx = executed_tx.clone(); - *new_tx.script_mut() = vec![]; - new_tx.precompute(&consensus_parameters.chain_id()).unwrap(); - - let block = PartialFuelBlock { - header: PartialBlockHeader { - consensus: ConsensusHeader { - height: 2.into(), - ..Default::default() - }, - ..Default::default() - }, - transactions: vec![new_tx.into()], - }; - - let ExecutionResult { - block, tx_status, .. - } = executor - .produce_without_commit_with_source_direct_resolve(Components { - header_to_produce: block.header, - transactions_source: OnceTransactionsSource::new(block.transactions), - gas_price: 0, - coinbase_recipient: Default::default(), - }) - .unwrap() - .into_result(); - assert!(matches!( - tx_status[1].result, - TransactionExecutionResult::Success { .. } - )); - let tx = block.transactions()[0].as_script().unwrap(); - assert_eq!(tx.inputs()[0].balance_root(), balance_root); - assert_eq!(tx.inputs()[0].state_root(), state_root); - - let _ = executor - .validate(&block) - .expect("Validation of block should be successful"); + let tx_id = executed_tx.id(&ConsensusParameters::standard().chain_id()); + + // Check resulting roots + + // Input balances: 0 of asset_id [2; 32] + let mut hasher = Sha256::new(); + hasher.update(&contract_id); + hasher.update(&1u64.to_be_bytes()); // number of balances + hasher.update(&asset_id); + hasher.update(&0u64.to_be_bytes()); // balance + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root))); + + // Output balances: 100 of asset_id [2; 32] + let mut hasher = Sha256::new(); + hasher.update(&contract_id); + hasher.update(&1u64.to_be_bytes()); // number of balances + hasher.update(&asset_id); + hasher.update(&0u64.to_be_bytes()); // balance + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root))); + + // Input state: empty slot tx_id + let mut hasher = Sha256::new(); + hasher.update(&contract_id); + hasher.update(&1u64.to_be_bytes()); // number of slots + hasher.update(&tx_id); // the slot key that is modified + hasher.update(&[0u8]); // the slot did not contain any value + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!(executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(expected_state_root))); + + // Output state: slot tx_id with value 1 + let mut hasher = Sha256::new(); + hasher.update(&contract_id); + hasher.update(&1u64.to_be_bytes()); // number of slots + hasher.update(&tx_id); // the slot key that is modified + hasher.update(&[1u8]); // the slot contains a 1 + hasher.update(&32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update(&Bytes32::new({ + let mut value = [0u8; 32]; + value[..8].copy_from_slice(&1u64.to_be_bytes()); + value + })); // The value in the slot is 1 + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!(executed_tx.outputs()[0].state_root(), Some(&Bytes32::new(expected_state_root))); } #[test] diff --git a/crates/services/executor/Cargo.toml b/crates/services/executor/Cargo.toml index 814a841fa12..f5bfbc3697e 100644 --- a/crates/services/executor/Cargo.toml +++ b/crates/services/executor/Cargo.toml @@ -29,6 +29,7 @@ fuel-core-types = { workspace = true, default-features = false, features = [ ] } parking_lot = { workspace = true } serde = { workspace = true } +sha2 = "0.10" tracing = { workspace = true } [dev-dependencies] diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs new file mode 100644 index 00000000000..ac292fdc7e1 --- /dev/null +++ b/crates/services/executor/src/contract_state.rs @@ -0,0 +1,121 @@ +use alloc::{collections::BTreeMap, vec::Vec}; + +use fuel_core_storage::{ + column::Column, kv_store::WriteOperation, transactional::Changes, ContractsAssetKey, ContractsStateKey +}; +use fuel_core_types::{ + fuel_tx::{Bytes32, AssetId, ContractId, Word}, + services::executor::StorageReadReplayEvent, +}; +use sha2::{ + Digest, + Sha256, +}; + +/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution +pub fn compute_balances_hash(record: &[StorageReadReplayEvent], changes: &Changes) -> Bytes32 { + let mut touched_assets: BTreeMap> = BTreeMap::new(); + for r in record { + if r.column == Column::ContractsAssets as u32 { + let key = ContractsAssetKey::from_slice(&r.key).unwrap(); + let contract_id = key.contract_id(); + let asset_id = key.asset_id(); + + touched_assets + .entry(*contract_id) + .or_default() + .insert(asset_id.clone(), r.value.clone().map(|v| { + let mut buf = [0; 8]; + buf.copy_from_slice(v.as_slice()); + Word::from_be_bytes(buf) + }).unwrap_or(0)); + } + } + + for (change_column, change) in changes { + if *change_column == Column::ContractsAssets as u32 { + for (key, value) in change.iter() { + let key = ContractsStateKey::from_slice(&key).unwrap(); + let contract_id = key.contract_id(); + let state_key = key.state_key(); + + touched_assets + .get_mut(contract_id) + .expect("Column cannot have been changed if it was not accessed") + .insert(AssetId::from(*state_key.clone()), match value { + WriteOperation::Insert(v) => { + let mut buf = [0; 8]; + buf.copy_from_slice(v); + Word::from_be_bytes(buf) + }, + WriteOperation::Remove => 0, + }); + } + } + } + + let mut hasher = Sha256::new(); + for (contract_id, values) in touched_assets { + hasher.update(&*contract_id); + hasher.update(&(values.len() as u64).to_be_bytes()); + for (state_key, state_value) in values { + hasher.update(&state_key); + hasher.update(&state_value.to_be_bytes()); + } + } + let digest: [u8; 32] = hasher.finalize().into(); + Bytes32::from(digest) +} + +/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution +pub fn compute_state_hash(record: &[StorageReadReplayEvent], changes: &Changes) -> Bytes32 { + let mut touched_slots: BTreeMap>>> = BTreeMap::new(); + for r in record { + if r.column == Column::ContractsState as u32 { + let key = ContractsStateKey::from_slice(&r.key).unwrap(); + let contract_id = key.contract_id(); + let state_key = key.state_key(); + + touched_slots + .entry(*contract_id) + .or_default() + .insert(state_key.clone(), r.value.clone()); + } + } + + for (change_column, change) in changes { + if *change_column == Column::ContractsState as u32 { + for (key, value) in change.iter() { + let key = ContractsStateKey::from_slice(&key).unwrap(); + let contract_id = key.contract_id(); + let state_key = key.state_key(); + + touched_slots + .get_mut(contract_id) + .expect("Column cannot have been changed if it was not accessed") + .insert(state_key.clone(), match value { + WriteOperation::Insert(v) => Some(v.to_vec()), + WriteOperation::Remove => None, + }); + } + } + } + + let mut hasher = Sha256::new(); + for (contract_id, values) in touched_slots { + hasher.update(&*contract_id); + hasher.update(&(values.len() as u64).to_be_bytes()); + for (state_key, state_value) in values { + hasher.update(&state_key); + if let Some(value) = state_value { + hasher.update(&[1u8]); + hasher.update(&(value.len() as Word).to_be_bytes()); + hasher.update(&value); + } else { + hasher.update(&[0u8]); + } + } + } + let digest: [u8; 32] = hasher.finalize().into(); + Bytes32::from(digest) +} diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index e996b9b91cc..4484d809cb0 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1,4 +1,8 @@ use crate::{ + contract_state::{ + compute_balances_hash, + compute_state_hash, + }, ports::{ MaybeCheckedTransaction, NewTxWaiterPort, @@ -7,6 +11,7 @@ use crate::{ TransactionsSource, }, refs::ContractRef, + storage_access_recorder::StorageAccessRecorder, }; use fuel_core_storage::{ StorageAsMut, @@ -136,6 +141,7 @@ use fuel_core_types::{ ExecutionResult, ForcedTransactionFailure, Result as ExecutorResult, + StorageReadReplayEvent, TransactionExecutionResult, TransactionExecutionStatus, TransactionValidityError, @@ -157,10 +163,10 @@ use tracing::{ }; #[cfg(feature = "std")] -use std::borrow::Cow; +use std::{borrow::Cow}; #[cfg(not(feature = "std"))] -use alloc::borrow::Cow; +use alloc::{borrow::Cow}; #[cfg(feature = "alloc")] use alloc::{ @@ -1420,7 +1426,7 @@ where )?; } - self.compute_inputs(core::slice::from_mut(&mut input), storage_tx)?; + self.compute_inputs(core::slice::from_mut(&mut input), storage_tx, &[])?; let (input, output) = self.execute_mint_with_vm( header, @@ -1590,7 +1596,10 @@ where where T: KeyValueInspect, { - let mut sub_block_db_commit = storage_tx + + let mut storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); + + let mut sub_block_db_commit = storage_tx_record .write_transaction() .with_policy(ConflictPolicy::Overwrite); @@ -1609,7 +1618,11 @@ where ) .map_err(|e| format!("{e}")) .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; - sub_block_db_commit.commit()?; + + let (recorder, changes) = sub_block_db_commit.into_inner(); + let record = core::mem::take(&mut *recorder.record.lock()); + + storage_tx.commit_changes(changes.clone())?; let block_height = *header.height(); let output = *mint.output_contract(); @@ -1624,9 +1637,8 @@ where )?; self.compute_state_of_not_utxo_outputs( outputs.as_mut_slice(), - core::slice::from_ref(input), - *coinbase_id, - storage_tx, + &record, + &changes, )?; let Input::Contract(input) = core::mem::take(input) else { return Err(ExecutorError::Other( @@ -1641,23 +1653,12 @@ where Ok((input, output)) } - fn update_tx_outputs( - &self, - storage_tx: &TxStorageTransaction, - tx_id: TxId, - tx: &mut Tx, - ) -> ExecutorResult<()> + fn update_tx_outputs(&self, tx: &mut Tx, record: &[StorageReadReplayEvent], changes: &Changes) -> ExecutorResult<()> where Tx: ExecutableTransaction, - T: KeyValueInspect, { let mut outputs = core::mem::take(tx.outputs_mut()); - self.compute_state_of_not_utxo_outputs( - &mut outputs, - tx.inputs(), - tx_id, - storage_tx, - )?; + self.compute_state_of_not_utxo_outputs(&mut outputs, record, changes)?; *tx.outputs_mut() = outputs; Ok(()) } @@ -1822,7 +1823,9 @@ where { let tx_id = checked_tx.id(); - let mut sub_block_db_commit = storage_tx + let storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); + + let mut sub_block_db_commit = storage_tx_record .read_transaction() .with_policy(ConflictPolicy::Overwrite); @@ -1929,17 +1932,25 @@ where Self::update_input_used_gas(predicate_gas_used, tx_id, &mut tx)?; + let ( + StorageAccessRecorder { + storage: storage_tx_recovered, + record, + }, + changes, + ) = sub_block_db_commit.into_inner(); + let record = record.lock().clone(); + // We always need to update inputs with storage state before execution, // because VM zeroes malleable fields during the execution. - self.compute_inputs(tx.inputs_mut(), storage_tx)?; + self.compute_inputs(tx.inputs_mut(), storage_tx_recovered, &record)?; // only commit state changes if execution was a success if !reverted { - let changes = sub_block_db_commit.into_changes(); - storage_tx.commit_changes(changes)?; + storage_tx.commit_changes(changes.clone())?; } - self.update_tx_outputs(storage_tx, tx_id, &mut tx)?; + self.update_tx_outputs(&mut tx, &record, &changes)?; Ok((reverted, state, tx, receipts.to_vec())) } @@ -2137,6 +2148,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, + record: &[StorageReadReplayEvent], ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2176,8 +2188,9 @@ where 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()?; - *state_root = contract.state_root()?; + + *balance_root = compute_balances_hash(record, &Changes::default()); + *state_root = compute_state_hash(record, &Changes::default()); } _ => {} } @@ -2190,34 +2203,16 @@ where /// Computes all zeroed or variable outputs. /// In production mode, updates the outputs with computed values. /// In validation mode, compares the outputs with computed inputs. - fn compute_state_of_not_utxo_outputs( + fn compute_state_of_not_utxo_outputs( &self, outputs: &mut [Output], - inputs: &[Input], - tx_id: TxId, - db: &TxStorageTransaction, - ) -> ExecutorResult<()> - where - T: KeyValueInspect, - { + record: &[StorageReadReplayEvent], + changes: &Changes, + ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { - let contract_id = - if let Some(Input::Contract(input::contract::Contract { - contract_id, - .. - })) = inputs.get(contract_output.input_index as usize) - { - contract_id - } else { - return Err(ExecutorError::InvalidTransactionOutcome { - transaction_id: tx_id, - }) - }; - - let contract = ContractRef::new(db, *contract_id); - contract_output.balance_root = contract.balance_root()?; - contract_output.state_root = contract.state_root()?; + contract_output.balance_root = compute_balances_hash(record, changes); + contract_output.state_root = compute_state_hash(record, changes); } } Ok(()) diff --git a/crates/services/executor/src/lib.rs b/crates/services/executor/src/lib.rs index d28e5dbb612..071a33c9fd1 100644 --- a/crates/services/executor/src/lib.rs +++ b/crates/services/executor/src/lib.rs @@ -11,5 +11,8 @@ pub mod executor; pub mod ports; pub mod refs; +mod contract_state; +mod storage_access_recorder; + #[cfg(test)] fuel_core_trace::enable_tracing!(); diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs new file mode 100644 index 00000000000..31c511f29f6 --- /dev/null +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -0,0 +1,52 @@ +use fuel_core_storage::{ + Result as StorageResult, + kv_store::{ + KeyValueInspect, + StorageColumn, + Value, + }, +}; +use fuel_core_types::services::executor::StorageReadReplayEvent; +use parking_lot::Mutex; + +use alloc::{ + sync::Arc, + vec::Vec, +}; + +pub struct StorageAccessRecorder +where + S: KeyValueInspect, +{ + pub storage: S, + pub record: Arc>>, +} + +impl StorageAccessRecorder +where + S: KeyValueInspect, +{ + pub fn new(storage: S) -> Self { + Self { + storage, + record: Default::default(), + } + } +} + +impl KeyValueInspect for StorageAccessRecorder +where + S: KeyValueInspect, +{ + type Column = S::Column; + + fn get(&self, key: &[u8], column: Self::Column) -> StorageResult> { + let value = self.storage.get(key, column)?; + self.record.lock().push(StorageReadReplayEvent { + column: column.id(), + key: key.to_vec(), + value: value.as_ref().map(|v| v.to_vec()), + }); + Ok(value) + } +} From 78a030da8ff6a24dcb94fef9ae95c49fecf49980 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 15 Sep 2025 13:35:43 +0300 Subject: [PATCH 02/22] fmt, clippy --- crates/fuel-core/src/executor.rs | 104 ++++++++++------- .../services/executor/src/contract_state.rs | 109 +++++++++++------- crates/services/executor/src/executor.rs | 12 +- tests/tests/assemble_tx.rs | 10 +- tests/tests/gas_price.rs | 5 +- tests/tests/tx.rs | 2 +- 6 files changed, 150 insertions(+), 92 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index abd8b49e751..18b06fcdeee 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -171,7 +171,6 @@ mod tests { Sha256, }; - #[derive(Clone, Debug, Default)] struct Config { /// Network-wide common parameters used for validating the chain. @@ -496,7 +495,7 @@ mod tests { mint.tx_pointer(), &TxPointer::new(*block.header().height(), 1) ); - let empty_sha: [u8; 32] = Sha256::digest(&[]).into(); + let empty_sha: [u8; 32] = Sha256::digest([]).into(); assert_eq!(mint.mint_asset_id(), &AssetId::BASE); assert_eq!(mint.mint_amount(), &expected_fee_amount_1); assert_eq!(mint.input_contract().contract_id, recipient); @@ -1684,11 +1683,23 @@ mod tests { tx_status[2].result, TransactionExecutionResult::Success { .. } )); - let empty: [u8; 32] = Sha256::digest(&[]).into(); - assert_eq!(executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(empty))); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(empty))); - assert_eq!(executed_tx.outputs()[0].state_root(), Some(&Bytes32::new(empty))); - assert_eq!(executed_tx.outputs()[0].balance_root(), Some(&Bytes32::new(empty))); + let empty: [u8; 32] = Sha256::digest([]).into(); + assert_eq!( + executed_tx.inputs()[0].state_root(), + Some(&Bytes32::new(empty)) + ); + assert_eq!( + executed_tx.inputs()[0].balance_root(), + Some(&Bytes32::new(empty)) + ); + assert_eq!( + executed_tx.outputs()[0].state_root(), + Some(&Bytes32::new(empty)) + ); + assert_eq!( + executed_tx.outputs()[0].balance_root(), + Some(&Bytes32::new(empty)) + ); } #[test] @@ -1749,9 +1760,15 @@ mod tests { ); // Both balance and state roots are empty, and should match hash of empty data. - let empty: [u8; 32] = Sha256::digest(&[]).into(); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(empty))); - assert_eq!(executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(empty))); + let empty: [u8; 32] = Sha256::digest([]).into(); + assert_eq!( + executed_tx.inputs()[0].balance_root(), + Some(&Bytes32::new(empty)) + ); + assert_eq!( + executed_tx.inputs()[0].state_root(), + Some(&Bytes32::new(empty)) + ); } #[test] @@ -1763,13 +1780,10 @@ mod tests { // Create a contract that modifies the state let (create, contract_id) = create_contract( // Sets value 1 to slot matching the tx id - vec![ - op::sww(RegId::ZERO, 0x29, RegId::ONE), - op::ret(1), - ] - .into_iter() - .collect::>() - .as_slice(), + vec![op::sww(RegId::ZERO, 0x29, RegId::ONE), op::ret(1)] + .into_iter() + .collect::>() + .as_slice(), &mut rng, ); @@ -1848,45 +1862,57 @@ mod tests { // Input balances: 0 of asset_id [2; 32] let mut hasher = Sha256::new(); - hasher.update(&contract_id); - hasher.update(&1u64.to_be_bytes()); // number of balances - hasher.update(&asset_id); - hasher.update(&0u64.to_be_bytes()); // balance + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of balances + hasher.update(asset_id); + hasher.update(0u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root))); + assert_eq!( + executed_tx.inputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); // Output balances: 100 of asset_id [2; 32] let mut hasher = Sha256::new(); - hasher.update(&contract_id); - hasher.update(&1u64.to_be_bytes()); // number of balances - hasher.update(&asset_id); - hasher.update(&0u64.to_be_bytes()); // balance + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of balances + hasher.update(asset_id); + hasher.update(0u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); - assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root))); + assert_eq!( + executed_tx.inputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); // Input state: empty slot tx_id let mut hasher = Sha256::new(); - hasher.update(&contract_id); - hasher.update(&1u64.to_be_bytes()); // number of slots - hasher.update(&tx_id); // the slot key that is modified - hasher.update(&[0u8]); // the slot did not contain any value + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of slots + hasher.update(tx_id); // the slot key that is modified + hasher.update([0u8]); // the slot did not contain any value let expected_state_root: [u8; 32] = hasher.finalize().into(); - assert_eq!(executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(expected_state_root))); + assert_eq!( + executed_tx.inputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); // Output state: slot tx_id with value 1 let mut hasher = Sha256::new(); - hasher.update(&contract_id); - hasher.update(&1u64.to_be_bytes()); // number of slots - hasher.update(&tx_id); // the slot key that is modified - hasher.update(&[1u8]); // the slot contains a 1 - hasher.update(&32u64.to_be_bytes()); // slot size is 32 bytes - hasher.update(&Bytes32::new({ + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of slots + hasher.update(tx_id); // the slot key that is modified + hasher.update([1u8]); // the slot contains a 1 + hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update(Bytes32::new({ let mut value = [0u8; 32]; value[..8].copy_from_slice(&1u64.to_be_bytes()); value })); // The value in the slot is 1 let expected_state_root: [u8; 32] = hasher.finalize().into(); - assert_eq!(executed_tx.outputs()[0].state_root(), Some(&Bytes32::new(expected_state_root))); + assert_eq!( + executed_tx.outputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); } #[test] diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs index ac292fdc7e1..d6d3739f031 100644 --- a/crates/services/executor/src/contract_state.rs +++ b/crates/services/executor/src/contract_state.rs @@ -1,10 +1,22 @@ -use alloc::{collections::BTreeMap, vec::Vec}; +use alloc::{ + collections::BTreeMap, + vec::Vec, +}; use fuel_core_storage::{ - column::Column, kv_store::WriteOperation, transactional::Changes, ContractsAssetKey, ContractsStateKey + ContractsAssetKey, + ContractsStateKey, + column::Column, + kv_store::WriteOperation, + transactional::Changes, }; use fuel_core_types::{ - fuel_tx::{Bytes32, AssetId, ContractId, Word}, + fuel_tx::{ + AssetId, + Bytes32, + ContractId, + Word, + }, services::executor::StorageReadReplayEvent, }; use sha2::{ @@ -13,54 +25,64 @@ use sha2::{ }; /// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution -pub fn compute_balances_hash(record: &[StorageReadReplayEvent], changes: &Changes) -> Bytes32 { - let mut touched_assets: BTreeMap> = BTreeMap::new(); +pub fn compute_balances_hash( + record: &[StorageReadReplayEvent], + changes: &Changes, +) -> Bytes32 { + let mut touched_assets: BTreeMap> = + BTreeMap::new(); for r in record { if r.column == Column::ContractsAssets as u32 { let key = ContractsAssetKey::from_slice(&r.key).unwrap(); let contract_id = key.contract_id(); let asset_id = key.asset_id(); - touched_assets - .entry(*contract_id) - .or_default() - .insert(asset_id.clone(), r.value.clone().map(|v| { - let mut buf = [0; 8]; - buf.copy_from_slice(v.as_slice()); - Word::from_be_bytes(buf) - }).unwrap_or(0)); + touched_assets.entry(*contract_id).or_default().insert( + *asset_id, + r.value + .clone() + .map(|v| { + let mut buf = [0; 8]; + buf.copy_from_slice(v.as_slice()); + Word::from_be_bytes(buf) + }) + .unwrap_or(0), + ); } } for (change_column, change) in changes { if *change_column == Column::ContractsAssets as u32 { for (key, value) in change.iter() { - let key = ContractsStateKey::from_slice(&key).unwrap(); + let key = ContractsStateKey::from_slice(key).unwrap(); let contract_id = key.contract_id(); let state_key = key.state_key(); touched_assets .get_mut(contract_id) .expect("Column cannot have been changed if it was not accessed") - .insert(AssetId::from(*state_key.clone()), match value { - WriteOperation::Insert(v) => { - let mut buf = [0; 8]; - buf.copy_from_slice(v); - Word::from_be_bytes(buf) + .insert( + AssetId::from(**state_key), + match value { + WriteOperation::Insert(v) => { + let mut buf = [0; 8]; + buf.copy_from_slice(v); + Word::from_be_bytes(buf) + } + WriteOperation::Remove => 0, }, - WriteOperation::Remove => 0, - }); + ); } } } let mut hasher = Sha256::new(); for (contract_id, values) in touched_assets { - hasher.update(&*contract_id); - hasher.update(&(values.len() as u64).to_be_bytes()); + hasher.update(*contract_id); + hasher.update((values.len() as u64).to_be_bytes()); for (state_key, state_value) in values { - hasher.update(&state_key); - hasher.update(&state_value.to_be_bytes()); + hasher.update(state_key); + hasher.update(state_value.to_be_bytes()); } } let digest: [u8; 32] = hasher.finalize().into(); @@ -68,8 +90,12 @@ pub fn compute_balances_hash(record: &[StorageReadReplayEvent], changes: &Change } /// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution -pub fn compute_state_hash(record: &[StorageReadReplayEvent], changes: &Changes) -> Bytes32 { - let mut touched_slots: BTreeMap>>> = BTreeMap::new(); +pub fn compute_state_hash( + record: &[StorageReadReplayEvent], + changes: &Changes, +) -> Bytes32 { + let mut touched_slots: BTreeMap>>> = + BTreeMap::new(); for r in record { if r.column == Column::ContractsState as u32 { let key = ContractsStateKey::from_slice(&r.key).unwrap(); @@ -79,40 +105,43 @@ pub fn compute_state_hash(record: &[StorageReadReplayEvent], changes: &Changes) touched_slots .entry(*contract_id) .or_default() - .insert(state_key.clone(), r.value.clone()); + .insert(*state_key, r.value.clone()); } } for (change_column, change) in changes { if *change_column == Column::ContractsState as u32 { for (key, value) in change.iter() { - let key = ContractsStateKey::from_slice(&key).unwrap(); + let key = ContractsStateKey::from_slice(key).unwrap(); let contract_id = key.contract_id(); let state_key = key.state_key(); - + touched_slots .get_mut(contract_id) .expect("Column cannot have been changed if it was not accessed") - .insert(state_key.clone(), match value { - WriteOperation::Insert(v) => Some(v.to_vec()), - WriteOperation::Remove => None, - }); + .insert( + *state_key, + match value { + WriteOperation::Insert(v) => Some(v.to_vec()), + WriteOperation::Remove => None, + }, + ); } } } let mut hasher = Sha256::new(); for (contract_id, values) in touched_slots { - hasher.update(&*contract_id); - hasher.update(&(values.len() as u64).to_be_bytes()); + hasher.update(*contract_id); + hasher.update((values.len() as u64).to_be_bytes()); for (state_key, state_value) in values { - hasher.update(&state_key); + hasher.update(state_key); if let Some(value) = state_value { - hasher.update(&[1u8]); - hasher.update(&(value.len() as Word).to_be_bytes()); + hasher.update([1u8]); + hasher.update((value.len() as Word).to_be_bytes()); hasher.update(&value); } else { - hasher.update(&[0u8]); + hasher.update([0u8]); } } } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 4484d809cb0..1a3450ad5cd 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -163,10 +163,10 @@ use tracing::{ }; #[cfg(feature = "std")] -use std::{borrow::Cow}; +use std::borrow::Cow; #[cfg(not(feature = "std"))] -use alloc::{borrow::Cow}; +use alloc::borrow::Cow; #[cfg(feature = "alloc")] use alloc::{ @@ -1596,7 +1596,6 @@ where where T: KeyValueInspect, { - let mut storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); let mut sub_block_db_commit = storage_tx_record @@ -1653,7 +1652,12 @@ where Ok((input, output)) } - fn update_tx_outputs(&self, tx: &mut Tx, record: &[StorageReadReplayEvent], changes: &Changes) -> ExecutorResult<()> + fn update_tx_outputs( + &self, + tx: &mut Tx, + record: &[StorageReadReplayEvent], + changes: &Changes, + ) -> ExecutorResult<()> where Tx: ExecutableTransaction, { diff --git a/tests/tests/assemble_tx.rs b/tests/tests/assemble_tx.rs index a0c376e6579..c00b2350756 100644 --- a/tests/tests/assemble_tx.rs +++ b/tests/tests/assemble_tx.rs @@ -259,16 +259,16 @@ async fn assemble_transaction__finds_another_input_if_inputs_not_spendable() { let amount = Word::MAX / 2; let sender = Address::default(); let message = MessageConfig { - sender: sender.clone(), - recipient: recipient.clone(), - nonce: nonce.clone(), + sender, + recipient, + nonce, amount, data: data.clone(), ..Default::default() }; let second_message = MessageConfig { - sender: sender.clone(), - recipient: recipient.clone(), + sender, + recipient, nonce: [2; 32].into(), amount: Word::MAX / 2, data: vec![], diff --git a/tests/tests/gas_price.rs b/tests/tests/gas_price.rs index 778df45d98b..7099a9c526b 100644 --- a/tests/tests/gas_price.rs +++ b/tests/tests/gas_price.rs @@ -78,7 +78,6 @@ use rand::{ }; use std::{ self, - iter::repeat, ops::Deref, time::Duration, }; @@ -116,7 +115,7 @@ fn arb_large_tx( rng: &mut R, asset_id: Option, ) -> Transaction { - let mut script: Vec<_> = repeat(op::noop()).take(10_000).collect(); + let mut script: Vec<_> = std::iter::repeat_n(op::noop(), 10_000).collect(); script.push(op::ret(RegId::ONE)); let script_bytes = script.iter().flat_map(|op| op.to_bytes()).collect(); let mut builder = TransactionBuilder::script(script_bytes, vec![]); @@ -139,7 +138,7 @@ fn arb_small_tx( max_fee_limit: Word, rng: &mut R, ) -> Transaction { - let mut script: Vec<_> = repeat(op::noop()).take(10).collect(); + let mut script: Vec<_> = std::iter::repeat_n(op::noop(), 10).collect(); script.push(op::ret(RegId::ONE)); let script_bytes = script.iter().flat_map(|op| op.to_bytes()).collect(); let mut builder = TransactionBuilder::script(script_bytes, vec![]); diff --git a/tests/tests/tx.rs b/tests/tests/tx.rs index bcf27fb7673..90bee66290f 100644 --- a/tests/tests/tx.rs +++ b/tests/tests/tx.rs @@ -211,7 +211,7 @@ fn arb_large_script_tx( size: usize, rng: &mut R, ) -> Transaction { - let mut script: Vec<_> = std::iter::repeat(op::noop()).take(size).collect(); + let mut script: Vec<_> = std::iter::repeat_n(op::noop(), size).collect(); script.push(op::ret(RegId::ONE)); let script_bytes = script.iter().flat_map(|op| op.to_bytes()).collect(); let mut builder = TransactionBuilder::script(script_bytes, vec![]); From be0e90146acf1311d7012837a928057725b333d3 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 15 Sep 2025 13:38:04 +0300 Subject: [PATCH 03/22] Add changelog --- .changes/breaking/3099.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/breaking/3099.md diff --git a/.changes/breaking/3099.md b/.changes/breaking/3099.md new file mode 100644 index 00000000000..98aa93c30ba --- /dev/null +++ b/.changes/breaking/3099.md @@ -0,0 +1 @@ +Use hash-based `balance_root` and `state_root` fields in tx inputs and outputs \ No newline at end of file From d82bf5e97f852fd120a5460fb6031008296dbd4b Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 15 Sep 2025 13:47:31 +0300 Subject: [PATCH 04/22] Fix no_std issues --- crates/services/executor/src/contract_state.rs | 4 ++++ crates/services/executor/src/storage_access_recorder.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs index d6d3739f031..60a4d809f14 100644 --- a/crates/services/executor/src/contract_state.rs +++ b/crates/services/executor/src/contract_state.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "std")] +use std::collections::BTreeMap; + +#[cfg(all(feature = "alloc", not(feature = "std")))] use alloc::{ collections::BTreeMap, vec::Vec, diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 31c511f29f6..55e397914fa 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -9,6 +9,10 @@ use fuel_core_storage::{ use fuel_core_types::services::executor::StorageReadReplayEvent; use parking_lot::Mutex; +#[cfg(feature = "std")] +use std::sync::Arc; + +#[cfg(not(feature = "std"))] use alloc::{ sync::Arc, vec::Vec, From c57b2d24c4d1f20a58186673bd3cfee5a617e638 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 17 Sep 2025 09:13:05 +0300 Subject: [PATCH 05/22] Add new test cases --- crates/fuel-core/src/executor.rs | 564 +++++++++++++++++- .../services/executor/src/contract_state.rs | 4 + 2 files changed, 537 insertions(+), 31 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 18b06fcdeee..797a830e0a7 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -73,10 +73,7 @@ mod tests { op, }, fuel_crypto::SecretKey, - fuel_merkle::{ - common::empty_sum_sha256, - sparse, - }, + fuel_merkle::common::empty_sum_sha256, fuel_tx::{ Bytes32, ConsensusParameters, @@ -1652,8 +1649,8 @@ mod tests { .transaction() .clone() .into(); - let db = &mut Database::default(); + let db = &mut Database::default(); let mut executor = create_executor( db.clone(), Config { @@ -1677,12 +1674,16 @@ mod tests { block, tx_status, .. } = executor.produce_and_commit(block).unwrap(); - // Assert the balance and state roots should be the same before and after execution. + // Ensure all txs succeeded. + assert!( + tx_status.iter().all(|s| (matches!( + s.result, + TransactionExecutionResult::Success { .. } + ))) + ); + + // Assert the balance and state roots are same before and after execution. let executed_tx = block.transactions()[1].as_script().unwrap(); - assert!(matches!( - tx_status[2].result, - TransactionExecutionResult::Success { .. } - )); let empty: [u8; 32] = Sha256::digest([]).into(); assert_eq!( executed_tx.inputs()[0].state_root(), @@ -1719,8 +1720,8 @@ mod tests { .transaction() .clone() .into(); - let db = &mut Database::default(); + let db = &mut Database::default(); let mut executor = create_executor( db.clone(), Config { @@ -1779,17 +1780,22 @@ mod tests { // Create a contract that modifies the state let (create, contract_id) = create_contract( - // Sets value 1 to slot matching the tx id - vec![op::sww(RegId::ZERO, 0x29, RegId::ONE), op::ret(1)] - .into_iter() - .collect::>() - .as_slice(), + // Increment the slot matching the tx id by one + vec![ + op::srw(0x10, 0x29, RegId::ZERO), + op::addi(0x10, 0x10, 1), + op::sww(RegId::ZERO, 0x29, 0x10), + op::ret(1), + ] + .into_iter() + .collect::>() + .as_slice(), &mut rng, ); let transfer_amount = 100 as Word; let asset_id = AssetId::from([2; 32]); - let (script, data_offset) = script_with_data_offset!( + let (script, _) = script_with_data_offset!( data_offset, vec![ // Set register `0x10` to `Call` @@ -1806,9 +1812,7 @@ mod tests { let script_data: Vec = [ asset_id.as_ref(), - Call::new(contract_id, transfer_amount, data_offset as Word) - .to_bytes() - .as_ref(), + Call::new(contract_id, 0, 0).to_bytes().as_ref(), ] .into_iter() .flatten() @@ -1850,10 +1854,13 @@ mod tests { let ExecutionResult { block, tx_status, .. } = executor.produce_and_commit(block).unwrap(); - assert!(matches!( - tx_status[2].result, - TransactionExecutionResult::Success { .. } - )); + + assert!( + tx_status.iter().all(|s| (matches!( + s.result, + TransactionExecutionResult::Success { .. } + ))) + ); let executed_tx = block.transactions()[1].as_script().unwrap(); let tx_id = executed_tx.id(&ConsensusParameters::standard().chain_id()); @@ -1877,6 +1884,145 @@ mod tests { hasher.update(contract_id); hasher.update(1u64.to_be_bytes()); // number of balances hasher.update(asset_id); + hasher.update(100u64.to_be_bytes()); // balance + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Input state: empty slot tx_id + let mut hasher = Sha256::new(); + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of slots + hasher.update(tx_id); // the slot key that is modified + hasher.update([0u8]); // the slot did not contain any value + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.inputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + + // Output state: slot tx_id with value 1 + let mut hasher = Sha256::new(); + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of slots + hasher.update(tx_id); // the slot key that is modified + hasher.update([1u8]); // the slot contains a value + hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update({ + let mut value = [0u8; 32]; + value[..8].copy_from_slice(&1u64.to_be_bytes()); // the value is 1 + value + }); // The value in the slot is 1 + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + } + + #[test] + fn contracts_balance_and_state_roots_updated_correctly_with_multiple_modifications() { + // Values in inputs and outputs are random. If the execution of the transaction that + // modifies the state and the balance is successful, it should update roots. + let mut rng = StdRng::seed_from_u64(2322u64); + + // Create a contract that modifies the state + let (create, contract_id) = create_contract( + // Increment the slot matching the tx id by one + vec![ + op::srw(0x10, 0x29, RegId::ZERO), + op::addi(0x10, 0x10, 1), + op::sww(RegId::ZERO, 0x29, 0x10), + op::ret(1), + ] + .into_iter() + .collect::>() + .as_slice(), + &mut rng, + ); + + let transfer_amount = 100 as Word; + let asset_id = AssetId::from([2; 32]); + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // Set register `0x10` to `Call` + op::movi(0x10, data_offset + AssetId::LEN as u32), + // Set register `0x11` with offset to data that contains `asset_id` + op::movi(0x11, data_offset), + // Set register `0x12` with `transfer_amount` + op::movi(0x12, transfer_amount as u32), + op::call(0x10, 0x12, 0x11, RegId::CGAS), + // call again for the second increment, but don't transfer any tokens + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ], + TxParameters::DEFAULT.tx_offset() + ); + + let script_data: Vec = [ + asset_id.as_ref(), + Call::new(contract_id, 0, 0).to_bytes().as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let modify_balance_and_state_tx = TxBuilder::new(2322) + .script_gas_limit(10000) + .coin_input(AssetId::zeroed(), 10000) + .start_script(script, script_data) + .contract_input(contract_id) + .coin_input(asset_id, transfer_amount) + .fee_input() + .contract_output(&contract_id) + .build() + .transaction() + .clone(); + + let db = Database::default(); + let mut executor = create_executor( + db.clone(), + Config { + forbid_fake_coins_default: false, + ..Default::default() + }, + ); + + let block = PartialFuelBlock { + header: PartialBlockHeader { + consensus: ConsensusHeader { + height: 1.into(), + ..Default::default() + }, + ..Default::default() + }, + transactions: vec![create.into(), modify_balance_and_state_tx.into()], + }; + + let ExecutionResult { + block, tx_status, .. + } = executor.produce_and_commit(block).unwrap(); + assert!( + tx_status.iter().all(|s| (matches!( + s.result, + TransactionExecutionResult::Success { .. } + ))) + ); + + let executed_tx = block.transactions()[1].as_script().unwrap(); + let tx_id = executed_tx.id(&ConsensusParameters::standard().chain_id()); + + // Check resulting roots + + // Input balances: 0 of asset_id [2; 32] + let mut hasher = Sha256::new(); + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of balances + hasher.update(asset_id); hasher.update(0u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); assert_eq!( @@ -1884,6 +2030,18 @@ mod tests { Some(&Bytes32::new(expected_balance_root)) ); + // Output balances: 100 of asset_id [2; 32] + let mut hasher = Sha256::new(); + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of balances + hasher.update(asset_id); + hasher.update(100u64.to_be_bytes()); // balance + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + // Input state: empty slot tx_id let mut hasher = Sha256::new(); hasher.update(contract_id); @@ -1896,18 +2054,363 @@ mod tests { Some(&Bytes32::new(expected_state_root)) ); - // Output state: slot tx_id with value 1 + // Output state: slot tx_id with value 2 let mut hasher = Sha256::new(); hasher.update(contract_id); hasher.update(1u64.to_be_bytes()); // number of slots hasher.update(tx_id); // the slot key that is modified - hasher.update([1u8]); // the slot contains a 1 + hasher.update([1u8]); // the slot has a value hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes - hasher.update(Bytes32::new({ + hasher.update({ let mut value = [0u8; 32]; - value[..8].copy_from_slice(&1u64.to_be_bytes()); + value[..8].copy_from_slice(&2u64.to_be_bytes()); // the value is 2 value - })); // The value in the slot is 1 + }); // The value in the slot is 1 + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + } + + #[test] + fn contracts_balance_and_state_roots_updated_correctly_after_modifying_multiple_slots() + { + // Values in inputs and outputs are random. If the execution of the transaction that + // modifies the state and the balance is successful, it should update roots. + let mut rng = StdRng::seed_from_u64(2322u64); + + // Create a contract that modifies the state + let (create, contract_id) = create_contract( + // Set first four slots to contain their slot keysz + vec![ + // Allocate space for the ids + op::movi(0x10, 32), + op::aloc(0x10), + // Store the values + op::movi(0x10, 0), + op::sww(RegId::HP, 0x29, 0x10), + op::addi(0x10, 0x10, 1), + op::sw(RegId::HP, 0x10, 0), + op::sww(RegId::HP, 0x29, 0x10), + op::addi(0x10, 0x10, 1), + op::sw(RegId::HP, 0x10, 0), + op::sww(RegId::HP, 0x29, 0x10), + op::addi(0x10, 0x10, 1), + op::sw(RegId::HP, 0x10, 0), + op::sww(RegId::HP, 0x29, 0x10), + // Done + op::ret(1), + ] + .into_iter() + .collect::>() + .as_slice(), + &mut rng, + ); + + let transfer_amount = 100 as Word; + let asset_id = AssetId::from([2; 32]); + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // Set register `0x10` to `Call` + op::movi(0x10, data_offset + AssetId::LEN as u32), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ], + TxParameters::DEFAULT.tx_offset() + ); + + let script_data: Vec = [ + asset_id.as_ref(), + Call::new(contract_id, 0, 0).to_bytes().as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let mut builder = TxBuilder::new(2322); + + let tx_1 = builder + .script_gas_limit(10000) + .coin_input(AssetId::zeroed(), 10000) + .start_script(script.clone(), script_data.clone()) + .contract_input(contract_id) + .coin_input(asset_id, transfer_amount) + .fee_input() + .contract_output(&contract_id) + .build() + .transaction() + .clone(); + + let tx_2 = builder + .script_gas_limit(10000) + .coin_input(AssetId::zeroed(), 10000) + .start_script(script, script_data) + .contract_input(contract_id) + .coin_input(asset_id, transfer_amount) + .fee_input() + .contract_output(&contract_id) + .build() + .transaction() + .clone(); + + let db = Database::default(); + let mut executor = create_executor( + db.clone(), + Config { + forbid_fake_coins_default: false, + ..Default::default() + }, + ); + + let block = PartialFuelBlock { + header: PartialBlockHeader { + consensus: ConsensusHeader { + height: 1.into(), + ..Default::default() + }, + ..Default::default() + }, + transactions: vec![create.into(), tx_1.into(), tx_2.into()], + }; + + let ExecutionResult { + block, tx_status, .. + } = executor.produce_and_commit(block).unwrap(); + assert!(matches!( + tx_status[3].result, + TransactionExecutionResult::Success { .. } + )); + + let executed_tx_1 = block.transactions()[1].as_script().unwrap(); + let executed_tx_2 = block.transactions()[2].as_script().unwrap(); + + // Check resulting roots + + // Input/Output balances for both txs: None + let expected_balance_root: [u8; 32] = Sha256::digest(&[]).into(); + assert_eq!( + executed_tx_1.inputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + assert_eq!( + executed_tx_1.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + assert_eq!( + executed_tx_2.inputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + assert_eq!( + executed_tx_2.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Input state for tx 1: empty slots + let mut hasher = Sha256::new(); + hasher.update(contract_id); + hasher.update(4u64.to_be_bytes()); // number of slots + for i in 0..4u64 { + let mut slot_id = [0u8; 32]; + slot_id[..8].copy_from_slice(&i.to_be_bytes()); + hasher.update(slot_id); // the slot key that is modified + hasher.update([0u8]); // the slot did not contain any value + } + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx_1.inputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + + // Output state for tx 1 and input/output state for tx 1: slots with values 0, 1, 2, 3 + let mut hasher = Sha256::new(); + hasher.update(contract_id); + hasher.update(4u64.to_be_bytes()); // number of slots + for i in 0..4u64 { + let mut slot_id = [0u8; 32]; + slot_id[..8].copy_from_slice(&i.to_be_bytes()); + hasher.update(slot_id); // the slot key that is modified + hasher.update([1u8]); // the slot contains a value + hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update(slot_id); // slot value (matches the id) + } + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx_1.outputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + assert_eq!( + executed_tx_2.inputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + assert_eq!( + executed_tx_2.outputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + } + + #[test] + fn contracts_balance_and_state_roots_updated_correctly_after_calling_multiple_contracts() + { + // Values in inputs and outputs are random. If the execution of the transaction that + // modifies the state and the balance is successful, it should update roots. + let mut rng = StdRng::seed_from_u64(2322u64); + + // Increment the slot matching the tx id by one + let contract_code = vec![ + op::srw(0x10, 0x29, RegId::ZERO), + op::addi(0x10, 0x10, 1), + op::sww(RegId::ZERO, 0x29, 0x10), + op::ret(1), + ] + .into_iter() + .collect::>(); + + // Create a two different contracts that modify the state + let (create1, contract_id1) = create_contract(&contract_code, &mut rng); + let (create2, contract_id2) = create_contract(&contract_code, &mut rng); + + let transfer_amount = 100 as Word; + let asset_id = AssetId::from([2; 32]); + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // Set register `0x11` with offset to data that contains `asset_id` + op::movi(0x11, data_offset), + // Set register `0x12` with `transfer_amount` + op::movi(0x12, transfer_amount as u32), + // Call first contract + op::movi(0x10, data_offset + AssetId::LEN as u32), + op::call(0x10, 0x12, 0x11, RegId::CGAS), + // Call second contract + op::movi(0x10, data_offset + AssetId::LEN as u32 + Call::LEN as u32), + op::call(0x10, 0x12, 0x11, RegId::CGAS), + op::ret(RegId::ONE), + ], + TxParameters::DEFAULT.tx_offset() + ); + + let script_data: Vec = [ + asset_id.as_ref(), + Call::new(contract_id1, 0, 0).to_bytes().as_ref(), + Call::new(contract_id2, 0, 0).to_bytes().as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let tx = TxBuilder::new(2322) + .script_gas_limit(10000) + .coin_input(AssetId::zeroed(), 10000) + .start_script(script, script_data) + .contract_input(contract_id1) + .contract_input(contract_id2) + .coin_input(asset_id, transfer_amount * 2) + .fee_input() + .contract_output(&contract_id1) + .contract_output(&contract_id2) + .build() + .transaction() + .clone(); + + let db = Database::default(); + let mut executor = create_executor( + db.clone(), + Config { + forbid_fake_coins_default: false, + ..Default::default() + }, + ); + + let block = PartialFuelBlock { + header: PartialBlockHeader { + consensus: ConsensusHeader { + height: 1.into(), + ..Default::default() + }, + ..Default::default() + }, + transactions: vec![create1.into(), create2.into(), tx.into()], + }; + + let ExecutionResult { + block, tx_status, .. + } = executor.produce_and_commit(block).unwrap(); + println!("{tx_status:#?}"); + assert!( + tx_status.iter().all(|s| (matches!( + s.result, + TransactionExecutionResult::Success { .. } + ))) + ); + + let executed_tx = block.transactions()[2].as_script().unwrap(); + let tx_id = executed_tx.id(&ConsensusParameters::standard().chain_id()); + + // Check resulting roots + + let mut contract_ids = [contract_id1, contract_id2]; + contract_ids.sort(); + + // Input balances: 0 of asset_id [2; 32] for both contracts + let mut hasher = Sha256::new(); + for contract_id in &contract_ids { + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of balances + hasher.update(asset_id); + hasher.update(0u64.to_be_bytes()); // balance + } + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.inputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Output balances: 100 of asset_id [2; 32] for both contracts + let mut hasher = Sha256::new(); + for contract_id in &contract_ids { + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of balances + hasher.update(asset_id); + hasher.update(100u64.to_be_bytes()); // balance + } + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Input state: empty slots for both contracts + let mut hasher = Sha256::new(); + for contract_id in &contract_ids { + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of slots + hasher.update(tx_id); // the slot key matches tx_id + hasher.update([0u8]); // the slot did not contain any value + } + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.inputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + + // Output state: the slot tx_id with value 1 for both contracts + let mut hasher = Sha256::new(); + for contract_id in &contract_ids { + hasher.update(contract_id); + hasher.update(1u64.to_be_bytes()); // number of slots + hasher.update(tx_id); // the slot key matches tx_id + hasher.update([1u8]); // the slot contains a value + hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update({ + let mut value = [0u8; 32]; + value[..8].copy_from_slice(&1u64.to_be_bytes()); // the value is 1 + value + }); // slot value (matches the id) + } let expected_state_root: [u8; 32] = hasher.finalize().into(); assert_eq!( executed_tx.outputs()[0].state_root(), @@ -1961,10 +2464,9 @@ mod tests { let _ = executor.produce_and_commit(block).unwrap(); // Assert the balance root should not be affected. - let empty_state = (*sparse::empty_sum()).into(); assert_eq!( ContractRef::new(db, contract_id).balance_root().unwrap(), - empty_state + Bytes32::zeroed() ); } diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs index 60a4d809f14..bdcbb60eafd 100644 --- a/crates/services/executor/src/contract_state.rs +++ b/crates/services/executor/src/contract_state.rs @@ -28,6 +28,8 @@ use sha2::{ Sha256, }; +/// Computes a hash of all contract balances that were read or modified. +/// The hash is not dependent on the order of reads or writes. /// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution pub fn compute_balances_hash( record: &[StorageReadReplayEvent], @@ -93,6 +95,8 @@ pub fn compute_balances_hash( Bytes32::from(digest) } +/// Computes a hash of all contract state slots that were read or modified. +/// The hash is not dependent on the order of reads or writes. /// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution pub fn compute_state_hash( record: &[StorageReadReplayEvent], From 6656479df62754e414a712d70ad0f1e86d213651 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 17 Sep 2025 09:54:02 +0300 Subject: [PATCH 06/22] Use per-contract state in hashes (smh) --- crates/fuel-core/src/executor.rs | 198 ++++++++++++------ .../services/executor/src/contract_state.rs | 125 ++++++----- crates/services/executor/src/executor.rs | 40 +++- 3 files changed, 232 insertions(+), 131 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 797a830e0a7..471482355ea 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -1869,8 +1869,6 @@ mod tests { // Input balances: 0 of asset_id [2; 32] let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of balances hasher.update(asset_id); hasher.update(0u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); @@ -1881,8 +1879,6 @@ mod tests { // Output balances: 100 of asset_id [2; 32] let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of balances hasher.update(asset_id); hasher.update(100u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); @@ -1893,8 +1889,6 @@ mod tests { // Input state: empty slot tx_id let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of slots hasher.update(tx_id); // the slot key that is modified hasher.update([0u8]); // the slot did not contain any value let expected_state_root: [u8; 32] = hasher.finalize().into(); @@ -1905,8 +1899,6 @@ mod tests { // Output state: slot tx_id with value 1 let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of slots hasher.update(tx_id); // the slot key that is modified hasher.update([1u8]); // the slot contains a value hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes @@ -2020,8 +2012,6 @@ mod tests { // Input balances: 0 of asset_id [2; 32] let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of balances hasher.update(asset_id); hasher.update(0u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); @@ -2032,8 +2022,6 @@ mod tests { // Output balances: 100 of asset_id [2; 32] let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of balances hasher.update(asset_id); hasher.update(100u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); @@ -2044,8 +2032,6 @@ mod tests { // Input state: empty slot tx_id let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of slots hasher.update(tx_id); // the slot key that is modified hasher.update([0u8]); // the slot did not contain any value let expected_state_root: [u8; 32] = hasher.finalize().into(); @@ -2056,8 +2042,6 @@ mod tests { // Output state: slot tx_id with value 2 let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of slots hasher.update(tx_id); // the slot key that is modified hasher.update([1u8]); // the slot has a value hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes @@ -2190,7 +2174,7 @@ mod tests { // Check resulting roots // Input/Output balances for both txs: None - let expected_balance_root: [u8; 32] = Sha256::digest(&[]).into(); + let expected_balance_root: [u8; 32] = Sha256::digest([]).into(); assert_eq!( executed_tx_1.inputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root)) @@ -2210,8 +2194,6 @@ mod tests { // Input state for tx 1: empty slots let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(4u64.to_be_bytes()); // number of slots for i in 0..4u64 { let mut slot_id = [0u8; 32]; slot_id[..8].copy_from_slice(&i.to_be_bytes()); @@ -2226,8 +2208,6 @@ mod tests { // Output state for tx 1 and input/output state for tx 1: slots with values 0, 1, 2, 3 let mut hasher = Sha256::new(); - hasher.update(contract_id); - hasher.update(4u64.to_be_bytes()); // number of slots for i in 0..4u64 { let mut slot_id = [0u8; 32]; slot_id[..8].copy_from_slice(&i.to_be_bytes()); @@ -2251,11 +2231,13 @@ mod tests { ); } + /// Creates two different contracts that modify the state and calls them both from a single + /// transaction. Then creates another transaction that calls one of the contracts again. + /// Ensures the balance and state roots are updated correctly after each call, and that they + /// are properly independent between the two contracts. #[test] fn contracts_balance_and_state_roots_updated_correctly_after_calling_multiple_contracts() { - // Values in inputs and outputs are random. If the execution of the transaction that - // modifies the state and the balance is successful, it should update roots. let mut rng = StdRng::seed_from_u64(2322u64); // Increment the slot matching the tx id by one @@ -2274,7 +2256,10 @@ mod tests { let transfer_amount = 100 as Word; let asset_id = AssetId::from([2; 32]); - let (script, _) = script_with_data_offset!( + + let mut builder = TxBuilder::new(2322); + + let (script1, _) = script_with_data_offset!( data_offset, vec![ // Set register `0x11` with offset to data that contains `asset_id` @@ -2292,7 +2277,7 @@ mod tests { TxParameters::DEFAULT.tx_offset() ); - let script_data: Vec = [ + let script_data1: Vec = [ asset_id.as_ref(), Call::new(contract_id1, 0, 0).to_bytes().as_ref(), Call::new(contract_id2, 0, 0).to_bytes().as_ref(), @@ -2302,10 +2287,10 @@ mod tests { .copied() .collect(); - let tx = TxBuilder::new(2322) + let tx1 = builder .script_gas_limit(10000) .coin_input(AssetId::zeroed(), 10000) - .start_script(script, script_data) + .start_script(script1, script_data1) .contract_input(contract_id1) .contract_input(contract_id2) .coin_input(asset_id, transfer_amount * 2) @@ -2316,6 +2301,42 @@ mod tests { .transaction() .clone(); + let (script2, _) = script_with_data_offset!( + data_offset, + vec![ + // Set register `0x11` with offset to data that contains `asset_id` + op::movi(0x11, data_offset), + // Set register `0x12` with `transfer_amount` + op::movi(0x12, transfer_amount as u32), + // Call first contract + op::movi(0x10, data_offset + AssetId::LEN as u32), + op::call(0x10, 0x12, 0x11, RegId::CGAS), + op::ret(RegId::ONE), + ], + TxParameters::DEFAULT.tx_offset() + ); + + let script_data2: Vec = [ + asset_id.as_ref(), + Call::new(contract_id1, 0, 0).to_bytes().as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let tx2 = builder + .script_gas_limit(10000) + .coin_input(AssetId::zeroed(), 10000) + .start_script(script2, script_data2) + .contract_input(contract_id1) + .coin_input(asset_id, transfer_amount) + .fee_input() + .contract_output(&contract_id1) + .build() + .transaction() + .clone(); + let db = Database::default(); let mut executor = create_executor( db.clone(), @@ -2333,13 +2354,12 @@ mod tests { }, ..Default::default() }, - transactions: vec![create1.into(), create2.into(), tx.into()], + transactions: vec![create1.into(), create2.into(), tx1.into(), tx2.into()], }; let ExecutionResult { block, tx_status, .. } = executor.produce_and_commit(block).unwrap(); - println!("{tx_status:#?}"); assert!( tx_status.iter().all(|s| (matches!( s.result, @@ -2347,70 +2367,124 @@ mod tests { ))) ); - let executed_tx = block.transactions()[2].as_script().unwrap(); - let tx_id = executed_tx.id(&ConsensusParameters::standard().chain_id()); - // Check resulting roots + // Tx 1 + let executed_tx = block.transactions()[2].as_script().unwrap(); + let tx_id1 = executed_tx.id(&ConsensusParameters::standard().chain_id()); + let mut contract_ids = [contract_id1, contract_id2]; contract_ids.sort(); // Input balances: 0 of asset_id [2; 32] for both contracts let mut hasher = Sha256::new(); - for contract_id in &contract_ids { - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of balances - hasher.update(asset_id); - hasher.update(0u64.to_be_bytes()); // balance - } + hasher.update(asset_id); + hasher.update(0u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); assert_eq!( executed_tx.inputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root)) ); + assert_eq!( + executed_tx.inputs()[1].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); // Output balances: 100 of asset_id [2; 32] for both contracts let mut hasher = Sha256::new(); - for contract_id in &contract_ids { - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of balances - hasher.update(asset_id); - hasher.update(100u64.to_be_bytes()); // balance - } + hasher.update(asset_id); + hasher.update(100u64.to_be_bytes()); // balance let expected_balance_root: [u8; 32] = hasher.finalize().into(); assert_eq!( executed_tx.outputs()[0].balance_root(), Some(&Bytes32::new(expected_balance_root)) ); + assert_eq!( + executed_tx.outputs()[1].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); // Input state: empty slots for both contracts let mut hasher = Sha256::new(); - for contract_id in &contract_ids { - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of slots - hasher.update(tx_id); // the slot key matches tx_id - hasher.update([0u8]); // the slot did not contain any value - } + hasher.update(tx_id1); // the slot key matches tx_id + hasher.update([0u8]); // the slot did not contain any value let expected_state_root: [u8; 32] = hasher.finalize().into(); assert_eq!( executed_tx.inputs()[0].state_root(), Some(&Bytes32::new(expected_state_root)) ); + assert_eq!( + executed_tx.inputs()[1].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); // Output state: the slot tx_id with value 1 for both contracts let mut hasher = Sha256::new(); - for contract_id in &contract_ids { - hasher.update(contract_id); - hasher.update(1u64.to_be_bytes()); // number of slots - hasher.update(tx_id); // the slot key matches tx_id - hasher.update([1u8]); // the slot contains a value - hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes - hasher.update({ - let mut value = [0u8; 32]; - value[..8].copy_from_slice(&1u64.to_be_bytes()); // the value is 1 - value - }); // slot value (matches the id) - } + hasher.update(tx_id1); // the slot key matches tx_id + hasher.update([1u8]); // the slot contains a value + hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update({ + let mut value = [0u8; 32]; + value[..8].copy_from_slice(&1u64.to_be_bytes()); // the value is 1 + value + }); + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + assert_eq!( + executed_tx.outputs()[1].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + + // Tx 2 + let executed_tx = block.transactions()[3].as_script().unwrap(); + let tx_id2 = executed_tx.id(&ConsensusParameters::standard().chain_id()); + + let mut tx_ids = [tx_id1, tx_id2]; + tx_ids.sort(); + + // Input balance: 100 of asset_id [2; 32] + let mut hasher = Sha256::new(); + hasher.update(asset_id); + hasher.update(100u64.to_be_bytes()); // balance + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.inputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Output balance: 200 of asset_id [2; 32] + let mut hasher = Sha256::new(); + hasher.update(asset_id); + hasher.update(200u64.to_be_bytes()); // balance + let expected_balance_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Input state: one empty slot (from tx 2), the slot from tx 1 is not accessed + let mut hasher = Sha256::new(); + hasher.update(tx_id2); // the slot key matches tx_id + hasher.update([0u8]); // the slot contains no value + let expected_state_root: [u8; 32] = hasher.finalize().into(); + assert_eq!( + executed_tx.inputs()[0].state_root(), + Some(&Bytes32::new(expected_state_root)) + ); + + // Input state: one slot with value 1 (from tx 2), the slot from tx 1 is not accessed + let mut hasher = Sha256::new(); + hasher.update(tx_id2); // the slot key matches tx_id + hasher.update([1u8]); // the slot contains a value + hasher.update(32u64.to_be_bytes()); // slot size is 32 bytes + hasher.update({ + let mut value = [0u8; 32]; + value[..8].copy_from_slice(&1u64.to_be_bytes()); // the value is 1 + value + }); let expected_state_root: [u8; 32] = hasher.finalize().into(); assert_eq!( executed_tx.outputs()[0].state_root(), diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs index bdcbb60eafd..5f628fc9dc0 100644 --- a/crates/services/executor/src/contract_state.rs +++ b/crates/services/executor/src/contract_state.rs @@ -32,19 +32,23 @@ use sha2::{ /// The hash is not dependent on the order of reads or writes. /// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution pub fn compute_balances_hash( + contract_id: &ContractId, record: &[StorageReadReplayEvent], changes: &Changes, ) -> Bytes32 { - let mut touched_assets: BTreeMap> = - BTreeMap::new(); + let mut touched_assets: BTreeMap = BTreeMap::new(); for r in record { if r.column == Column::ContractsAssets as u32 { let key = ContractsAssetKey::from_slice(&r.key).unwrap(); - let contract_id = key.contract_id(); - let asset_id = key.asset_id(); + let r_contract_id = key.contract_id(); + let r_asset_id = key.asset_id(); - touched_assets.entry(*contract_id).or_default().insert( - *asset_id, + if r_contract_id != contract_id { + continue; + } + + touched_assets.insert( + *r_asset_id, r.value .clone() .map(|v| { @@ -61,35 +65,32 @@ pub fn compute_balances_hash( if *change_column == Column::ContractsAssets as u32 { for (key, value) in change.iter() { let key = ContractsStateKey::from_slice(key).unwrap(); - let contract_id = key.contract_id(); - let state_key = key.state_key(); - - touched_assets - .get_mut(contract_id) - .expect("Column cannot have been changed if it was not accessed") - .insert( - AssetId::from(**state_key), - match value { - WriteOperation::Insert(v) => { - let mut buf = [0; 8]; - buf.copy_from_slice(v); - Word::from_be_bytes(buf) - } - WriteOperation::Remove => 0, - }, - ); + let c_contract_id = key.contract_id(); + let c_state_key = key.state_key(); + + if c_contract_id != contract_id { + continue; + } + + touched_assets.insert( + AssetId::from(**c_state_key), + match value { + WriteOperation::Insert(v) => { + let mut buf = [0; 8]; + buf.copy_from_slice(v); + Word::from_be_bytes(buf) + } + WriteOperation::Remove => 0, + }, + ); } } } let mut hasher = Sha256::new(); - for (contract_id, values) in touched_assets { - hasher.update(*contract_id); - hasher.update((values.len() as u64).to_be_bytes()); - for (state_key, state_value) in values { - hasher.update(state_key); - hasher.update(state_value.to_be_bytes()); - } + for (state_key, state_value) in touched_assets { + hasher.update(state_key); + hasher.update(state_value.to_be_bytes()); } let digest: [u8; 32] = hasher.finalize().into(); Bytes32::from(digest) @@ -99,21 +100,22 @@ pub fn compute_balances_hash( /// The hash is not dependent on the order of reads or writes. /// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution pub fn compute_state_hash( + contract_id: &ContractId, record: &[StorageReadReplayEvent], changes: &Changes, ) -> Bytes32 { - let mut touched_slots: BTreeMap>>> = - BTreeMap::new(); + let mut touched_slots: BTreeMap>> = BTreeMap::new(); for r in record { if r.column == Column::ContractsState as u32 { let key = ContractsStateKey::from_slice(&r.key).unwrap(); - let contract_id = key.contract_id(); - let state_key = key.state_key(); + let r_contract_id = key.contract_id(); + let r_state_key = key.state_key(); - touched_slots - .entry(*contract_id) - .or_default() - .insert(*state_key, r.value.clone()); + if r_contract_id != contract_id { + continue; + } + + touched_slots.insert(*r_state_key, r.value.clone()); } } @@ -121,36 +123,33 @@ pub fn compute_state_hash( if *change_column == Column::ContractsState as u32 { for (key, value) in change.iter() { let key = ContractsStateKey::from_slice(key).unwrap(); - let contract_id = key.contract_id(); - let state_key = key.state_key(); - - touched_slots - .get_mut(contract_id) - .expect("Column cannot have been changed if it was not accessed") - .insert( - *state_key, - match value { - WriteOperation::Insert(v) => Some(v.to_vec()), - WriteOperation::Remove => None, - }, - ); + let c_contract_id = key.contract_id(); + let c_state_key = key.state_key(); + + if c_contract_id != contract_id { + continue; + } + + touched_slots.insert( + *c_state_key, + match value { + WriteOperation::Insert(v) => Some(v.to_vec()), + WriteOperation::Remove => None, + }, + ); } } } let mut hasher = Sha256::new(); - for (contract_id, values) in touched_slots { - hasher.update(*contract_id); - hasher.update((values.len() as u64).to_be_bytes()); - for (state_key, state_value) in values { - hasher.update(state_key); - if let Some(value) = state_value { - hasher.update([1u8]); - hasher.update((value.len() as Word).to_be_bytes()); - hasher.update(&value); - } else { - hasher.update([0u8]); - } + for (state_key, state_value) in touched_slots { + hasher.update(state_key); + if let Some(value) = state_value { + hasher.update([1u8]); + hasher.update((value.len() as Word).to_be_bytes()); + hasher.update(&value); + } else { + hasher.update([0u8]); } } let digest: [u8; 32] = hasher.finalize().into(); diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 1a3450ad5cd..0d0435afdf4 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1635,6 +1635,8 @@ where outputs.as_slice(), )?; self.compute_state_of_not_utxo_outputs( + *coinbase_id, + core::slice::from_ref(input), outputs.as_mut_slice(), &record, &changes, @@ -1654,6 +1656,7 @@ where fn update_tx_outputs( &self, + tx_id: TxId, tx: &mut Tx, record: &[StorageReadReplayEvent], changes: &Changes, @@ -1662,7 +1665,13 @@ where Tx: ExecutableTransaction, { let mut outputs = core::mem::take(tx.outputs_mut()); - self.compute_state_of_not_utxo_outputs(&mut outputs, record, changes)?; + self.compute_state_of_not_utxo_outputs( + tx_id, + tx.inputs(), + &mut outputs, + record, + changes, + )?; *tx.outputs_mut() = outputs; Ok(()) } @@ -1954,7 +1963,7 @@ where storage_tx.commit_changes(changes.clone())?; } - self.update_tx_outputs(&mut tx, &record, &changes)?; + self.update_tx_outputs(tx_id, &mut tx, &record, &changes)?; Ok((reverted, state, tx, receipts.to_vec())) } @@ -2193,8 +2202,10 @@ where *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); - *balance_root = compute_balances_hash(record, &Changes::default()); - *state_root = compute_state_hash(record, &Changes::default()); + *balance_root = + compute_balances_hash(contract_id, record, &Changes::default()); + *state_root = + compute_state_hash(contract_id, record, &Changes::default()); } _ => {} } @@ -2209,14 +2220,31 @@ where /// In validation mode, compares the outputs with computed inputs. fn compute_state_of_not_utxo_outputs( &self, + tx_id: TxId, + inputs: &[Input], outputs: &mut [Output], record: &[StorageReadReplayEvent], changes: &Changes, ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { - contract_output.balance_root = compute_balances_hash(record, changes); - contract_output.state_root = compute_state_hash(record, changes); + let contract_id = + if let Some(Input::Contract(input::contract::Contract { + contract_id, + .. + })) = inputs.get(contract_output.input_index as usize) + { + contract_id + } else { + return Err(ExecutorError::InvalidTransactionOutcome { + transaction_id: tx_id, + }) + }; + + contract_output.balance_root = + compute_balances_hash(contract_id, record, changes); + contract_output.state_root = + compute_state_hash(contract_id, record, changes); } } Ok(()) From 486612d70d7df944642b8fa9ac3d3c444bf37879 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 24 Sep 2025 15:57:40 +0300 Subject: [PATCH 07/22] Rewrite using lower-level storage traits --- .../src/service/adapters/graphql_api.rs | 11 +- .../services/executor/src/contract_state.rs | 123 +-- crates/services/executor/src/executor.rs | 89 +- .../executor/src/storage_access_recorder.rs | 982 +++++++++++++++++- crates/storage/src/blueprint.rs | 21 +- crates/storage/src/blueprint/merklized.rs | 14 +- crates/storage/src/blueprint/plain.rs | 19 +- crates/storage/src/blueprint/sparse.rs | 31 +- crates/storage/src/iter.rs | 39 +- crates/storage/src/merkle/sparse.rs | 19 +- crates/storage/src/structured_storage.rs | 11 +- 11 files changed, 1102 insertions(+), 257 deletions(-) diff --git a/crates/fuel-core/src/service/adapters/graphql_api.rs b/crates/fuel-core/src/service/adapters/graphql_api.rs index a99009232b5..52fc3c55e47 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api.rs @@ -9,11 +9,7 @@ use super::{ import_result_provider, }; use crate::{ - database::{ - Database, - OnChainIterableKeyValueView, - database_description::compression::CompressionDatabase, - }, + database::OnChainIterableKeyValueView, fuel_core_graphql_api::ports::{ BlockProducerPort, ChainStateProvider, @@ -45,7 +41,7 @@ use fuel_core_compression_service::storage::CompressedBlocks; use fuel_core_services::stream::BoxStream; use fuel_core_storage::{ Result as StorageResult, - blueprint::BlueprintInspect, + blueprint::BlueprintCodec, kv_store::KeyValueInspect, not_found, structured_storage::TableWithBlueprint, @@ -303,9 +299,8 @@ impl DatabaseDaCompressedBlocks for CompressionServiceAdapter { use fuel_core_storage::codec::Encode; let encoded_height = - <::Blueprint as BlueprintInspect< + <::Blueprint as BlueprintCodec< CompressedBlocks, - Database, /* in the future it would be nice to use a dummy impl, but it's not worth the effort rn */ >>::KeyCodec::encode(height); let column = ::column(); self.storage() diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs index 5f628fc9dc0..3cd5c5cb8e6 100644 --- a/crates/services/executor/src/contract_state.rs +++ b/crates/services/executor/src/contract_state.rs @@ -7,21 +7,10 @@ use alloc::{ vec::Vec, }; -use fuel_core_storage::{ - ContractsAssetKey, - ContractsStateKey, - column::Column, - kv_store::WriteOperation, - transactional::Changes, -}; -use fuel_core_types::{ - fuel_tx::{ - AssetId, - Bytes32, - ContractId, - Word, - }, - services::executor::StorageReadReplayEvent, +use fuel_core_types::fuel_tx::{ + AssetId, + Bytes32, + Word, }; use sha2::{ Digest, @@ -30,63 +19,7 @@ use sha2::{ /// Computes a hash of all contract balances that were read or modified. /// The hash is not dependent on the order of reads or writes. -/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution -pub fn compute_balances_hash( - contract_id: &ContractId, - record: &[StorageReadReplayEvent], - changes: &Changes, -) -> Bytes32 { - let mut touched_assets: BTreeMap = BTreeMap::new(); - for r in record { - if r.column == Column::ContractsAssets as u32 { - let key = ContractsAssetKey::from_slice(&r.key).unwrap(); - let r_contract_id = key.contract_id(); - let r_asset_id = key.asset_id(); - - if r_contract_id != contract_id { - continue; - } - - touched_assets.insert( - *r_asset_id, - r.value - .clone() - .map(|v| { - let mut buf = [0; 8]; - buf.copy_from_slice(v.as_slice()); - Word::from_be_bytes(buf) - }) - .unwrap_or(0), - ); - } - } - - for (change_column, change) in changes { - if *change_column == Column::ContractsAssets as u32 { - for (key, value) in change.iter() { - let key = ContractsStateKey::from_slice(key).unwrap(); - let c_contract_id = key.contract_id(); - let c_state_key = key.state_key(); - - if c_contract_id != contract_id { - continue; - } - - touched_assets.insert( - AssetId::from(**c_state_key), - match value { - WriteOperation::Insert(v) => { - let mut buf = [0; 8]; - buf.copy_from_slice(v); - Word::from_be_bytes(buf) - } - WriteOperation::Remove => 0, - }, - ); - } - } - } - +pub fn compute_balances_hash(touched_assets: BTreeMap) -> Bytes32 { let mut hasher = Sha256::new(); for (state_key, state_value) in touched_assets { hasher.update(state_key); @@ -98,56 +31,14 @@ pub fn compute_balances_hash( /// Computes a hash of all contract state slots that were read or modified. /// The hash is not dependent on the order of reads or writes. -/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution -pub fn compute_state_hash( - contract_id: &ContractId, - record: &[StorageReadReplayEvent], - changes: &Changes, -) -> Bytes32 { - let mut touched_slots: BTreeMap>> = BTreeMap::new(); - for r in record { - if r.column == Column::ContractsState as u32 { - let key = ContractsStateKey::from_slice(&r.key).unwrap(); - let r_contract_id = key.contract_id(); - let r_state_key = key.state_key(); - - if r_contract_id != contract_id { - continue; - } - - touched_slots.insert(*r_state_key, r.value.clone()); - } - } - - for (change_column, change) in changes { - if *change_column == Column::ContractsState as u32 { - for (key, value) in change.iter() { - let key = ContractsStateKey::from_slice(key).unwrap(); - let c_contract_id = key.contract_id(); - let c_state_key = key.state_key(); - - if c_contract_id != contract_id { - continue; - } - - touched_slots.insert( - *c_state_key, - match value { - WriteOperation::Insert(v) => Some(v.to_vec()), - WriteOperation::Remove => None, - }, - ); - } - } - } - +pub fn compute_state_hash(touched_slots: BTreeMap>>) -> Bytes32 { let mut hasher = Sha256::new(); for (state_key, state_value) in touched_slots { hasher.update(state_key); if let Some(value) = state_value { hasher.update([1u8]); hasher.update((value.len() as Word).to_be_bytes()); - hasher.update(&value); + hasher.update(value); } else { hasher.update([0u8]); } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index b0eabeab0c6..284bdb9d525 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -11,7 +11,11 @@ use crate::{ TransactionsSource, }, refs::ContractRef, - storage_access_recorder::StorageAccessRecorder, + storage_access_recorder::{ + AccessedPerContract, + ContractAccesses, + StorageAccessRecorder, + }, }; use fuel_core_storage::{ StorageAsMut, @@ -141,7 +145,6 @@ use fuel_core_types::{ ExecutionResult, ForcedTransactionFailure, Result as ExecutorResult, - StorageReadReplayEvent, TransactionExecutionResult, TransactionExecutionStatus, TransactionValidityError, @@ -510,7 +513,8 @@ where } type BlockStorageTransaction = StorageTransaction; -type TxStorageTransaction<'a, T> = StorageTransaction<&'a mut BlockStorageTransaction>; +pub(crate) type TxStorageTransaction<'a, T> = + StorageTransaction<&'a mut BlockStorageTransaction>; #[derive(Clone, Debug)] pub struct BlockExecutor { @@ -1425,7 +1429,11 @@ where )?; } - self.compute_inputs(core::slice::from_mut(&mut input), storage_tx, &[])?; + self.compute_inputs( + core::slice::from_mut(&mut input), + storage_tx, + &AccessedPerContract::default(), + )?; let (input, output) = self.execute_mint_with_vm( header, @@ -1595,14 +1603,14 @@ where where T: KeyValueInspect, { - let mut storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); - - let mut sub_block_db_commit = storage_tx_record + let sub_block_db_commit = storage_tx .write_transaction() .with_policy(ConflictPolicy::Overwrite); + let mut storage_tx_record = StorageAccessRecorder::new(sub_block_db_commit); + let mut vm_db = VmStorage::new( - &mut sub_block_db_commit, + &mut storage_tx_record, &header.consensus, &header.application, coinbase_contract_id, @@ -1617,8 +1625,11 @@ where .map_err(|e| format!("{e}")) .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; - let (recorder, changes) = sub_block_db_commit.into_inner(); - let record = core::mem::take(&mut *recorder.record.lock()); + let StorageAccessRecorder { + storage: sub_block_db_commit, + record, + } = storage_tx_record; + let changes = sub_block_db_commit.into_changes(); storage_tx.commit_changes(changes.clone())?; @@ -1637,8 +1648,7 @@ where *coinbase_id, core::slice::from_ref(input), outputs.as_mut_slice(), - &record, - &changes, + &record.lock().clone(), )?; let Input::Contract(input) = core::mem::take(input) else { return Err(ExecutorError::Other( @@ -1657,20 +1667,13 @@ where &self, tx_id: TxId, tx: &mut Tx, - record: &[StorageReadReplayEvent], - changes: &Changes, + record: &AccessedPerContract, ) -> ExecutorResult<()> where Tx: ExecutableTransaction, { let mut outputs = core::mem::take(tx.outputs_mut()); - self.compute_state_of_not_utxo_outputs( - tx_id, - tx.inputs(), - &mut outputs, - record, - changes, - )?; + self.compute_state_of_not_utxo_outputs(tx_id, tx.inputs(), &mut outputs, record)?; *tx.outputs_mut() = outputs; Ok(()) } @@ -1846,14 +1849,14 @@ where { let tx_id = checked_tx.id(); - let storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); - - let mut sub_block_db_commit = storage_tx_record + let sub_block_db_commit = storage_tx .read_transaction() .with_policy(ConflictPolicy::Overwrite); + let mut storage_tx_record = StorageAccessRecorder::new(sub_block_db_commit); + let vm_db = VmStorage::new( - &mut sub_block_db_commit, + &mut storage_tx_record, &header.consensus, &header.application, coinbase_contract_id, @@ -1965,13 +1968,11 @@ where Self::update_input_used_gas(predicate_gas_used, tx_id, &mut tx)?; - let ( - StorageAccessRecorder { - storage: storage_tx_recovered, - record, - }, - changes, - ) = sub_block_db_commit.into_inner(); + let StorageAccessRecorder { + storage: storage_tx_recovered, + record, + } = storage_tx_record; + let (storage_tx_recovered, changes) = storage_tx_recovered.into_inner(); let record = record.lock().clone(); // We always need to update inputs with storage state before execution, @@ -1983,7 +1984,7 @@ where storage_tx.commit_changes(changes.clone())?; } - self.update_tx_outputs(tx_id, &mut tx, &record, &changes)?; + self.update_tx_outputs(tx_id, &mut tx, &record)?; Ok((reverted, state, tx, receipts.to_vec())) } @@ -2181,7 +2182,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, - record: &[StorageReadReplayEvent], + record: &AccessedPerContract, ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2222,10 +2223,11 @@ where *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); - *balance_root = - compute_balances_hash(contract_id, record, &Changes::default()); - *state_root = - compute_state_hash(contract_id, record, &Changes::default()); + let empty = ContractAccesses::default(); + let accessed = record.per_contract.get(contract_id).unwrap_or(&empty); + + *balance_root = compute_balances_hash(accessed.assets_initial()); + *state_root = compute_state_hash(accessed.slots_initial()); } _ => {} } @@ -2243,8 +2245,7 @@ where tx_id: TxId, inputs: &[Input], outputs: &mut [Output], - record: &[StorageReadReplayEvent], - changes: &Changes, + record: &AccessedPerContract, ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { @@ -2261,10 +2262,12 @@ where }) }; + let empty = ContractAccesses::default(); + let accessed = record.per_contract.get(contract_id).unwrap_or(&empty); + contract_output.balance_root = - compute_balances_hash(contract_id, record, changes); - contract_output.state_root = - compute_state_hash(contract_id, record, changes); + compute_balances_hash(accessed.assets_final()); + contract_output.state_root = compute_state_hash(accessed.slots_final()); } } Ok(()) diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 55e397914fa..7e267a1eea6 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -1,56 +1,986 @@ +#![allow(unused)] + use fuel_core_storage::{ - Result as StorageResult, - kv_store::{ - KeyValueInspect, - StorageColumn, - Value, + ContractsAssetKey, + ContractsStateKey, + Error, + Mappable, + StorageBatchMutate, + StorageInspect, + StorageMutate, + StorageRead, + StorageSize, + StorageWrite, + blueprint::{ + BlueprintCodec, + BlueprintInspect, + }, + codec::{ + Decode, + Encode, + Encoder, + primitive::Primitive, + raw::Raw, + }, + column::Column, + merkle::sparse::KeyConverter, + structured_storage::TableWithBlueprint, + tables::{ + ConsensusParametersVersions, + ContractsAssets, + ContractsRawCode, + ContractsState, + FuelBlocks, + StateTransitionBytecodeVersions, + UploadedBytecodes, + merkle::{ + ContractsAssetsMerkleData, + ContractsAssetsMerkleMetadata, + }, }, }; -use fuel_core_types::services::executor::StorageReadReplayEvent; +use fuel_core_types::{ + fuel_tx::{ + AssetId, + Bytes32, + ContractId, + Word, + }, + fuel_types::bytes, + fuel_vm::BlobData, +}; use parking_lot::Mutex; #[cfg(feature = "std")] -use std::sync::Arc; +use std::{ + borrow::Cow, + borrow::ToOwned, + collections::BTreeMap, + sync::Arc, +}; #[cfg(not(feature = "std"))] use alloc::{ + borrow::Cow, + borrow::ToOwned, + collections::BTreeMap, sync::Arc, vec::Vec, }; -pub struct StorageAccessRecorder -where - S: KeyValueInspect, -{ +/// Value state before and after execution +#[derive(Debug, Clone)] +pub(crate) struct ValueHistory { + /// First observed value + pub first: Option, + /// Last observed value + pub last: Option, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct ContractAccesses { + assets: BTreeMap>, + slots: BTreeMap>>, +} +impl ContractAccesses { + pub fn assets_initial(&self) -> BTreeMap { + self.assets + .iter() + .map(|(k, v)| (*k, v.first.unwrap_or(0))) + .collect() + } + + pub fn assets_final(&self) -> BTreeMap { + self.assets + .iter() + .map(|(k, v)| (*k, v.last.unwrap_or(0))) + .collect() + } + + pub fn slots_initial(&self) -> BTreeMap>> { + self.slots + .iter() + .map(|(k, v)| (*k, v.first.as_ref())) + .collect() + } + + pub fn slots_final(&self) -> BTreeMap>> { + self.slots + .iter() + .map(|(k, v)| (*k, v.last.as_ref())) + .collect() + } +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct AccessedPerContract { + pub per_contract: BTreeMap, +} + +pub(crate) struct StorageAccessRecorder { pub storage: S, - pub record: Arc>>, + pub record: Arc>, } -impl StorageAccessRecorder -where - S: KeyValueInspect, -{ +impl StorageAccessRecorder { pub fn new(storage: S) -> Self { Self { storage, record: Default::default(), } } + + fn read_asset(&self, key: &ContractsAssetKey, value: Option) { + let contract_id = key.contract_id(); + let asset_id = key.asset_id(); + + self.record + .lock() + .per_contract + .entry(*contract_id) + .or_default() + .assets + .entry(*asset_id) + .or_insert(ValueHistory { + first: value, + last: value, + }); + } + + fn write_asset(&self, key: &ContractsAssetKey, value: Word) { + let contract_id = key.contract_id(); + let asset_id = key.asset_id(); + + self.record + .lock() + .per_contract + .entry(*contract_id) + .or_default() + .assets + .entry(*asset_id) + .and_modify(|e| e.last = Some(value)); + } + + fn remove_asset(&self, key: &ContractsAssetKey) { + let contract_id = key.contract_id(); + let asset_id = key.asset_id(); + + self.record + .lock() + .per_contract + .entry(*contract_id) + .or_default() + .assets + .entry(*asset_id) + .and_modify(|e| e.last = None); + } + + fn read_state( + &self, + key: &ContractsStateKey, + value: Option<&::OwnedValue>, + ) { + let contract_id = key.contract_id(); + let state_key = key.state_key(); + + self.record + .lock() + .per_contract + .entry(*contract_id) + .or_default() + .slots + .entry(*state_key) + .or_insert(ValueHistory { + first: value.map(|v| v.0.clone()), + last: value.map(|v| v.0.clone()), + }); + } + + fn write_state( + &self, + key: &ContractsStateKey, + value: ::OwnedValue, + ) { + let contract_id = key.contract_id(); + let state_key = key.state_key(); + + self.record + .lock() + .per_contract + .entry(*contract_id) + .or_default() + .slots + .entry(*state_key) + .and_modify(|e| e.last = Some(value.to_owned().into())); + } + + fn remove_state(&self, key: &ContractsStateKey) { + let contract_id = key.contract_id(); + let state_key = key.state_key(); + + self.record + .lock() + .per_contract + .entry(*contract_id) + .or_default() + .slots + .entry(*state_key) + .and_modify(|e| e.last = None); + } +} + +macro_rules! storage_passthrough { + ($column:ident) => { + impl StorageInspect<$column> for StorageAccessRecorder + where + S: StorageInspect<$column, Error = Error>, + { + type Error = S::Error; + + fn get( + &self, + key: &<$column as Mappable>::Key, + ) -> Result::OwnedValue>>, Self::Error> { + <_ as StorageInspect<$column>>::get(&self.storage, key) + } + + fn contains_key( + &self, + key: &<$column as Mappable>::Key, + ) -> Result { + <_ as StorageInspect<$column>>::contains_key(&self.storage, key) + } + } + + impl StorageSize<$column> for StorageAccessRecorder + where + S: StorageSize<$column> + StorageInspect<$column, Error = Error>, + { + fn size_of_value( + &self, + key: &<$column as Mappable>::Key, + ) -> Result, Error> { + <_ as StorageSize<$column>>::size_of_value(&self.storage, key) + } + } + + impl StorageMutate<$column> for StorageAccessRecorder + where + S: StorageInspect<$column, Error = Error> + + StorageMutate<$column, Error = Error>, + { + fn insert( + &mut self, + key: &<$column as Mappable>::Key, + value: &<$column as Mappable>::Value, + ) -> Result<(), Self::Error> { + <_ as StorageMutate<$column>>::insert(&mut self.storage, key, value) + } + + fn replace( + &mut self, + key: &<$column as Mappable>::Key, + value: &<$column as Mappable>::Value, + ) -> Result::OwnedValue>, Self::Error> { + <_ as StorageMutate<$column>>::replace(&mut self.storage, key, value) + } + + fn remove( + &mut self, + key: &<$column as Mappable>::Key, + ) -> Result<(), Self::Error> { + <_ as StorageMutate<$column>>::remove(&mut self.storage, key) + } + + fn take( + &mut self, + key: &<$column as Mappable>::Key, + ) -> Result::OwnedValue>, Self::Error> { + <_ as StorageMutate<$column>>::take(&mut self.storage, key) + } + } + + impl StorageRead<$column> for StorageAccessRecorder + where + S: StorageRead<$column, Error = Error>, + $column: Mappable + TableWithBlueprint, + { + fn read( + &self, + key: &<$column as Mappable>::Key, + offset: usize, + buf: &mut [u8], + ) -> Result { + <_ as StorageRead<$column>>::read(&self.storage, key, offset, buf) + } + + fn read_alloc( + &self, + key: &<$column as Mappable>::Key, + ) -> Result>, Self::Error> { + <_ as StorageRead<$column>>::read_alloc(&self.storage, key) + } + } + + impl StorageWrite<$column> for StorageAccessRecorder + where + S: StorageWrite<$column, Error = Error>, + $column: Mappable + TableWithBlueprint, + { + fn write_bytes( + &mut self, + key: &<$column as Mappable>::Key, + buf: &[u8], + ) -> Result<(), Self::Error> { + <_ as StorageWrite<$column>>::write_bytes(&mut self.storage, key, buf) + } + + fn replace_bytes( + &mut self, + key: &<$column as Mappable>::Key, + buf: &[u8], + ) -> Result>, Self::Error> { + <_ as StorageWrite<$column>>::replace_bytes(&mut self.storage, key, buf) + } + + fn take_bytes( + &mut self, + key: &<$column as Mappable>::Key, + ) -> Result>, Self::Error> { + <_ as StorageWrite<$column>>::take_bytes(&mut self.storage, key) + } + } + + impl StorageBatchMutate<$column> for StorageAccessRecorder + where + S: StorageBatchMutate<$column, Error = Error>, + $column: Mappable + TableWithBlueprint, + { + fn init_storage<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + + Iterator< + Item = ( + &'a <$column as Mappable>::Key, + &'a <$column as Mappable>::Value, + ), + >, + <$column as Mappable>::Key: 'a, + <$column as Mappable>::Value: 'a, + { + <_ as StorageBatchMutate<$column>>::init_storage(&mut self.storage, set) + } + + fn insert_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + + Iterator< + Item = ( + &'a <$column as Mappable>::Key, + &'a <$column as Mappable>::Value, + ), + >, + <$column as Mappable>::Key: 'a, + <$column as Mappable>::Value: 'a, + { + <_ as StorageBatchMutate<$column>>::insert_batch(&mut self.storage, set) + } + + fn remove_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + Iterator::Key>, + <$column as Mappable>::Key: 'a, + { + <_ as StorageBatchMutate<$column>>::remove_batch(&mut self.storage, set) + } + } + }; +} + +storage_passthrough!(BlobData); +storage_passthrough!(ConsensusParametersVersions); +storage_passthrough!(ContractsRawCode); +storage_passthrough!(FuelBlocks); +storage_passthrough!(StateTransitionBytecodeVersions); +storage_passthrough!(UploadedBytecodes); + +// Impl for ContractsAssets + +impl StorageInspect for StorageAccessRecorder +where + S: StorageInspect, +{ + type Error = S::Error; + + fn get( + &self, + key: &::Key, + ) -> Result::OwnedValue>>, Self::Error> { + let value = <_ as StorageInspect>::get(&self.storage, key)?; + self.read_asset(key, value.as_ref().map(|v| v.clone().into_owned())); + Ok(value) + } + + fn contains_key( + &self, + key: &::Key, + ) -> Result { + <_ as StorageInspect>::contains_key(&self.storage, key) + } +} + +impl StorageSize for StorageAccessRecorder +where + S: StorageSize + StorageInspect, +{ + fn size_of_value( + &self, + key: &::Key, + ) -> Result, Error> { + <_ as StorageSize>::size_of_value(&self.storage, key) + } +} + +impl StorageMutate for StorageAccessRecorder +where + S: StorageInspect + + StorageMutate, +{ + fn insert( + &mut self, + key: &::Key, + value: &::Value, + ) -> Result<(), Self::Error> { + <_ as StorageMutate>::insert(&mut self.storage, key, value)?; + self.write_asset(key, *value); + Ok(()) + } + + fn replace( + &mut self, + key: &::Key, + value: &::Value, + ) -> Result::OwnedValue>, Self::Error> { + let old_value = <_ as StorageMutate>::replace( + &mut self.storage, + key, + value, + )?; + self.read_asset(key, old_value); + self.write_asset(key, *value); + Ok(old_value) + } + + fn remove( + &mut self, + key: &::Key, + ) -> Result<(), Self::Error> { + <_ as StorageMutate>::remove(&mut self.storage, key)?; + self.remove_asset(key); + Ok(()) + } + + fn take( + &mut self, + key: &::Key, + ) -> Result::OwnedValue>, Self::Error> { + let value = <_ as StorageMutate>::take(&mut self.storage, key)?; + self.read_asset(key, value); + self.remove_asset(key); + Ok(value) + } +} + +impl StorageRead for StorageAccessRecorder +where + S: StorageRead, + ContractsAssets: Mappable + TableWithBlueprint, +{ + fn read( + &self, + key: &::Key, + offset: usize, + buf: &mut [u8], + ) -> Result { + // We need to get the whole value to record the access + let value = >::read_alloc(&self.storage, key)?; + + let value_dec = value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsAssets, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_asset(key, value_dec); + + let Some(bytes) = value else { + return Ok(false); + }; + + let bytes_len = bytes.len(); + let start = offset; + let end = offset.saturating_add(buf.len()); + + if end > bytes_len { + return Err(Error::Other(anyhow::anyhow!( + "Read out of bounds: value length is {bytes_len}, read range is {start}..{end}" + ))); + } + + let starting_from_offset = &bytes[start..end]; + buf[..].copy_from_slice(starting_from_offset); + Ok(true) + } + + fn read_alloc( + &self, + key: &::Key, + ) -> Result>, Self::Error> { + let value = >::read_alloc(&self.storage, key)?; + + let value_dec = value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsAssets, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_asset(key, value_dec); + Ok(value) + } +} + +impl StorageWrite for StorageAccessRecorder +where + S: StorageWrite, + ContractsAssets: Mappable + TableWithBlueprint, +{ + fn write_bytes( + &mut self, + key: &::Key, + buf: &[u8], + ) -> Result<(), Self::Error> { + let new_value = + <::Blueprint as BlueprintCodec< + ContractsAssets, + >>::ValueCodec::decode(buf)?; + + <_ as StorageWrite>::write_bytes(&mut self.storage, key, buf)?; + self.write_asset(key, new_value); + Ok(()) + } + + fn replace_bytes( + &mut self, + key: &::Key, + buf: &[u8], + ) -> Result>, Self::Error> { + let new_value = + <::Blueprint as BlueprintCodec< + ContractsAssets, + >>::ValueCodec::decode(buf)?; + + let old_value = <_ as StorageWrite>::replace_bytes( + &mut self.storage, + key, + buf, + )?; + let old_value_dec = old_value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsAssets, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_asset(key, old_value_dec); + self.write_asset(key, new_value); + + Ok(old_value) + } + + fn take_bytes( + &mut self, + key: &::Key, + ) -> Result>, Self::Error> { + let old_value = + <_ as StorageWrite>::take_bytes(&mut self.storage, key)?; + + let old_value_dec = old_value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsAssets, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_asset(key, old_value_dec); + self.remove_asset(key); + + Ok(old_value) + } +} + +impl StorageBatchMutate for StorageAccessRecorder +where + S: StorageBatchMutate, + ContractsAssets: Mappable + TableWithBlueprint, +{ + fn init_storage<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + + Iterator< + Item = ( + &'a ::Key, + &'a ::Value, + ), + >, + ::Key: 'a, + ::Value: 'a, + { + let mut items = Vec::new(); + for (k, v) in set { + self.write_asset(k, *v); + items.push((k, v)); + } + <_ as StorageBatchMutate>::init_storage( + &mut self.storage, + items.into_iter(), + ) + } + + fn insert_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + + Iterator< + Item = ( + &'a ::Key, + &'a ::Value, + ), + >, + ::Key: 'a, + ::Value: 'a, + { + let mut items = Vec::new(); + for (k, v) in set { + self.write_asset(k, *v); + items.push((k, v)); + } + <_ as StorageBatchMutate>::insert_batch( + &mut self.storage, + items.into_iter(), + ) + } + + fn remove_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + Iterator::Key>, + ::Key: 'a, + { + let mut items = Vec::new(); + for k in set { + self.remove_asset(k); + items.push(k); + } + <_ as StorageBatchMutate>::remove_batch( + &mut self.storage, + items.into_iter(), + ) + } +} + +// Impl for ContractsState +impl StorageInspect for StorageAccessRecorder +where + S: StorageInspect, +{ + type Error = S::Error; + + fn get( + &self, + key: &::Key, + ) -> Result::OwnedValue>>, Self::Error> { + let value = <_ as StorageInspect>::get(&self.storage, key)?; + self.read_state(key, value.as_deref()); + Ok(value) + } + + fn contains_key( + &self, + key: &::Key, + ) -> Result { + <_ as StorageInspect>::contains_key(&self.storage, key) + } +} + +impl StorageSize for StorageAccessRecorder +where + S: StorageSize + StorageInspect, +{ + fn size_of_value( + &self, + key: &::Key, + ) -> Result, Error> { + <_ as StorageSize>::size_of_value(&self.storage, key) + } } -impl KeyValueInspect for StorageAccessRecorder +impl StorageMutate for StorageAccessRecorder where - S: KeyValueInspect, + S: StorageInspect + + StorageMutate, { - type Column = S::Column; + fn insert( + &mut self, + key: &::Key, + value: &::Value, + ) -> Result<(), Self::Error> { + <_ as StorageMutate>::insert(&mut self.storage, key, value)?; + self.write_state(key, (*value).into()); + Ok(()) + } + + fn replace( + &mut self, + key: &::Key, + value: &::Value, + ) -> Result::OwnedValue>, Self::Error> { + let old_value = + <_ as StorageMutate>::replace(&mut self.storage, key, value)?; + self.read_state(key, old_value.as_ref()); + self.write_state(key, value.into()); + Ok(old_value) + } + + fn remove( + &mut self, + key: &::Key, + ) -> Result<(), Self::Error> { + <_ as StorageMutate>::remove(&mut self.storage, key)?; + self.remove_state(key); + Ok(()) + } + + fn take( + &mut self, + key: &::Key, + ) -> Result::OwnedValue>, Self::Error> { + let value = <_ as StorageMutate>::take(&mut self.storage, key)?; + self.read_state(key, value.as_ref()); + self.remove_state(key); + Ok(value) + } +} + +impl StorageRead for StorageAccessRecorder +where + S: StorageRead, + ContractsState: Mappable + TableWithBlueprint, +{ + fn read( + &self, + key: &::Key, + offset: usize, + buf: &mut [u8], + ) -> Result { + // We need to get the whole value to record the access + let value = >::read_alloc(&self.storage, key)?; + + let value_dec = value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsState, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_state(key, value_dec.as_ref()); + + let Some(bytes) = value else { + return Ok(false); + }; + + let bytes_len = bytes.len(); + let start = offset; + let end = offset.saturating_add(buf.len()); + + if end > bytes_len { + return Err(Error::Other(anyhow::anyhow!( + "Read out of bounds: value length is {bytes_len}, read range is {start}..{end}" + ))); + } + + let starting_from_offset = &bytes[start..end]; + buf[..].copy_from_slice(starting_from_offset); + Ok(true) + } + + fn read_alloc( + &self, + key: &::Key, + ) -> Result>, Self::Error> { + let value = >::read_alloc(&self.storage, key)?; + + let value_dec = value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsState, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_state(key, value_dec.as_ref()); - fn get(&self, key: &[u8], column: Self::Column) -> StorageResult> { - let value = self.storage.get(key, column)?; - self.record.lock().push(StorageReadReplayEvent { - column: column.id(), - key: key.to_vec(), - value: value.as_ref().map(|v| v.to_vec()), - }); Ok(value) } } + +impl StorageWrite for StorageAccessRecorder +where + S: StorageWrite, + ContractsState: Mappable + TableWithBlueprint, +{ + fn write_bytes( + &mut self, + key: &::Key, + buf: &[u8], + ) -> Result<(), Self::Error> { + let new_value = + <::Blueprint as BlueprintCodec< + ContractsState, + >>::ValueCodec::decode(buf)?; + + <_ as StorageWrite>::write_bytes(&mut self.storage, key, buf)?; + self.write_state(key, new_value); + Ok(()) + } + + fn replace_bytes( + &mut self, + key: &::Key, + buf: &[u8], + ) -> Result>, Self::Error> { + let new_value = + <::Blueprint as BlueprintCodec< + ContractsState, + >>::ValueCodec::decode(buf)?; + + let old_value = <_ as StorageWrite>::replace_bytes( + &mut self.storage, + key, + buf, + )?; + let old_value_dec = old_value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsState, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_state(key, old_value_dec.as_ref()); + self.write_state(key, new_value); + + Ok(old_value) + } + + fn take_bytes( + &mut self, + key: &::Key, + ) -> Result>, Self::Error> { + let old_value = + <_ as StorageWrite>::take_bytes(&mut self.storage, key)?; + + let old_value_dec = old_value + .as_ref() + .map(|v| { + <::Blueprint as BlueprintCodec< + ContractsState, + >>::ValueCodec::decode(v) + }) + .transpose()?; + + self.read_state(key, old_value_dec.as_ref()); + self.remove_state(key); + + Ok(old_value) + } +} + +impl StorageBatchMutate for StorageAccessRecorder +where + S: StorageBatchMutate, + ContractsState: Mappable + TableWithBlueprint, +{ + fn init_storage<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + + Iterator< + Item = ( + &'a ::Key, + &'a ::Value, + ), + >, + ::Key: 'a, + ::Value: 'a, + { + let mut items = Vec::new(); + for (k, v) in set { + self.write_state(k, (*v).into()); + items.push((k, v)); + } + <_ as StorageBatchMutate>::init_storage( + &mut self.storage, + items.into_iter(), + ) + } + + fn insert_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + + Iterator< + Item = ( + &'a ::Key, + &'a ::Value, + ), + >, + ::Key: 'a, + ::Value: 'a, + { + let mut items = Vec::new(); + for (k, v) in set { + self.write_state(k, (*v).into()); + items.push((k, v)); + } + <_ as StorageBatchMutate>::insert_batch( + &mut self.storage, + items.into_iter(), + ) + } + + fn remove_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> + where + Iter: 'a + Iterator::Key>, + ::Key: 'a, + { + let mut items = Vec::new(); + for k in set { + self.remove_state(k); + items.push(k); + } + <_ as StorageBatchMutate>::remove_batch( + &mut self.storage, + items.into_iter(), + ) + } +} diff --git a/crates/storage/src/blueprint.rs b/crates/storage/src/blueprint.rs index bbce620a0e2..908593dce26 100644 --- a/crates/storage/src/blueprint.rs +++ b/crates/storage/src/blueprint.rs @@ -23,6 +23,18 @@ pub mod merklized; pub mod plain; pub mod sparse; +/// Describes how to encode/decode the key and value for the given mappable table. +/// It is used by the blueprint to perform the actual encoding/decoding. +pub trait BlueprintCodec +where + M: Mappable, +{ + /// The codec used to encode and decode storage key. + type KeyCodec: Encode + Decode; + /// The codec used to encode and decode storage value. + type ValueCodec: Encode + Decode; +} + /// This trait allows defining the agnostic implementation for all storage /// traits(`StorageInspect,` `StorageMutate,` etc) while the main logic is /// hidden inside the blueprint. It allows quickly adding support for new @@ -30,18 +42,13 @@ pub mod sparse; /// infrastructure in other places. It allows changing the blueprint on the /// fly in the definition of the table without affecting other areas of the codebase. /// -/// The blueprint is responsible for encoding/decoding(usually it is done via `KeyCodec` and `ValueCodec`) +/// The blueprint is responsible for encoding/decoding (usually it is done via `KeyCodec` and `ValueCodec`) /// the key and value and putting/extracting it to/from the storage. -pub trait BlueprintInspect +pub trait BlueprintInspect: BlueprintCodec where M: Mappable, S: KeyValueInspect, { - /// The codec used to encode and decode storage key. - type KeyCodec: Encode + Decode; - /// The codec used to encode and decode storage value. - type ValueCodec: Encode + Decode; - /// Checks if the value exists in the storage. fn exists(storage: &S, key: &M::Key, column: S::Column) -> StorageResult { let key_encoder = Self::KeyCodec::encode(key); diff --git a/crates/storage/src/blueprint/merklized.rs b/crates/storage/src/blueprint/merklized.rs index f504e954d6d..1eedb3bc660 100644 --- a/crates/storage/src/blueprint/merklized.rs +++ b/crates/storage/src/blueprint/merklized.rs @@ -11,6 +11,7 @@ use crate::{ StorageInspect, StorageMutate, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -122,11 +123,10 @@ where } } -impl BlueprintInspect +impl BlueprintCodec for Merklized where M: Mappable, - S: KeyValueInspect, KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { @@ -134,6 +134,16 @@ where type ValueCodec = ValueCodec; } +impl BlueprintInspect + for Merklized +where + M: Mappable, + S: KeyValueInspect, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, +{ +} + impl BlueprintMutate for Merklized where diff --git a/crates/storage/src/blueprint/plain.rs b/crates/storage/src/blueprint/plain.rs index fb64fc7fe7a..87883bc5cc9 100644 --- a/crates/storage/src/blueprint/plain.rs +++ b/crates/storage/src/blueprint/plain.rs @@ -8,6 +8,7 @@ use crate::{ Mappable, Result as StorageResult, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -33,10 +34,9 @@ pub struct Plain { _marker: core::marker::PhantomData<(KeyCodec, ValueCodec)>, } -impl BlueprintInspect for Plain +impl BlueprintCodec for Plain where M: Mappable, - S: KeyValueInspect, KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { @@ -44,6 +44,15 @@ where type ValueCodec = ValueCodec; } +impl BlueprintInspect for Plain +where + M: Mappable, + S: KeyValueInspect, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, +{ +} + impl BlueprintMutate for Plain where M: Mappable, @@ -133,10 +142,10 @@ where column, set.map(|(key, value)| { let key_encoder = - >::KeyCodec::encode(key); + >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes().to_vec(); let value = - >::ValueCodec::encode_as_value( + >::ValueCodec::encode_as_value( value, ); (key_bytes, WriteOperation::Insert(value)) @@ -157,7 +166,7 @@ where column, set.map(|key| { let key_encoder = - >::KeyCodec::encode(key); + >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes().to_vec(); (key_bytes, WriteOperation::Remove) }), diff --git a/crates/storage/src/blueprint/sparse.rs b/crates/storage/src/blueprint/sparse.rs index ba09bd38b89..1eefe9f549c 100644 --- a/crates/storage/src/blueprint/sparse.rs +++ b/crates/storage/src/blueprint/sparse.rs @@ -12,6 +12,7 @@ use crate::{ StorageInspect, StorageMutate, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -166,11 +167,10 @@ where } } -impl BlueprintInspect +impl BlueprintCodec for Sparse where M: Mappable, - S: KeyValueInspect, KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { @@ -178,6 +178,16 @@ where type ValueCodec = ValueCodec; } +impl BlueprintInspect + for Sparse +where + M: Mappable, + S: KeyValueInspect, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, +{ +} + impl BlueprintMutate for Sparse where @@ -273,11 +283,6 @@ where } } -type NodeKeyCodec = - <::Blueprint as BlueprintInspect>::KeyCodec; -type NodeValueCodec = - <::Blueprint as BlueprintInspect>::ValueCodec; - impl SupportsBatching for Sparse where @@ -347,10 +352,14 @@ where )?; let nodes = nodes.iter().map(|(key, value)| { - let key = NodeKeyCodec::::encode(key) - .as_bytes() - .into_owned(); - let value = NodeValueCodec::::encode_as_value(value); + let key = <::Blueprint as BlueprintCodec< + Nodes, + >>::KeyCodec::encode(key) + .as_bytes() + .into_owned(); + let value = <::Blueprint as BlueprintCodec< + Nodes, + >>::ValueCodec::encode_as_value(value); (key, WriteOperation::Insert(value)) }); storage.batch_write(Nodes::column(), nodes)?; diff --git a/crates/storage/src/iter.rs b/crates/storage/src/iter.rs index d06419b386f..de3c15b3051 100644 --- a/crates/storage/src/iter.rs +++ b/crates/storage/src/iter.rs @@ -1,7 +1,10 @@ //! The module defines primitives that allow iterating of the storage. use crate::{ - blueprint::BlueprintInspect, + blueprint::{ + BlueprintCodec, + BlueprintInspect, + }, codec::{ Decode, Encode, @@ -164,9 +167,10 @@ where where P: AsRef<[u8]>, { - let encoder = start.map(|start| { - >::KeyCodec::encode(start) - }); + #[allow(clippy::redundant_closure)] + // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 + let encoder = start + .map(|start| >::KeyCodec::encode(start)); let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); @@ -179,10 +183,9 @@ where ) .map(|res| { res.and_then(|key| { - let key = >::KeyCodec::decode( - key.as_slice(), - ) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let key = + >::KeyCodec::decode(key.as_slice()) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; Ok(key) }) }) @@ -198,9 +201,10 @@ where where P: AsRef<[u8]>, { - let encoder = start.map(|start| { - >::KeyCodec::encode(start) - }); + #[allow(clippy::redundant_closure)] + // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 + let encoder = start + .map(|start| >::KeyCodec::encode(start)); let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); @@ -213,15 +217,12 @@ where ) .map(|val| { val.and_then(|(key, value)| { - let key = >::KeyCodec::decode( - key.as_slice(), - ) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let key = + >::KeyCodec::decode(key.as_slice()) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; let value = - >::ValueCodec::decode( - &value, - ) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + >::ValueCodec::decode(&value) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; Ok((key, value)) }) }) diff --git a/crates/storage/src/merkle/sparse.rs b/crates/storage/src/merkle/sparse.rs index c8348466f99..2f11a61ac5e 100644 --- a/crates/storage/src/merkle/sparse.rs +++ b/crates/storage/src/merkle/sparse.rs @@ -4,6 +4,7 @@ use crate::{ Mappable, Result as StorageResult, blueprint::{ + BlueprintCodec, BlueprintInspect, plain::Plain, sparse::{ @@ -114,17 +115,11 @@ where type OwnedValue = ::OwnedValue; } -type KeyCodec = - <
::Blueprint as BlueprintInspect< - Table, - DummyStorage>, - >>::KeyCodec; +type KeyCodec
= + <
::Blueprint as BlueprintCodec
>::KeyCodec; -type ValueCodec = - <
::Blueprint as BlueprintInspect< - Table, - DummyStorage>, - >>::ValueCodec; +type ValueCodec
= + <
::Blueprint as BlueprintCodec
>::ValueCodec; impl TableWithBlueprint for Merkleized
where @@ -134,8 +129,8 @@ where Table::Blueprint: BlueprintInspect>>, { type Blueprint = Sparse< - KeyCodec, - ValueCodec, + KeyCodec
, + ValueCodec
, MerkleMetadata, MerkleData
, KeyConverter
, diff --git a/crates/storage/src/structured_storage.rs b/crates/storage/src/structured_storage.rs index 3ce5e49ff32..cf52bf1b268 100644 --- a/crates/storage/src/structured_storage.rs +++ b/crates/storage/src/structured_storage.rs @@ -14,6 +14,7 @@ use crate::{ StorageSize, StorageWrite, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -362,10 +363,7 @@ where offset: usize, buf: &mut [u8], ) -> Result { - let key_encoder = - >>::KeyCodec::encode( - key, - ); + let key_encoder = >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes(); self.inner.read( key_bytes.as_ref(), @@ -379,10 +377,7 @@ where &self, key: &::Key, ) -> Result>, Self::Error> { - let key_encoder = - >>::KeyCodec::encode( - key, - ); + let key_encoder = >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes(); self.inner .get(key_bytes.as_ref(), ::column()) From d027f691c7510b5515ffc86cb86e6bb4b0d575dd Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 24 Sep 2025 17:03:25 +0300 Subject: [PATCH 08/22] Revert "Rewrite using lower-level storage traits" This reverts commit 486612d70d7df944642b8fa9ac3d3c444bf37879. --- .../src/service/adapters/graphql_api.rs | 11 +- .../services/executor/src/contract_state.rs | 123 ++- crates/services/executor/src/executor.rs | 89 +- .../executor/src/storage_access_recorder.rs | 982 +----------------- crates/storage/src/blueprint.rs | 21 +- crates/storage/src/blueprint/merklized.rs | 14 +- crates/storage/src/blueprint/plain.rs | 19 +- crates/storage/src/blueprint/sparse.rs | 31 +- crates/storage/src/iter.rs | 39 +- crates/storage/src/merkle/sparse.rs | 19 +- crates/storage/src/structured_storage.rs | 11 +- 11 files changed, 257 insertions(+), 1102 deletions(-) diff --git a/crates/fuel-core/src/service/adapters/graphql_api.rs b/crates/fuel-core/src/service/adapters/graphql_api.rs index 52fc3c55e47..a99009232b5 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api.rs @@ -9,7 +9,11 @@ use super::{ import_result_provider, }; use crate::{ - database::OnChainIterableKeyValueView, + database::{ + Database, + OnChainIterableKeyValueView, + database_description::compression::CompressionDatabase, + }, fuel_core_graphql_api::ports::{ BlockProducerPort, ChainStateProvider, @@ -41,7 +45,7 @@ use fuel_core_compression_service::storage::CompressedBlocks; use fuel_core_services::stream::BoxStream; use fuel_core_storage::{ Result as StorageResult, - blueprint::BlueprintCodec, + blueprint::BlueprintInspect, kv_store::KeyValueInspect, not_found, structured_storage::TableWithBlueprint, @@ -299,8 +303,9 @@ impl DatabaseDaCompressedBlocks for CompressionServiceAdapter { use fuel_core_storage::codec::Encode; let encoded_height = - <::Blueprint as BlueprintCodec< + <::Blueprint as BlueprintInspect< CompressedBlocks, + Database, /* in the future it would be nice to use a dummy impl, but it's not worth the effort rn */ >>::KeyCodec::encode(height); let column = ::column(); self.storage() diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs index 3cd5c5cb8e6..5f628fc9dc0 100644 --- a/crates/services/executor/src/contract_state.rs +++ b/crates/services/executor/src/contract_state.rs @@ -7,10 +7,21 @@ use alloc::{ vec::Vec, }; -use fuel_core_types::fuel_tx::{ - AssetId, - Bytes32, - Word, +use fuel_core_storage::{ + ContractsAssetKey, + ContractsStateKey, + column::Column, + kv_store::WriteOperation, + transactional::Changes, +}; +use fuel_core_types::{ + fuel_tx::{ + AssetId, + Bytes32, + ContractId, + Word, + }, + services::executor::StorageReadReplayEvent, }; use sha2::{ Digest, @@ -19,7 +30,63 @@ use sha2::{ /// Computes a hash of all contract balances that were read or modified. /// The hash is not dependent on the order of reads or writes. -pub fn compute_balances_hash(touched_assets: BTreeMap) -> Bytes32 { +/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution +pub fn compute_balances_hash( + contract_id: &ContractId, + record: &[StorageReadReplayEvent], + changes: &Changes, +) -> Bytes32 { + let mut touched_assets: BTreeMap = BTreeMap::new(); + for r in record { + if r.column == Column::ContractsAssets as u32 { + let key = ContractsAssetKey::from_slice(&r.key).unwrap(); + let r_contract_id = key.contract_id(); + let r_asset_id = key.asset_id(); + + if r_contract_id != contract_id { + continue; + } + + touched_assets.insert( + *r_asset_id, + r.value + .clone() + .map(|v| { + let mut buf = [0; 8]; + buf.copy_from_slice(v.as_slice()); + Word::from_be_bytes(buf) + }) + .unwrap_or(0), + ); + } + } + + for (change_column, change) in changes { + if *change_column == Column::ContractsAssets as u32 { + for (key, value) in change.iter() { + let key = ContractsStateKey::from_slice(key).unwrap(); + let c_contract_id = key.contract_id(); + let c_state_key = key.state_key(); + + if c_contract_id != contract_id { + continue; + } + + touched_assets.insert( + AssetId::from(**c_state_key), + match value { + WriteOperation::Insert(v) => { + let mut buf = [0; 8]; + buf.copy_from_slice(v); + Word::from_be_bytes(buf) + } + WriteOperation::Remove => 0, + }, + ); + } + } + } + let mut hasher = Sha256::new(); for (state_key, state_value) in touched_assets { hasher.update(state_key); @@ -31,14 +98,56 @@ pub fn compute_balances_hash(touched_assets: BTreeMap) -> Bytes32 /// Computes a hash of all contract state slots that were read or modified. /// The hash is not dependent on the order of reads or writes. -pub fn compute_state_hash(touched_slots: BTreeMap>>) -> Bytes32 { +/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution +pub fn compute_state_hash( + contract_id: &ContractId, + record: &[StorageReadReplayEvent], + changes: &Changes, +) -> Bytes32 { + let mut touched_slots: BTreeMap>> = BTreeMap::new(); + for r in record { + if r.column == Column::ContractsState as u32 { + let key = ContractsStateKey::from_slice(&r.key).unwrap(); + let r_contract_id = key.contract_id(); + let r_state_key = key.state_key(); + + if r_contract_id != contract_id { + continue; + } + + touched_slots.insert(*r_state_key, r.value.clone()); + } + } + + for (change_column, change) in changes { + if *change_column == Column::ContractsState as u32 { + for (key, value) in change.iter() { + let key = ContractsStateKey::from_slice(key).unwrap(); + let c_contract_id = key.contract_id(); + let c_state_key = key.state_key(); + + if c_contract_id != contract_id { + continue; + } + + touched_slots.insert( + *c_state_key, + match value { + WriteOperation::Insert(v) => Some(v.to_vec()), + WriteOperation::Remove => None, + }, + ); + } + } + } + let mut hasher = Sha256::new(); for (state_key, state_value) in touched_slots { hasher.update(state_key); if let Some(value) = state_value { hasher.update([1u8]); hasher.update((value.len() as Word).to_be_bytes()); - hasher.update(value); + hasher.update(&value); } else { hasher.update([0u8]); } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 284bdb9d525..b0eabeab0c6 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -11,11 +11,7 @@ use crate::{ TransactionsSource, }, refs::ContractRef, - storage_access_recorder::{ - AccessedPerContract, - ContractAccesses, - StorageAccessRecorder, - }, + storage_access_recorder::StorageAccessRecorder, }; use fuel_core_storage::{ StorageAsMut, @@ -145,6 +141,7 @@ use fuel_core_types::{ ExecutionResult, ForcedTransactionFailure, Result as ExecutorResult, + StorageReadReplayEvent, TransactionExecutionResult, TransactionExecutionStatus, TransactionValidityError, @@ -513,8 +510,7 @@ where } type BlockStorageTransaction = StorageTransaction; -pub(crate) type TxStorageTransaction<'a, T> = - StorageTransaction<&'a mut BlockStorageTransaction>; +type TxStorageTransaction<'a, T> = StorageTransaction<&'a mut BlockStorageTransaction>; #[derive(Clone, Debug)] pub struct BlockExecutor { @@ -1429,11 +1425,7 @@ where )?; } - self.compute_inputs( - core::slice::from_mut(&mut input), - storage_tx, - &AccessedPerContract::default(), - )?; + self.compute_inputs(core::slice::from_mut(&mut input), storage_tx, &[])?; let (input, output) = self.execute_mint_with_vm( header, @@ -1603,14 +1595,14 @@ where where T: KeyValueInspect, { - let sub_block_db_commit = storage_tx + let mut storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); + + let mut sub_block_db_commit = storage_tx_record .write_transaction() .with_policy(ConflictPolicy::Overwrite); - let mut storage_tx_record = StorageAccessRecorder::new(sub_block_db_commit); - let mut vm_db = VmStorage::new( - &mut storage_tx_record, + &mut sub_block_db_commit, &header.consensus, &header.application, coinbase_contract_id, @@ -1625,11 +1617,8 @@ where .map_err(|e| format!("{e}")) .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; - let StorageAccessRecorder { - storage: sub_block_db_commit, - record, - } = storage_tx_record; - let changes = sub_block_db_commit.into_changes(); + let (recorder, changes) = sub_block_db_commit.into_inner(); + let record = core::mem::take(&mut *recorder.record.lock()); storage_tx.commit_changes(changes.clone())?; @@ -1648,7 +1637,8 @@ where *coinbase_id, core::slice::from_ref(input), outputs.as_mut_slice(), - &record.lock().clone(), + &record, + &changes, )?; let Input::Contract(input) = core::mem::take(input) else { return Err(ExecutorError::Other( @@ -1667,13 +1657,20 @@ where &self, tx_id: TxId, tx: &mut Tx, - record: &AccessedPerContract, + record: &[StorageReadReplayEvent], + changes: &Changes, ) -> ExecutorResult<()> where Tx: ExecutableTransaction, { let mut outputs = core::mem::take(tx.outputs_mut()); - self.compute_state_of_not_utxo_outputs(tx_id, tx.inputs(), &mut outputs, record)?; + self.compute_state_of_not_utxo_outputs( + tx_id, + tx.inputs(), + &mut outputs, + record, + changes, + )?; *tx.outputs_mut() = outputs; Ok(()) } @@ -1849,14 +1846,14 @@ where { let tx_id = checked_tx.id(); - let sub_block_db_commit = storage_tx + let storage_tx_record = StorageAccessRecorder::new(&mut *storage_tx); + + let mut sub_block_db_commit = storage_tx_record .read_transaction() .with_policy(ConflictPolicy::Overwrite); - let mut storage_tx_record = StorageAccessRecorder::new(sub_block_db_commit); - let vm_db = VmStorage::new( - &mut storage_tx_record, + &mut sub_block_db_commit, &header.consensus, &header.application, coinbase_contract_id, @@ -1968,11 +1965,13 @@ where Self::update_input_used_gas(predicate_gas_used, tx_id, &mut tx)?; - let StorageAccessRecorder { - storage: storage_tx_recovered, - record, - } = storage_tx_record; - let (storage_tx_recovered, changes) = storage_tx_recovered.into_inner(); + let ( + StorageAccessRecorder { + storage: storage_tx_recovered, + record, + }, + changes, + ) = sub_block_db_commit.into_inner(); let record = record.lock().clone(); // We always need to update inputs with storage state before execution, @@ -1984,7 +1983,7 @@ where storage_tx.commit_changes(changes.clone())?; } - self.update_tx_outputs(tx_id, &mut tx, &record)?; + self.update_tx_outputs(tx_id, &mut tx, &record, &changes)?; Ok((reverted, state, tx, receipts.to_vec())) } @@ -2182,7 +2181,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, - record: &AccessedPerContract, + record: &[StorageReadReplayEvent], ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2223,11 +2222,10 @@ where *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); - let empty = ContractAccesses::default(); - let accessed = record.per_contract.get(contract_id).unwrap_or(&empty); - - *balance_root = compute_balances_hash(accessed.assets_initial()); - *state_root = compute_state_hash(accessed.slots_initial()); + *balance_root = + compute_balances_hash(contract_id, record, &Changes::default()); + *state_root = + compute_state_hash(contract_id, record, &Changes::default()); } _ => {} } @@ -2245,7 +2243,8 @@ where tx_id: TxId, inputs: &[Input], outputs: &mut [Output], - record: &AccessedPerContract, + record: &[StorageReadReplayEvent], + changes: &Changes, ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { @@ -2262,12 +2261,10 @@ where }) }; - let empty = ContractAccesses::default(); - let accessed = record.per_contract.get(contract_id).unwrap_or(&empty); - contract_output.balance_root = - compute_balances_hash(accessed.assets_final()); - contract_output.state_root = compute_state_hash(accessed.slots_final()); + compute_balances_hash(contract_id, record, changes); + contract_output.state_root = + compute_state_hash(contract_id, record, changes); } } Ok(()) diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 7e267a1eea6..55e397914fa 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -1,986 +1,56 @@ -#![allow(unused)] - use fuel_core_storage::{ - ContractsAssetKey, - ContractsStateKey, - Error, - Mappable, - StorageBatchMutate, - StorageInspect, - StorageMutate, - StorageRead, - StorageSize, - StorageWrite, - blueprint::{ - BlueprintCodec, - BlueprintInspect, - }, - codec::{ - Decode, - Encode, - Encoder, - primitive::Primitive, - raw::Raw, - }, - column::Column, - merkle::sparse::KeyConverter, - structured_storage::TableWithBlueprint, - tables::{ - ConsensusParametersVersions, - ContractsAssets, - ContractsRawCode, - ContractsState, - FuelBlocks, - StateTransitionBytecodeVersions, - UploadedBytecodes, - merkle::{ - ContractsAssetsMerkleData, - ContractsAssetsMerkleMetadata, - }, + Result as StorageResult, + kv_store::{ + KeyValueInspect, + StorageColumn, + Value, }, }; -use fuel_core_types::{ - fuel_tx::{ - AssetId, - Bytes32, - ContractId, - Word, - }, - fuel_types::bytes, - fuel_vm::BlobData, -}; +use fuel_core_types::services::executor::StorageReadReplayEvent; use parking_lot::Mutex; #[cfg(feature = "std")] -use std::{ - borrow::Cow, - borrow::ToOwned, - collections::BTreeMap, - sync::Arc, -}; +use std::sync::Arc; #[cfg(not(feature = "std"))] use alloc::{ - borrow::Cow, - borrow::ToOwned, - collections::BTreeMap, sync::Arc, vec::Vec, }; -/// Value state before and after execution -#[derive(Debug, Clone)] -pub(crate) struct ValueHistory { - /// First observed value - pub first: Option, - /// Last observed value - pub last: Option, -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct ContractAccesses { - assets: BTreeMap>, - slots: BTreeMap>>, -} -impl ContractAccesses { - pub fn assets_initial(&self) -> BTreeMap { - self.assets - .iter() - .map(|(k, v)| (*k, v.first.unwrap_or(0))) - .collect() - } - - pub fn assets_final(&self) -> BTreeMap { - self.assets - .iter() - .map(|(k, v)| (*k, v.last.unwrap_or(0))) - .collect() - } - - pub fn slots_initial(&self) -> BTreeMap>> { - self.slots - .iter() - .map(|(k, v)| (*k, v.first.as_ref())) - .collect() - } - - pub fn slots_final(&self) -> BTreeMap>> { - self.slots - .iter() - .map(|(k, v)| (*k, v.last.as_ref())) - .collect() - } -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct AccessedPerContract { - pub per_contract: BTreeMap, -} - -pub(crate) struct StorageAccessRecorder { +pub struct StorageAccessRecorder +where + S: KeyValueInspect, +{ pub storage: S, - pub record: Arc>, + pub record: Arc>>, } -impl StorageAccessRecorder { +impl StorageAccessRecorder +where + S: KeyValueInspect, +{ pub fn new(storage: S) -> Self { Self { storage, record: Default::default(), } } - - fn read_asset(&self, key: &ContractsAssetKey, value: Option) { - let contract_id = key.contract_id(); - let asset_id = key.asset_id(); - - self.record - .lock() - .per_contract - .entry(*contract_id) - .or_default() - .assets - .entry(*asset_id) - .or_insert(ValueHistory { - first: value, - last: value, - }); - } - - fn write_asset(&self, key: &ContractsAssetKey, value: Word) { - let contract_id = key.contract_id(); - let asset_id = key.asset_id(); - - self.record - .lock() - .per_contract - .entry(*contract_id) - .or_default() - .assets - .entry(*asset_id) - .and_modify(|e| e.last = Some(value)); - } - - fn remove_asset(&self, key: &ContractsAssetKey) { - let contract_id = key.contract_id(); - let asset_id = key.asset_id(); - - self.record - .lock() - .per_contract - .entry(*contract_id) - .or_default() - .assets - .entry(*asset_id) - .and_modify(|e| e.last = None); - } - - fn read_state( - &self, - key: &ContractsStateKey, - value: Option<&::OwnedValue>, - ) { - let contract_id = key.contract_id(); - let state_key = key.state_key(); - - self.record - .lock() - .per_contract - .entry(*contract_id) - .or_default() - .slots - .entry(*state_key) - .or_insert(ValueHistory { - first: value.map(|v| v.0.clone()), - last: value.map(|v| v.0.clone()), - }); - } - - fn write_state( - &self, - key: &ContractsStateKey, - value: ::OwnedValue, - ) { - let contract_id = key.contract_id(); - let state_key = key.state_key(); - - self.record - .lock() - .per_contract - .entry(*contract_id) - .or_default() - .slots - .entry(*state_key) - .and_modify(|e| e.last = Some(value.to_owned().into())); - } - - fn remove_state(&self, key: &ContractsStateKey) { - let contract_id = key.contract_id(); - let state_key = key.state_key(); - - self.record - .lock() - .per_contract - .entry(*contract_id) - .or_default() - .slots - .entry(*state_key) - .and_modify(|e| e.last = None); - } -} - -macro_rules! storage_passthrough { - ($column:ident) => { - impl StorageInspect<$column> for StorageAccessRecorder - where - S: StorageInspect<$column, Error = Error>, - { - type Error = S::Error; - - fn get( - &self, - key: &<$column as Mappable>::Key, - ) -> Result::OwnedValue>>, Self::Error> { - <_ as StorageInspect<$column>>::get(&self.storage, key) - } - - fn contains_key( - &self, - key: &<$column as Mappable>::Key, - ) -> Result { - <_ as StorageInspect<$column>>::contains_key(&self.storage, key) - } - } - - impl StorageSize<$column> for StorageAccessRecorder - where - S: StorageSize<$column> + StorageInspect<$column, Error = Error>, - { - fn size_of_value( - &self, - key: &<$column as Mappable>::Key, - ) -> Result, Error> { - <_ as StorageSize<$column>>::size_of_value(&self.storage, key) - } - } - - impl StorageMutate<$column> for StorageAccessRecorder - where - S: StorageInspect<$column, Error = Error> - + StorageMutate<$column, Error = Error>, - { - fn insert( - &mut self, - key: &<$column as Mappable>::Key, - value: &<$column as Mappable>::Value, - ) -> Result<(), Self::Error> { - <_ as StorageMutate<$column>>::insert(&mut self.storage, key, value) - } - - fn replace( - &mut self, - key: &<$column as Mappable>::Key, - value: &<$column as Mappable>::Value, - ) -> Result::OwnedValue>, Self::Error> { - <_ as StorageMutate<$column>>::replace(&mut self.storage, key, value) - } - - fn remove( - &mut self, - key: &<$column as Mappable>::Key, - ) -> Result<(), Self::Error> { - <_ as StorageMutate<$column>>::remove(&mut self.storage, key) - } - - fn take( - &mut self, - key: &<$column as Mappable>::Key, - ) -> Result::OwnedValue>, Self::Error> { - <_ as StorageMutate<$column>>::take(&mut self.storage, key) - } - } - - impl StorageRead<$column> for StorageAccessRecorder - where - S: StorageRead<$column, Error = Error>, - $column: Mappable + TableWithBlueprint, - { - fn read( - &self, - key: &<$column as Mappable>::Key, - offset: usize, - buf: &mut [u8], - ) -> Result { - <_ as StorageRead<$column>>::read(&self.storage, key, offset, buf) - } - - fn read_alloc( - &self, - key: &<$column as Mappable>::Key, - ) -> Result>, Self::Error> { - <_ as StorageRead<$column>>::read_alloc(&self.storage, key) - } - } - - impl StorageWrite<$column> for StorageAccessRecorder - where - S: StorageWrite<$column, Error = Error>, - $column: Mappable + TableWithBlueprint, - { - fn write_bytes( - &mut self, - key: &<$column as Mappable>::Key, - buf: &[u8], - ) -> Result<(), Self::Error> { - <_ as StorageWrite<$column>>::write_bytes(&mut self.storage, key, buf) - } - - fn replace_bytes( - &mut self, - key: &<$column as Mappable>::Key, - buf: &[u8], - ) -> Result>, Self::Error> { - <_ as StorageWrite<$column>>::replace_bytes(&mut self.storage, key, buf) - } - - fn take_bytes( - &mut self, - key: &<$column as Mappable>::Key, - ) -> Result>, Self::Error> { - <_ as StorageWrite<$column>>::take_bytes(&mut self.storage, key) - } - } - - impl StorageBatchMutate<$column> for StorageAccessRecorder - where - S: StorageBatchMutate<$column, Error = Error>, - $column: Mappable + TableWithBlueprint, - { - fn init_storage<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a - + Iterator< - Item = ( - &'a <$column as Mappable>::Key, - &'a <$column as Mappable>::Value, - ), - >, - <$column as Mappable>::Key: 'a, - <$column as Mappable>::Value: 'a, - { - <_ as StorageBatchMutate<$column>>::init_storage(&mut self.storage, set) - } - - fn insert_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a - + Iterator< - Item = ( - &'a <$column as Mappable>::Key, - &'a <$column as Mappable>::Value, - ), - >, - <$column as Mappable>::Key: 'a, - <$column as Mappable>::Value: 'a, - { - <_ as StorageBatchMutate<$column>>::insert_batch(&mut self.storage, set) - } - - fn remove_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a + Iterator::Key>, - <$column as Mappable>::Key: 'a, - { - <_ as StorageBatchMutate<$column>>::remove_batch(&mut self.storage, set) - } - } - }; -} - -storage_passthrough!(BlobData); -storage_passthrough!(ConsensusParametersVersions); -storage_passthrough!(ContractsRawCode); -storage_passthrough!(FuelBlocks); -storage_passthrough!(StateTransitionBytecodeVersions); -storage_passthrough!(UploadedBytecodes); - -// Impl for ContractsAssets - -impl StorageInspect for StorageAccessRecorder -where - S: StorageInspect, -{ - type Error = S::Error; - - fn get( - &self, - key: &::Key, - ) -> Result::OwnedValue>>, Self::Error> { - let value = <_ as StorageInspect>::get(&self.storage, key)?; - self.read_asset(key, value.as_ref().map(|v| v.clone().into_owned())); - Ok(value) - } - - fn contains_key( - &self, - key: &::Key, - ) -> Result { - <_ as StorageInspect>::contains_key(&self.storage, key) - } -} - -impl StorageSize for StorageAccessRecorder -where - S: StorageSize + StorageInspect, -{ - fn size_of_value( - &self, - key: &::Key, - ) -> Result, Error> { - <_ as StorageSize>::size_of_value(&self.storage, key) - } -} - -impl StorageMutate for StorageAccessRecorder -where - S: StorageInspect - + StorageMutate, -{ - fn insert( - &mut self, - key: &::Key, - value: &::Value, - ) -> Result<(), Self::Error> { - <_ as StorageMutate>::insert(&mut self.storage, key, value)?; - self.write_asset(key, *value); - Ok(()) - } - - fn replace( - &mut self, - key: &::Key, - value: &::Value, - ) -> Result::OwnedValue>, Self::Error> { - let old_value = <_ as StorageMutate>::replace( - &mut self.storage, - key, - value, - )?; - self.read_asset(key, old_value); - self.write_asset(key, *value); - Ok(old_value) - } - - fn remove( - &mut self, - key: &::Key, - ) -> Result<(), Self::Error> { - <_ as StorageMutate>::remove(&mut self.storage, key)?; - self.remove_asset(key); - Ok(()) - } - - fn take( - &mut self, - key: &::Key, - ) -> Result::OwnedValue>, Self::Error> { - let value = <_ as StorageMutate>::take(&mut self.storage, key)?; - self.read_asset(key, value); - self.remove_asset(key); - Ok(value) - } -} - -impl StorageRead for StorageAccessRecorder -where - S: StorageRead, - ContractsAssets: Mappable + TableWithBlueprint, -{ - fn read( - &self, - key: &::Key, - offset: usize, - buf: &mut [u8], - ) -> Result { - // We need to get the whole value to record the access - let value = >::read_alloc(&self.storage, key)?; - - let value_dec = value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsAssets, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_asset(key, value_dec); - - let Some(bytes) = value else { - return Ok(false); - }; - - let bytes_len = bytes.len(); - let start = offset; - let end = offset.saturating_add(buf.len()); - - if end > bytes_len { - return Err(Error::Other(anyhow::anyhow!( - "Read out of bounds: value length is {bytes_len}, read range is {start}..{end}" - ))); - } - - let starting_from_offset = &bytes[start..end]; - buf[..].copy_from_slice(starting_from_offset); - Ok(true) - } - - fn read_alloc( - &self, - key: &::Key, - ) -> Result>, Self::Error> { - let value = >::read_alloc(&self.storage, key)?; - - let value_dec = value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsAssets, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_asset(key, value_dec); - Ok(value) - } -} - -impl StorageWrite for StorageAccessRecorder -where - S: StorageWrite, - ContractsAssets: Mappable + TableWithBlueprint, -{ - fn write_bytes( - &mut self, - key: &::Key, - buf: &[u8], - ) -> Result<(), Self::Error> { - let new_value = - <::Blueprint as BlueprintCodec< - ContractsAssets, - >>::ValueCodec::decode(buf)?; - - <_ as StorageWrite>::write_bytes(&mut self.storage, key, buf)?; - self.write_asset(key, new_value); - Ok(()) - } - - fn replace_bytes( - &mut self, - key: &::Key, - buf: &[u8], - ) -> Result>, Self::Error> { - let new_value = - <::Blueprint as BlueprintCodec< - ContractsAssets, - >>::ValueCodec::decode(buf)?; - - let old_value = <_ as StorageWrite>::replace_bytes( - &mut self.storage, - key, - buf, - )?; - let old_value_dec = old_value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsAssets, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_asset(key, old_value_dec); - self.write_asset(key, new_value); - - Ok(old_value) - } - - fn take_bytes( - &mut self, - key: &::Key, - ) -> Result>, Self::Error> { - let old_value = - <_ as StorageWrite>::take_bytes(&mut self.storage, key)?; - - let old_value_dec = old_value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsAssets, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_asset(key, old_value_dec); - self.remove_asset(key); - - Ok(old_value) - } -} - -impl StorageBatchMutate for StorageAccessRecorder -where - S: StorageBatchMutate, - ContractsAssets: Mappable + TableWithBlueprint, -{ - fn init_storage<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a - + Iterator< - Item = ( - &'a ::Key, - &'a ::Value, - ), - >, - ::Key: 'a, - ::Value: 'a, - { - let mut items = Vec::new(); - for (k, v) in set { - self.write_asset(k, *v); - items.push((k, v)); - } - <_ as StorageBatchMutate>::init_storage( - &mut self.storage, - items.into_iter(), - ) - } - - fn insert_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a - + Iterator< - Item = ( - &'a ::Key, - &'a ::Value, - ), - >, - ::Key: 'a, - ::Value: 'a, - { - let mut items = Vec::new(); - for (k, v) in set { - self.write_asset(k, *v); - items.push((k, v)); - } - <_ as StorageBatchMutate>::insert_batch( - &mut self.storage, - items.into_iter(), - ) - } - - fn remove_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a + Iterator::Key>, - ::Key: 'a, - { - let mut items = Vec::new(); - for k in set { - self.remove_asset(k); - items.push(k); - } - <_ as StorageBatchMutate>::remove_batch( - &mut self.storage, - items.into_iter(), - ) - } -} - -// Impl for ContractsState -impl StorageInspect for StorageAccessRecorder -where - S: StorageInspect, -{ - type Error = S::Error; - - fn get( - &self, - key: &::Key, - ) -> Result::OwnedValue>>, Self::Error> { - let value = <_ as StorageInspect>::get(&self.storage, key)?; - self.read_state(key, value.as_deref()); - Ok(value) - } - - fn contains_key( - &self, - key: &::Key, - ) -> Result { - <_ as StorageInspect>::contains_key(&self.storage, key) - } -} - -impl StorageSize for StorageAccessRecorder -where - S: StorageSize + StorageInspect, -{ - fn size_of_value( - &self, - key: &::Key, - ) -> Result, Error> { - <_ as StorageSize>::size_of_value(&self.storage, key) - } } -impl StorageMutate for StorageAccessRecorder +impl KeyValueInspect for StorageAccessRecorder where - S: StorageInspect - + StorageMutate, + S: KeyValueInspect, { - fn insert( - &mut self, - key: &::Key, - value: &::Value, - ) -> Result<(), Self::Error> { - <_ as StorageMutate>::insert(&mut self.storage, key, value)?; - self.write_state(key, (*value).into()); - Ok(()) - } - - fn replace( - &mut self, - key: &::Key, - value: &::Value, - ) -> Result::OwnedValue>, Self::Error> { - let old_value = - <_ as StorageMutate>::replace(&mut self.storage, key, value)?; - self.read_state(key, old_value.as_ref()); - self.write_state(key, value.into()); - Ok(old_value) - } - - fn remove( - &mut self, - key: &::Key, - ) -> Result<(), Self::Error> { - <_ as StorageMutate>::remove(&mut self.storage, key)?; - self.remove_state(key); - Ok(()) - } - - fn take( - &mut self, - key: &::Key, - ) -> Result::OwnedValue>, Self::Error> { - let value = <_ as StorageMutate>::take(&mut self.storage, key)?; - self.read_state(key, value.as_ref()); - self.remove_state(key); - Ok(value) - } -} - -impl StorageRead for StorageAccessRecorder -where - S: StorageRead, - ContractsState: Mappable + TableWithBlueprint, -{ - fn read( - &self, - key: &::Key, - offset: usize, - buf: &mut [u8], - ) -> Result { - // We need to get the whole value to record the access - let value = >::read_alloc(&self.storage, key)?; - - let value_dec = value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsState, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_state(key, value_dec.as_ref()); - - let Some(bytes) = value else { - return Ok(false); - }; - - let bytes_len = bytes.len(); - let start = offset; - let end = offset.saturating_add(buf.len()); - - if end > bytes_len { - return Err(Error::Other(anyhow::anyhow!( - "Read out of bounds: value length is {bytes_len}, read range is {start}..{end}" - ))); - } - - let starting_from_offset = &bytes[start..end]; - buf[..].copy_from_slice(starting_from_offset); - Ok(true) - } - - fn read_alloc( - &self, - key: &::Key, - ) -> Result>, Self::Error> { - let value = >::read_alloc(&self.storage, key)?; - - let value_dec = value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsState, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_state(key, value_dec.as_ref()); + type Column = S::Column; + fn get(&self, key: &[u8], column: Self::Column) -> StorageResult> { + let value = self.storage.get(key, column)?; + self.record.lock().push(StorageReadReplayEvent { + column: column.id(), + key: key.to_vec(), + value: value.as_ref().map(|v| v.to_vec()), + }); Ok(value) } } - -impl StorageWrite for StorageAccessRecorder -where - S: StorageWrite, - ContractsState: Mappable + TableWithBlueprint, -{ - fn write_bytes( - &mut self, - key: &::Key, - buf: &[u8], - ) -> Result<(), Self::Error> { - let new_value = - <::Blueprint as BlueprintCodec< - ContractsState, - >>::ValueCodec::decode(buf)?; - - <_ as StorageWrite>::write_bytes(&mut self.storage, key, buf)?; - self.write_state(key, new_value); - Ok(()) - } - - fn replace_bytes( - &mut self, - key: &::Key, - buf: &[u8], - ) -> Result>, Self::Error> { - let new_value = - <::Blueprint as BlueprintCodec< - ContractsState, - >>::ValueCodec::decode(buf)?; - - let old_value = <_ as StorageWrite>::replace_bytes( - &mut self.storage, - key, - buf, - )?; - let old_value_dec = old_value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsState, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_state(key, old_value_dec.as_ref()); - self.write_state(key, new_value); - - Ok(old_value) - } - - fn take_bytes( - &mut self, - key: &::Key, - ) -> Result>, Self::Error> { - let old_value = - <_ as StorageWrite>::take_bytes(&mut self.storage, key)?; - - let old_value_dec = old_value - .as_ref() - .map(|v| { - <::Blueprint as BlueprintCodec< - ContractsState, - >>::ValueCodec::decode(v) - }) - .transpose()?; - - self.read_state(key, old_value_dec.as_ref()); - self.remove_state(key); - - Ok(old_value) - } -} - -impl StorageBatchMutate for StorageAccessRecorder -where - S: StorageBatchMutate, - ContractsState: Mappable + TableWithBlueprint, -{ - fn init_storage<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a - + Iterator< - Item = ( - &'a ::Key, - &'a ::Value, - ), - >, - ::Key: 'a, - ::Value: 'a, - { - let mut items = Vec::new(); - for (k, v) in set { - self.write_state(k, (*v).into()); - items.push((k, v)); - } - <_ as StorageBatchMutate>::init_storage( - &mut self.storage, - items.into_iter(), - ) - } - - fn insert_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a - + Iterator< - Item = ( - &'a ::Key, - &'a ::Value, - ), - >, - ::Key: 'a, - ::Value: 'a, - { - let mut items = Vec::new(); - for (k, v) in set { - self.write_state(k, (*v).into()); - items.push((k, v)); - } - <_ as StorageBatchMutate>::insert_batch( - &mut self.storage, - items.into_iter(), - ) - } - - fn remove_batch<'a, Iter>(&mut self, set: Iter) -> Result<(), Error> - where - Iter: 'a + Iterator::Key>, - ::Key: 'a, - { - let mut items = Vec::new(); - for k in set { - self.remove_state(k); - items.push(k); - } - <_ as StorageBatchMutate>::remove_batch( - &mut self.storage, - items.into_iter(), - ) - } -} diff --git a/crates/storage/src/blueprint.rs b/crates/storage/src/blueprint.rs index 908593dce26..bbce620a0e2 100644 --- a/crates/storage/src/blueprint.rs +++ b/crates/storage/src/blueprint.rs @@ -23,18 +23,6 @@ pub mod merklized; pub mod plain; pub mod sparse; -/// Describes how to encode/decode the key and value for the given mappable table. -/// It is used by the blueprint to perform the actual encoding/decoding. -pub trait BlueprintCodec -where - M: Mappable, -{ - /// The codec used to encode and decode storage key. - type KeyCodec: Encode + Decode; - /// The codec used to encode and decode storage value. - type ValueCodec: Encode + Decode; -} - /// This trait allows defining the agnostic implementation for all storage /// traits(`StorageInspect,` `StorageMutate,` etc) while the main logic is /// hidden inside the blueprint. It allows quickly adding support for new @@ -42,13 +30,18 @@ where /// infrastructure in other places. It allows changing the blueprint on the /// fly in the definition of the table without affecting other areas of the codebase. /// -/// The blueprint is responsible for encoding/decoding (usually it is done via `KeyCodec` and `ValueCodec`) +/// The blueprint is responsible for encoding/decoding(usually it is done via `KeyCodec` and `ValueCodec`) /// the key and value and putting/extracting it to/from the storage. -pub trait BlueprintInspect: BlueprintCodec +pub trait BlueprintInspect where M: Mappable, S: KeyValueInspect, { + /// The codec used to encode and decode storage key. + type KeyCodec: Encode + Decode; + /// The codec used to encode and decode storage value. + type ValueCodec: Encode + Decode; + /// Checks if the value exists in the storage. fn exists(storage: &S, key: &M::Key, column: S::Column) -> StorageResult { let key_encoder = Self::KeyCodec::encode(key); diff --git a/crates/storage/src/blueprint/merklized.rs b/crates/storage/src/blueprint/merklized.rs index 1eedb3bc660..f504e954d6d 100644 --- a/crates/storage/src/blueprint/merklized.rs +++ b/crates/storage/src/blueprint/merklized.rs @@ -11,7 +11,6 @@ use crate::{ StorageInspect, StorageMutate, blueprint::{ - BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -123,17 +122,6 @@ where } } -impl BlueprintCodec - for Merklized -where - M: Mappable, - KeyCodec: Encode + Decode, - ValueCodec: Encode + Decode, -{ - type KeyCodec = KeyCodec; - type ValueCodec = ValueCodec; -} - impl BlueprintInspect for Merklized where @@ -142,6 +130,8 @@ where KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { + type KeyCodec = KeyCodec; + type ValueCodec = ValueCodec; } impl BlueprintMutate diff --git a/crates/storage/src/blueprint/plain.rs b/crates/storage/src/blueprint/plain.rs index 87883bc5cc9..fb64fc7fe7a 100644 --- a/crates/storage/src/blueprint/plain.rs +++ b/crates/storage/src/blueprint/plain.rs @@ -8,7 +8,6 @@ use crate::{ Mappable, Result as StorageResult, blueprint::{ - BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -34,16 +33,6 @@ pub struct Plain { _marker: core::marker::PhantomData<(KeyCodec, ValueCodec)>, } -impl BlueprintCodec for Plain -where - M: Mappable, - KeyCodec: Encode + Decode, - ValueCodec: Encode + Decode, -{ - type KeyCodec = KeyCodec; - type ValueCodec = ValueCodec; -} - impl BlueprintInspect for Plain where M: Mappable, @@ -51,6 +40,8 @@ where KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { + type KeyCodec = KeyCodec; + type ValueCodec = ValueCodec; } impl BlueprintMutate for Plain @@ -142,10 +133,10 @@ where column, set.map(|(key, value)| { let key_encoder = - >::KeyCodec::encode(key); + >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes().to_vec(); let value = - >::ValueCodec::encode_as_value( + >::ValueCodec::encode_as_value( value, ); (key_bytes, WriteOperation::Insert(value)) @@ -166,7 +157,7 @@ where column, set.map(|key| { let key_encoder = - >::KeyCodec::encode(key); + >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes().to_vec(); (key_bytes, WriteOperation::Remove) }), diff --git a/crates/storage/src/blueprint/sparse.rs b/crates/storage/src/blueprint/sparse.rs index 1eefe9f549c..ba09bd38b89 100644 --- a/crates/storage/src/blueprint/sparse.rs +++ b/crates/storage/src/blueprint/sparse.rs @@ -12,7 +12,6 @@ use crate::{ StorageInspect, StorageMutate, blueprint::{ - BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -167,17 +166,6 @@ where } } -impl BlueprintCodec - for Sparse -where - M: Mappable, - KeyCodec: Encode + Decode, - ValueCodec: Encode + Decode, -{ - type KeyCodec = KeyCodec; - type ValueCodec = ValueCodec; -} - impl BlueprintInspect for Sparse where @@ -186,6 +174,8 @@ where KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { + type KeyCodec = KeyCodec; + type ValueCodec = ValueCodec; } impl BlueprintMutate @@ -283,6 +273,11 @@ where } } +type NodeKeyCodec = + <::Blueprint as BlueprintInspect>::KeyCodec; +type NodeValueCodec = + <::Blueprint as BlueprintInspect>::ValueCodec; + impl SupportsBatching for Sparse where @@ -352,14 +347,10 @@ where )?; let nodes = nodes.iter().map(|(key, value)| { - let key = <::Blueprint as BlueprintCodec< - Nodes, - >>::KeyCodec::encode(key) - .as_bytes() - .into_owned(); - let value = <::Blueprint as BlueprintCodec< - Nodes, - >>::ValueCodec::encode_as_value(value); + let key = NodeKeyCodec::::encode(key) + .as_bytes() + .into_owned(); + let value = NodeValueCodec::::encode_as_value(value); (key, WriteOperation::Insert(value)) }); storage.batch_write(Nodes::column(), nodes)?; diff --git a/crates/storage/src/iter.rs b/crates/storage/src/iter.rs index de3c15b3051..d06419b386f 100644 --- a/crates/storage/src/iter.rs +++ b/crates/storage/src/iter.rs @@ -1,10 +1,7 @@ //! The module defines primitives that allow iterating of the storage. use crate::{ - blueprint::{ - BlueprintCodec, - BlueprintInspect, - }, + blueprint::BlueprintInspect, codec::{ Decode, Encode, @@ -167,10 +164,9 @@ where where P: AsRef<[u8]>, { - #[allow(clippy::redundant_closure)] - // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 - let encoder = start - .map(|start| >::KeyCodec::encode(start)); + let encoder = start.map(|start| { + >::KeyCodec::encode(start) + }); let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); @@ -183,9 +179,10 @@ where ) .map(|res| { res.and_then(|key| { - let key = - >::KeyCodec::decode(key.as_slice()) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let key = >::KeyCodec::decode( + key.as_slice(), + ) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; Ok(key) }) }) @@ -201,10 +198,9 @@ where where P: AsRef<[u8]>, { - #[allow(clippy::redundant_closure)] - // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 - let encoder = start - .map(|start| >::KeyCodec::encode(start)); + let encoder = start.map(|start| { + >::KeyCodec::encode(start) + }); let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); @@ -217,12 +213,15 @@ where ) .map(|val| { val.and_then(|(key, value)| { - let key = - >::KeyCodec::decode(key.as_slice()) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let key = >::KeyCodec::decode( + key.as_slice(), + ) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; let value = - >::ValueCodec::decode(&value) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + >::ValueCodec::decode( + &value, + ) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; Ok((key, value)) }) }) diff --git a/crates/storage/src/merkle/sparse.rs b/crates/storage/src/merkle/sparse.rs index 2f11a61ac5e..c8348466f99 100644 --- a/crates/storage/src/merkle/sparse.rs +++ b/crates/storage/src/merkle/sparse.rs @@ -4,7 +4,6 @@ use crate::{ Mappable, Result as StorageResult, blueprint::{ - BlueprintCodec, BlueprintInspect, plain::Plain, sparse::{ @@ -115,11 +114,17 @@ where type OwnedValue =
::OwnedValue; } -type KeyCodec
= - <
::Blueprint as BlueprintCodec
>::KeyCodec; +type KeyCodec = + <
::Blueprint as BlueprintInspect< + Table, + DummyStorage>, + >>::KeyCodec; -type ValueCodec
= - <
::Blueprint as BlueprintCodec
>::ValueCodec; +type ValueCodec = + <
::Blueprint as BlueprintInspect< + Table, + DummyStorage>, + >>::ValueCodec; impl TableWithBlueprint for Merkleized
where @@ -129,8 +134,8 @@ where Table::Blueprint: BlueprintInspect>>, { type Blueprint = Sparse< - KeyCodec
, - ValueCodec
, + KeyCodec, + ValueCodec, MerkleMetadata, MerkleData
, KeyConverter
, diff --git a/crates/storage/src/structured_storage.rs b/crates/storage/src/structured_storage.rs index cf52bf1b268..3ce5e49ff32 100644 --- a/crates/storage/src/structured_storage.rs +++ b/crates/storage/src/structured_storage.rs @@ -14,7 +14,6 @@ use crate::{ StorageSize, StorageWrite, blueprint::{ - BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -363,7 +362,10 @@ where offset: usize, buf: &mut [u8], ) -> Result { - let key_encoder = >::KeyCodec::encode(key); + let key_encoder = + >>::KeyCodec::encode( + key, + ); let key_bytes = key_encoder.as_bytes(); self.inner.read( key_bytes.as_ref(), @@ -377,7 +379,10 @@ where &self, key: &::Key, ) -> Result>, Self::Error> { - let key_encoder = >::KeyCodec::encode(key); + let key_encoder = + >>::KeyCodec::encode( + key, + ); let key_bytes = key_encoder.as_bytes(); self.inner .get(key_bytes.as_ref(), ::column()) From 95c98de982687853fc7fd55785f1c6dc860a9de0 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 24 Sep 2025 18:41:49 +0300 Subject: [PATCH 09/22] Use mark-and-fetch approach instead of trying to collect all values in tx --- .../src/service/adapters/graphql_api.rs | 11 +- .../services/executor/src/contract_state.rs | 157 ------------------ .../executor/src/contract_state_hash.rs | 54 ++++++ crates/services/executor/src/executor.rs | 149 ++++++++++++++--- crates/services/executor/src/lib.rs | 2 +- .../executor/src/storage_access_recorder.rs | 112 +++++++++++-- crates/storage/src/blueprint.rs | 21 ++- crates/storage/src/blueprint/merklized.rs | 14 +- crates/storage/src/blueprint/plain.rs | 19 ++- crates/storage/src/blueprint/sparse.rs | 31 ++-- crates/storage/src/iter.rs | 39 ++--- crates/storage/src/merkle/sparse.rs | 19 +-- crates/storage/src/structured_storage.rs | 11 +- 13 files changed, 374 insertions(+), 265 deletions(-) delete mode 100644 crates/services/executor/src/contract_state.rs create mode 100644 crates/services/executor/src/contract_state_hash.rs diff --git a/crates/fuel-core/src/service/adapters/graphql_api.rs b/crates/fuel-core/src/service/adapters/graphql_api.rs index a99009232b5..52fc3c55e47 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api.rs @@ -9,11 +9,7 @@ use super::{ import_result_provider, }; use crate::{ - database::{ - Database, - OnChainIterableKeyValueView, - database_description::compression::CompressionDatabase, - }, + database::OnChainIterableKeyValueView, fuel_core_graphql_api::ports::{ BlockProducerPort, ChainStateProvider, @@ -45,7 +41,7 @@ use fuel_core_compression_service::storage::CompressedBlocks; use fuel_core_services::stream::BoxStream; use fuel_core_storage::{ Result as StorageResult, - blueprint::BlueprintInspect, + blueprint::BlueprintCodec, kv_store::KeyValueInspect, not_found, structured_storage::TableWithBlueprint, @@ -303,9 +299,8 @@ impl DatabaseDaCompressedBlocks for CompressionServiceAdapter { use fuel_core_storage::codec::Encode; let encoded_height = - <::Blueprint as BlueprintInspect< + <::Blueprint as BlueprintCodec< CompressedBlocks, - Database, /* in the future it would be nice to use a dummy impl, but it's not worth the effort rn */ >>::KeyCodec::encode(height); let column = ::column(); self.storage() diff --git a/crates/services/executor/src/contract_state.rs b/crates/services/executor/src/contract_state.rs deleted file mode 100644 index 5f628fc9dc0..00000000000 --- a/crates/services/executor/src/contract_state.rs +++ /dev/null @@ -1,157 +0,0 @@ -#[cfg(feature = "std")] -use std::collections::BTreeMap; - -#[cfg(all(feature = "alloc", not(feature = "std")))] -use alloc::{ - collections::BTreeMap, - vec::Vec, -}; - -use fuel_core_storage::{ - ContractsAssetKey, - ContractsStateKey, - column::Column, - kv_store::WriteOperation, - transactional::Changes, -}; -use fuel_core_types::{ - fuel_tx::{ - AssetId, - Bytes32, - ContractId, - Word, - }, - services::executor::StorageReadReplayEvent, -}; -use sha2::{ - Digest, - Sha256, -}; - -/// Computes a hash of all contract balances that were read or modified. -/// The hash is not dependent on the order of reads or writes. -/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution -pub fn compute_balances_hash( - contract_id: &ContractId, - record: &[StorageReadReplayEvent], - changes: &Changes, -) -> Bytes32 { - let mut touched_assets: BTreeMap = BTreeMap::new(); - for r in record { - if r.column == Column::ContractsAssets as u32 { - let key = ContractsAssetKey::from_slice(&r.key).unwrap(); - let r_contract_id = key.contract_id(); - let r_asset_id = key.asset_id(); - - if r_contract_id != contract_id { - continue; - } - - touched_assets.insert( - *r_asset_id, - r.value - .clone() - .map(|v| { - let mut buf = [0; 8]; - buf.copy_from_slice(v.as_slice()); - Word::from_be_bytes(buf) - }) - .unwrap_or(0), - ); - } - } - - for (change_column, change) in changes { - if *change_column == Column::ContractsAssets as u32 { - for (key, value) in change.iter() { - let key = ContractsStateKey::from_slice(key).unwrap(); - let c_contract_id = key.contract_id(); - let c_state_key = key.state_key(); - - if c_contract_id != contract_id { - continue; - } - - touched_assets.insert( - AssetId::from(**c_state_key), - match value { - WriteOperation::Insert(v) => { - let mut buf = [0; 8]; - buf.copy_from_slice(v); - Word::from_be_bytes(buf) - } - WriteOperation::Remove => 0, - }, - ); - } - } - } - - let mut hasher = Sha256::new(); - for (state_key, state_value) in touched_assets { - hasher.update(state_key); - hasher.update(state_value.to_be_bytes()); - } - let digest: [u8; 32] = hasher.finalize().into(); - Bytes32::from(digest) -} - -/// Computes a hash of all contract state slots that were read or modified. -/// The hash is not dependent on the order of reads or writes. -/// Leave `changes` empty when there are no changes yet, i.e. when computing state before execution -pub fn compute_state_hash( - contract_id: &ContractId, - record: &[StorageReadReplayEvent], - changes: &Changes, -) -> Bytes32 { - let mut touched_slots: BTreeMap>> = BTreeMap::new(); - for r in record { - if r.column == Column::ContractsState as u32 { - let key = ContractsStateKey::from_slice(&r.key).unwrap(); - let r_contract_id = key.contract_id(); - let r_state_key = key.state_key(); - - if r_contract_id != contract_id { - continue; - } - - touched_slots.insert(*r_state_key, r.value.clone()); - } - } - - for (change_column, change) in changes { - if *change_column == Column::ContractsState as u32 { - for (key, value) in change.iter() { - let key = ContractsStateKey::from_slice(key).unwrap(); - let c_contract_id = key.contract_id(); - let c_state_key = key.state_key(); - - if c_contract_id != contract_id { - continue; - } - - touched_slots.insert( - *c_state_key, - match value { - WriteOperation::Insert(v) => Some(v.to_vec()), - WriteOperation::Remove => None, - }, - ); - } - } - } - - let mut hasher = Sha256::new(); - for (state_key, state_value) in touched_slots { - hasher.update(state_key); - if let Some(value) = state_value { - hasher.update([1u8]); - hasher.update((value.len() as Word).to_be_bytes()); - hasher.update(&value); - } else { - hasher.update([0u8]); - } - } - let digest: [u8; 32] = hasher.finalize().into(); - Bytes32::from(digest) -} diff --git a/crates/services/executor/src/contract_state_hash.rs b/crates/services/executor/src/contract_state_hash.rs new file mode 100644 index 00000000000..e67a160ebea --- /dev/null +++ b/crates/services/executor/src/contract_state_hash.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "std")] +use std::collections::BTreeSet; + +#[cfg(all(feature = "alloc", not(feature = "std")))] +use alloc::{ + collections::BTreeSet, + vec::Vec, +}; + +use fuel_core_types::fuel_tx::{ + AssetId, + Bytes32, + Word, +}; +use sha2::{ + Digest, + Sha256, +}; + +/// Computes a hash of all contract balances that were read or modified. +/// The hash is not dependent on the order of reads or writes. +pub fn compute_balances_hash( + accessed: &BTreeSet, + get_value: impl Fn(&AssetId) -> Result, +) -> Result { + let mut hasher = Sha256::new(); + for key in accessed { + hasher.update(key); + hasher.update(get_value(key)?.to_be_bytes()); + } + let digest: [u8; 32] = hasher.finalize().into(); + Ok(Bytes32::from(digest)) +} + +/// Computes a hash of all contract state slots that were read or modified. +/// The hash is not dependent on the order of reads or writes. +pub fn compute_state_hash( + accessed: &BTreeSet, + get_value: impl Fn(&Bytes32) -> Result>, E>, +) -> Result { + let mut hasher = Sha256::new(); + for key in accessed { + hasher.update(key); + if let Some(value) = get_value(key)? { + hasher.update([1u8]); + hasher.update((value.len() as Word).to_be_bytes()); + hasher.update(&value); + } else { + hasher.update([0u8]); + } + } + let digest: [u8; 32] = hasher.finalize().into(); + Ok(Bytes32::from(digest)) +} diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index b0eabeab0c6..8349f832eb0 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1,5 +1,5 @@ use crate::{ - contract_state::{ + contract_state_hash::{ compute_balances_hash, compute_state_hash, }, @@ -11,17 +11,30 @@ use crate::{ TransactionsSource, }, refs::ContractRef, - storage_access_recorder::StorageAccessRecorder, + storage_access_recorder::{ + ContractAccesses, + StorageAccessRecorder, + }, }; use fuel_core_storage::{ + ContractsAssetKey, + ContractsStateKey, StorageAsMut, StorageAsRef, + blueprint::BlueprintCodec, + codec::Decode, column::Column, - kv_store::KeyValueInspect, + kv_store::{ + KeyValueInspect, + WriteOperation, + }, + structured_storage::TableWithBlueprint, tables::{ Coins, ConsensusParametersVersions, + ContractsAssets, ContractsLatestUtxo, + ContractsState, FuelBlocks, Messages, ProcessedTransactions, @@ -32,6 +45,7 @@ use fuel_core_storage::{ IntoTransaction, Modifiable, ReadTransaction, + ReferenceBytesKey, StorageTransaction, WriteTransaction, }, @@ -141,7 +155,6 @@ use fuel_core_types::{ ExecutionResult, ForcedTransactionFailure, Result as ExecutorResult, - StorageReadReplayEvent, TransactionExecutionResult, TransactionExecutionStatus, TransactionValidityError, @@ -163,10 +176,16 @@ use tracing::{ }; #[cfg(feature = "std")] -use std::borrow::Cow; +use std::{ + borrow::Cow, + collections::BTreeMap, +}; #[cfg(not(feature = "std"))] -use alloc::borrow::Cow; +use alloc::{ + borrow::Cow, + collections::BTreeMap, +}; #[cfg(feature = "alloc")] use alloc::{ @@ -1425,7 +1444,11 @@ where )?; } - self.compute_inputs(core::slice::from_mut(&mut input), storage_tx, &[])?; + self.compute_inputs( + core::slice::from_mut(&mut input), + storage_tx, + &Default::default(), + )?; let (input, output) = self.execute_mint_with_vm( header, @@ -1618,7 +1641,7 @@ where .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; let (recorder, changes) = sub_block_db_commit.into_inner(); - let record = core::mem::take(&mut *recorder.record.lock()); + let record = core::mem::take(&mut *recorder.record.lock()).finalize(&changes); storage_tx.commit_changes(changes.clone())?; @@ -1638,6 +1661,7 @@ where core::slice::from_ref(input), outputs.as_mut_slice(), &record, + storage_tx, &changes, )?; let Input::Contract(input) = core::mem::take(input) else { @@ -1653,15 +1677,17 @@ where Ok((input, output)) } - fn update_tx_outputs( + fn update_tx_outputs( &self, tx_id: TxId, tx: &mut Tx, - record: &[StorageReadReplayEvent], + record: &BTreeMap, + db: &TxStorageTransaction, changes: &Changes, ) -> ExecutorResult<()> where Tx: ExecutableTransaction, + T: KeyValueInspect, { let mut outputs = core::mem::take(tx.outputs_mut()); self.compute_state_of_not_utxo_outputs( @@ -1669,6 +1695,7 @@ where tx.inputs(), &mut outputs, record, + db, changes, )?; *tx.outputs_mut() = outputs; @@ -1972,18 +1999,18 @@ where }, changes, ) = sub_block_db_commit.into_inner(); - let record = record.lock().clone(); + let record = record.lock().clone().finalize(&changes); // We always need to update inputs with storage state before execution, // because VM zeroes malleable fields during the execution. self.compute_inputs(tx.inputs_mut(), storage_tx_recovered, &record)?; + self.update_tx_outputs(tx_id, &mut tx, &record, storage_tx, &changes)?; // only commit state changes if execution was a success if !reverted { storage_tx.commit_changes(changes.clone())?; } - self.update_tx_outputs(tx_id, &mut tx, &record, &changes)?; Ok((reverted, state, tx, receipts.to_vec())) } @@ -2181,7 +2208,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, - record: &[StorageReadReplayEvent], + record: &BTreeMap, ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2222,10 +2249,19 @@ where *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); - *balance_root = - compute_balances_hash(contract_id, record, &Changes::default()); - *state_root = - compute_state_hash(contract_id, record, &Changes::default()); + let empty = ContractAccesses::default(); + let for_contract = record.get(contract_id).unwrap_or(&empty); + + *balance_root = compute_balances_hash(&for_contract.assets, |key| { + db.storage::() + .get(&ContractsAssetKey::new(contract_id, key)) + .map(|v| v.map_or(0, |c| c.into_owned())) + })?; + *state_root = compute_state_hash(&for_contract.slots, |key| { + db.storage::() + .get(&ContractsStateKey::new(contract_id, key)) + .map(|v| v.map(|c| c.0.clone())) + })?; } _ => {} } @@ -2238,14 +2274,18 @@ where /// Computes all zeroed or variable outputs. /// In production mode, updates the outputs with computed values. /// In validation mode, compares the outputs with computed inputs. - fn compute_state_of_not_utxo_outputs( + fn compute_state_of_not_utxo_outputs( &self, tx_id: TxId, inputs: &[Input], outputs: &mut [Output], - record: &[StorageReadReplayEvent], + record: &BTreeMap, + db: &TxStorageTransaction, changes: &Changes, - ) -> ExecutorResult<()> { + ) -> ExecutorResult<()> + where + T: KeyValueInspect, + { for output in outputs { if let Output::Contract(contract_output) = output { let contract_id = @@ -2261,10 +2301,71 @@ where }) }; - contract_output.balance_root = - compute_balances_hash(contract_id, record, changes); - contract_output.state_root = - compute_state_hash(contract_id, record, changes); + let empty = ContractAccesses::default(); + let for_contract = record.get(contract_id).unwrap_or(&empty); + + let empty = BTreeMap::default(); + let asset_changes = changes + .get(&Column::ContractsAssets.as_u32()) + .unwrap_or(&empty); + let state_changes = changes + .get(&Column::ContractsState.as_u32()) + .unwrap_or(&empty); + + contract_output.balance_root = compute_balances_hash( + &for_contract.assets, + |key| { + let column_key = ContractsAssetKey::new(contract_id, key); + let column_key = + ReferenceBytesKey::from(column_key.as_ref().to_vec()); + if let Some(changed_value) = + asset_changes.get(&column_key).map(|value| { + match value { + WriteOperation::Insert(v) => { + < + ::Blueprint as BlueprintCodec> + ::ValueCodec::decode(v) + .map_err(|err| ExecutorError::StorageError(err.to_string())) + }, + WriteOperation::Remove => Ok(0), + } + }) + { + return changed_value; + } + db.storage::() + .get(&ContractsAssetKey::new(contract_id, key)) + .map(|v| v.map_or(0, |c| c.into_owned())) + .map_err(|err| ExecutorError::StorageError(err.to_string())) + }, + )?; + contract_output.state_root = compute_state_hash( + &for_contract.slots, + |key| { + let column_key = ContractsStateKey::new(contract_id, key); + let column_key = + ReferenceBytesKey::from(column_key.as_ref().to_vec()); + if let Some(changed_value) = + state_changes.get(&column_key).map(|value| { + match value { + WriteOperation::Insert(v) => { + Some(< + ::Blueprint as BlueprintCodec> + ::ValueCodec::decode(v) + .map_err(|err| ExecutorError::StorageError(err.to_string()))) + }, + WriteOperation::Remove => None, + } + }) + { + return changed_value.transpose(); + } + db.storage::() + .get(&ContractsStateKey::new(contract_id, key)) + .map(|v| v.map(|c| c.0.clone())) + .map_err(|err| ExecutorError::StorageError(err.to_string())) + }, + )?; } } Ok(()) diff --git a/crates/services/executor/src/lib.rs b/crates/services/executor/src/lib.rs index 071a33c9fd1..f6c33113468 100644 --- a/crates/services/executor/src/lib.rs +++ b/crates/services/executor/src/lib.rs @@ -11,7 +11,7 @@ pub mod executor; pub mod ports; pub mod refs; -mod contract_state; +mod contract_state_hash; mod storage_access_recorder; #[cfg(test)] diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 55e397914fa..6bc0a3f006a 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -1,29 +1,91 @@ use fuel_core_storage::{ + ContractsAssetKey, + ContractsStateKey, Result as StorageResult, + column::Column, kv_store::{ KeyValueInspect, StorageColumn, Value, }, + transactional::Changes, +}; +use fuel_core_types::fuel_tx::{ + AssetId, + Bytes32, + ContractId, }; -use fuel_core_types::services::executor::StorageReadReplayEvent; use parking_lot::Mutex; #[cfg(feature = "std")] -use std::sync::Arc; +use std::{ + collections::{ + BTreeMap, + BTreeSet, + }, + sync::Arc, +}; #[cfg(not(feature = "std"))] use alloc::{ + collections::{ + BTreeMap, + BTreeSet, + }, sync::Arc, - vec::Vec, }; +#[derive(Debug, Clone, Default)] +pub(crate) struct ContractAccesses { + pub assets: BTreeSet, + pub slots: BTreeSet, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct ReadsPerContract { + pub per_contract: BTreeMap, +} + +impl ReadsPerContract { + /// Mark some key as accessed without actually reading it. + fn mark(&mut self, key: &[u8], column_id: u32) { + if column_id == Column::ContractsAssets.as_u32() { + let key = ContractsAssetKey::from_slice(&key).unwrap(); + self.per_contract + .entry(*key.contract_id()) + .or_default() + .assets + .insert(*key.asset_id()); + } else if column_id == Column::ContractsState.as_u32() { + let key = ContractsStateKey::from_slice(&key).unwrap(); + self.per_contract + .entry(*key.contract_id()) + .or_default() + .slots + .insert(*key.state_key()); + } + } + + /// Go through the changes of a transaction and mark all changed keys as accessed. + pub(crate) fn finalize( + mut self, + changes: &Changes, + ) -> BTreeMap { + for (change_column, change) in changes { + for (key, _) in change.iter() { + self.mark(key, *change_column); + } + } + self.per_contract + } +} + pub struct StorageAccessRecorder where S: KeyValueInspect, { pub storage: S, - pub record: Arc>>, + pub record: Arc>, } impl StorageAccessRecorder @@ -36,6 +98,11 @@ where record: Default::default(), } } + + /// Mark some key as accessed without actually reading it. + fn mark(&self, key: &[u8], column_id: u32) { + self.record.lock().mark(key, column_id); + } } impl KeyValueInspect for StorageAccessRecorder @@ -45,12 +112,35 @@ where type Column = S::Column; fn get(&self, key: &[u8], column: Self::Column) -> StorageResult> { - let value = self.storage.get(key, column)?; - self.record.lock().push(StorageReadReplayEvent { - column: column.id(), - key: key.to_vec(), - value: value.as_ref().map(|v| v.to_vec()), - }); - Ok(value) + self.mark(key, column.id()); + self.storage.get(key, column) + } + + /// Checks if the value exists in the storage. + fn exists(&self, key: &[u8], column: Self::Column) -> StorageResult { + self.mark(key, column.id()); + self.storage.exists(key, column) + } + + /// Returns the size of the value in the storage. + fn size_of_value( + &self, + key: &[u8], + column: Self::Column, + ) -> StorageResult> { + self.mark(key, column.id()); + self.storage.size_of_value(key, column) + } + + /// Reads the value from the storage into the `buf` and returns the whether the value exists. + fn read( + &self, + key: &[u8], + column: Self::Column, + offset: usize, + buf: &mut [u8], + ) -> StorageResult { + self.mark(key, column.id()); + self.storage.read(key, column, offset, buf) } } diff --git a/crates/storage/src/blueprint.rs b/crates/storage/src/blueprint.rs index bbce620a0e2..908593dce26 100644 --- a/crates/storage/src/blueprint.rs +++ b/crates/storage/src/blueprint.rs @@ -23,6 +23,18 @@ pub mod merklized; pub mod plain; pub mod sparse; +/// Describes how to encode/decode the key and value for the given mappable table. +/// It is used by the blueprint to perform the actual encoding/decoding. +pub trait BlueprintCodec +where + M: Mappable, +{ + /// The codec used to encode and decode storage key. + type KeyCodec: Encode + Decode; + /// The codec used to encode and decode storage value. + type ValueCodec: Encode + Decode; +} + /// This trait allows defining the agnostic implementation for all storage /// traits(`StorageInspect,` `StorageMutate,` etc) while the main logic is /// hidden inside the blueprint. It allows quickly adding support for new @@ -30,18 +42,13 @@ pub mod sparse; /// infrastructure in other places. It allows changing the blueprint on the /// fly in the definition of the table without affecting other areas of the codebase. /// -/// The blueprint is responsible for encoding/decoding(usually it is done via `KeyCodec` and `ValueCodec`) +/// The blueprint is responsible for encoding/decoding (usually it is done via `KeyCodec` and `ValueCodec`) /// the key and value and putting/extracting it to/from the storage. -pub trait BlueprintInspect +pub trait BlueprintInspect: BlueprintCodec where M: Mappable, S: KeyValueInspect, { - /// The codec used to encode and decode storage key. - type KeyCodec: Encode + Decode; - /// The codec used to encode and decode storage value. - type ValueCodec: Encode + Decode; - /// Checks if the value exists in the storage. fn exists(storage: &S, key: &M::Key, column: S::Column) -> StorageResult { let key_encoder = Self::KeyCodec::encode(key); diff --git a/crates/storage/src/blueprint/merklized.rs b/crates/storage/src/blueprint/merklized.rs index f504e954d6d..1eedb3bc660 100644 --- a/crates/storage/src/blueprint/merklized.rs +++ b/crates/storage/src/blueprint/merklized.rs @@ -11,6 +11,7 @@ use crate::{ StorageInspect, StorageMutate, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -122,11 +123,10 @@ where } } -impl BlueprintInspect +impl BlueprintCodec for Merklized where M: Mappable, - S: KeyValueInspect, KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { @@ -134,6 +134,16 @@ where type ValueCodec = ValueCodec; } +impl BlueprintInspect + for Merklized +where + M: Mappable, + S: KeyValueInspect, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, +{ +} + impl BlueprintMutate for Merklized where diff --git a/crates/storage/src/blueprint/plain.rs b/crates/storage/src/blueprint/plain.rs index fb64fc7fe7a..87883bc5cc9 100644 --- a/crates/storage/src/blueprint/plain.rs +++ b/crates/storage/src/blueprint/plain.rs @@ -8,6 +8,7 @@ use crate::{ Mappable, Result as StorageResult, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -33,10 +34,9 @@ pub struct Plain { _marker: core::marker::PhantomData<(KeyCodec, ValueCodec)>, } -impl BlueprintInspect for Plain +impl BlueprintCodec for Plain where M: Mappable, - S: KeyValueInspect, KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { @@ -44,6 +44,15 @@ where type ValueCodec = ValueCodec; } +impl BlueprintInspect for Plain +where + M: Mappable, + S: KeyValueInspect, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, +{ +} + impl BlueprintMutate for Plain where M: Mappable, @@ -133,10 +142,10 @@ where column, set.map(|(key, value)| { let key_encoder = - >::KeyCodec::encode(key); + >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes().to_vec(); let value = - >::ValueCodec::encode_as_value( + >::ValueCodec::encode_as_value( value, ); (key_bytes, WriteOperation::Insert(value)) @@ -157,7 +166,7 @@ where column, set.map(|key| { let key_encoder = - >::KeyCodec::encode(key); + >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes().to_vec(); (key_bytes, WriteOperation::Remove) }), diff --git a/crates/storage/src/blueprint/sparse.rs b/crates/storage/src/blueprint/sparse.rs index ba09bd38b89..1eefe9f549c 100644 --- a/crates/storage/src/blueprint/sparse.rs +++ b/crates/storage/src/blueprint/sparse.rs @@ -12,6 +12,7 @@ use crate::{ StorageInspect, StorageMutate, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -166,11 +167,10 @@ where } } -impl BlueprintInspect +impl BlueprintCodec for Sparse where M: Mappable, - S: KeyValueInspect, KeyCodec: Encode + Decode, ValueCodec: Encode + Decode, { @@ -178,6 +178,16 @@ where type ValueCodec = ValueCodec; } +impl BlueprintInspect + for Sparse +where + M: Mappable, + S: KeyValueInspect, + KeyCodec: Encode + Decode, + ValueCodec: Encode + Decode, +{ +} + impl BlueprintMutate for Sparse where @@ -273,11 +283,6 @@ where } } -type NodeKeyCodec = - <::Blueprint as BlueprintInspect>::KeyCodec; -type NodeValueCodec = - <::Blueprint as BlueprintInspect>::ValueCodec; - impl SupportsBatching for Sparse where @@ -347,10 +352,14 @@ where )?; let nodes = nodes.iter().map(|(key, value)| { - let key = NodeKeyCodec::::encode(key) - .as_bytes() - .into_owned(); - let value = NodeValueCodec::::encode_as_value(value); + let key = <::Blueprint as BlueprintCodec< + Nodes, + >>::KeyCodec::encode(key) + .as_bytes() + .into_owned(); + let value = <::Blueprint as BlueprintCodec< + Nodes, + >>::ValueCodec::encode_as_value(value); (key, WriteOperation::Insert(value)) }); storage.batch_write(Nodes::column(), nodes)?; diff --git a/crates/storage/src/iter.rs b/crates/storage/src/iter.rs index d06419b386f..de3c15b3051 100644 --- a/crates/storage/src/iter.rs +++ b/crates/storage/src/iter.rs @@ -1,7 +1,10 @@ //! The module defines primitives that allow iterating of the storage. use crate::{ - blueprint::BlueprintInspect, + blueprint::{ + BlueprintCodec, + BlueprintInspect, + }, codec::{ Decode, Encode, @@ -164,9 +167,10 @@ where where P: AsRef<[u8]>, { - let encoder = start.map(|start| { - >::KeyCodec::encode(start) - }); + #[allow(clippy::redundant_closure)] + // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 + let encoder = start + .map(|start| >::KeyCodec::encode(start)); let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); @@ -179,10 +183,9 @@ where ) .map(|res| { res.and_then(|key| { - let key = >::KeyCodec::decode( - key.as_slice(), - ) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let key = + >::KeyCodec::decode(key.as_slice()) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; Ok(key) }) }) @@ -198,9 +201,10 @@ where where P: AsRef<[u8]>, { - let encoder = start.map(|start| { - >::KeyCodec::encode(start) - }); + #[allow(clippy::redundant_closure)] + // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 + let encoder = start + .map(|start| >::KeyCodec::encode(start)); let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); @@ -213,15 +217,12 @@ where ) .map(|val| { val.and_then(|(key, value)| { - let key = >::KeyCodec::decode( - key.as_slice(), - ) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let key = + >::KeyCodec::decode(key.as_slice()) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; let value = - >::ValueCodec::decode( - &value, - ) - .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + >::ValueCodec::decode(&value) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; Ok((key, value)) }) }) diff --git a/crates/storage/src/merkle/sparse.rs b/crates/storage/src/merkle/sparse.rs index c8348466f99..2f11a61ac5e 100644 --- a/crates/storage/src/merkle/sparse.rs +++ b/crates/storage/src/merkle/sparse.rs @@ -4,6 +4,7 @@ use crate::{ Mappable, Result as StorageResult, blueprint::{ + BlueprintCodec, BlueprintInspect, plain::Plain, sparse::{ @@ -114,17 +115,11 @@ where type OwnedValue =
::OwnedValue; } -type KeyCodec = - <
::Blueprint as BlueprintInspect< - Table, - DummyStorage>, - >>::KeyCodec; +type KeyCodec
= + <
::Blueprint as BlueprintCodec
>::KeyCodec; -type ValueCodec = - <
::Blueprint as BlueprintInspect< - Table, - DummyStorage>, - >>::ValueCodec; +type ValueCodec
= + <
::Blueprint as BlueprintCodec
>::ValueCodec; impl TableWithBlueprint for Merkleized
where @@ -134,8 +129,8 @@ where Table::Blueprint: BlueprintInspect>>, { type Blueprint = Sparse< - KeyCodec, - ValueCodec, + KeyCodec
, + ValueCodec
, MerkleMetadata, MerkleData
, KeyConverter
, diff --git a/crates/storage/src/structured_storage.rs b/crates/storage/src/structured_storage.rs index 3ce5e49ff32..cf52bf1b268 100644 --- a/crates/storage/src/structured_storage.rs +++ b/crates/storage/src/structured_storage.rs @@ -14,6 +14,7 @@ use crate::{ StorageSize, StorageWrite, blueprint::{ + BlueprintCodec, BlueprintInspect, BlueprintMutate, SupportsBatching, @@ -362,10 +363,7 @@ where offset: usize, buf: &mut [u8], ) -> Result { - let key_encoder = - >>::KeyCodec::encode( - key, - ); + let key_encoder = >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes(); self.inner.read( key_bytes.as_ref(), @@ -379,10 +377,7 @@ where &self, key: &::Key, ) -> Result>, Self::Error> { - let key_encoder = - >>::KeyCodec::encode( - key, - ); + let key_encoder = >::KeyCodec::encode(key); let key_bytes = key_encoder.as_bytes(); self.inner .get(key_bytes.as_ref(), ::column()) From a95fc1264163c205f2e37c3139822c5421fedcb7 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 24 Sep 2025 18:47:37 +0300 Subject: [PATCH 10/22] clippy --- crates/services/executor/src/storage_access_recorder.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 6bc0a3f006a..2cac2dbfdeb 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -47,17 +47,17 @@ pub(crate) struct ReadsPerContract { } impl ReadsPerContract { - /// Mark some key as accessed without actually reading it. + /// Mark some key as accessed fn mark(&mut self, key: &[u8], column_id: u32) { if column_id == Column::ContractsAssets.as_u32() { - let key = ContractsAssetKey::from_slice(&key).unwrap(); + let key = ContractsAssetKey::from_slice(key).unwrap(); self.per_contract .entry(*key.contract_id()) .or_default() .assets .insert(*key.asset_id()); } else if column_id == Column::ContractsState.as_u32() { - let key = ContractsStateKey::from_slice(&key).unwrap(); + let key = ContractsStateKey::from_slice(key).unwrap(); self.per_contract .entry(*key.contract_id()) .or_default() @@ -116,13 +116,11 @@ where self.storage.get(key, column) } - /// Checks if the value exists in the storage. fn exists(&self, key: &[u8], column: Self::Column) -> StorageResult { self.mark(key, column.id()); self.storage.exists(key, column) } - /// Returns the size of the value in the storage. fn size_of_value( &self, key: &[u8], @@ -132,7 +130,6 @@ where self.storage.size_of_value(key, column) } - /// Reads the value from the storage into the `buf` and returns the whether the value exists. fn read( &self, key: &[u8], From 3ecc453b6394a2a941fd3b15d16d01943eafc08e Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 29 Sep 2025 16:51:34 +0300 Subject: [PATCH 11/22] Hide Mutex inside StorageAccessRecorder --- crates/services/executor/src/executor.rs | 13 ++++--------- .../executor/src/storage_access_recorder.rs | 9 +++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 8349f832eb0..3badf542e3a 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1641,7 +1641,7 @@ where .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; let (recorder, changes) = sub_block_db_commit.into_inner(); - let record = core::mem::take(&mut *recorder.record.lock()).finalize(&changes); + let record = recorder.get_reads().finalize(&changes); storage_tx.commit_changes(changes.clone())?; @@ -1992,14 +1992,9 @@ where Self::update_input_used_gas(predicate_gas_used, tx_id, &mut tx)?; - let ( - StorageAccessRecorder { - storage: storage_tx_recovered, - record, - }, - changes, - ) = sub_block_db_commit.into_inner(); - let record = record.lock().clone().finalize(&changes); + let (recorder, changes) = sub_block_db_commit.into_inner(); + let (storage_tx_recovered, record) = recorder.into_inner(); + let record = record.finalize(&changes); // We always need to update inputs with storage state before execution, // because VM zeroes malleable fields during the execution. diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 2cac2dbfdeb..4ce013a57b7 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -99,6 +99,15 @@ where } } + pub fn into_inner(&self) -> (&S, ReadsPerContract) { + (&self.storage, self.record.lock().clone()) + } + + /// Get the recorded accesses so far. + pub fn get_reads(&self) -> ReadsPerContract { + self.record.lock().clone() + } + /// Mark some key as accessed without actually reading it. fn mark(&self, key: &[u8], column_id: u32) { self.record.lock().mark(key, column_id); From 1850e33c5d14536e9b53b9a98f6039332ad4e420 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 29 Sep 2025 16:51:49 +0300 Subject: [PATCH 12/22] Fix tx updates when it reverts --- crates/services/executor/src/executor.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 3badf542e3a..6da89b49313 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1999,11 +1999,19 @@ where // We always need to update inputs with storage state before execution, // because VM zeroes malleable fields during the execution. self.compute_inputs(tx.inputs_mut(), storage_tx_recovered, &record)?; - self.update_tx_outputs(tx_id, &mut tx, &record, storage_tx, &changes)?; // only commit state changes if execution was a success if !reverted { + self.update_tx_outputs(tx_id, &mut tx, &record, storage_tx, &changes)?; storage_tx.commit_changes(changes.clone())?; + } else { + self.update_tx_outputs( + tx_id, + &mut tx, + &record, + storage_tx, + &Changes::default(), + )?; } Ok((reverted, state, tx, receipts.to_vec())) From 3e6ef07d7ae99ab48ab3487cfecdffb86c765505 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 29 Sep 2025 18:49:07 +0300 Subject: [PATCH 13/22] Use a centralized combiner step instead of in-demand db access --- .../executor/src/contract_state_hash.rs | 26 ++- crates/services/executor/src/executor.rs | 160 ++++-------------- .../executor/src/storage_access_recorder.rs | 117 ++++++++++++- 3 files changed, 152 insertions(+), 151 deletions(-) diff --git a/crates/services/executor/src/contract_state_hash.rs b/crates/services/executor/src/contract_state_hash.rs index e67a160ebea..aefed9524ba 100644 --- a/crates/services/executor/src/contract_state_hash.rs +++ b/crates/services/executor/src/contract_state_hash.rs @@ -1,9 +1,9 @@ #[cfg(feature = "std")] -use std::collections::BTreeSet; +use std::collections::BTreeMap; #[cfg(all(feature = "alloc", not(feature = "std")))] use alloc::{ - collections::BTreeSet, + collections::BTreeMap, vec::Vec, }; @@ -19,29 +19,23 @@ use sha2::{ /// Computes a hash of all contract balances that were read or modified. /// The hash is not dependent on the order of reads or writes. -pub fn compute_balances_hash( - accessed: &BTreeSet, - get_value: impl Fn(&AssetId) -> Result, -) -> Result { +pub fn compute_balances_hash(accessed: &BTreeMap) -> Bytes32 { let mut hasher = Sha256::new(); - for key in accessed { + for (key, value) in accessed { hasher.update(key); - hasher.update(get_value(key)?.to_be_bytes()); + hasher.update(value.to_be_bytes()); } let digest: [u8; 32] = hasher.finalize().into(); - Ok(Bytes32::from(digest)) + Bytes32::from(digest) } /// Computes a hash of all contract state slots that were read or modified. /// The hash is not dependent on the order of reads or writes. -pub fn compute_state_hash( - accessed: &BTreeSet, - get_value: impl Fn(&Bytes32) -> Result>, E>, -) -> Result { +pub fn compute_state_hash(accessed: &BTreeMap>>) -> Bytes32 { let mut hasher = Sha256::new(); - for key in accessed { + for (key, value) in accessed { hasher.update(key); - if let Some(value) = get_value(key)? { + if let Some(value) = value { hasher.update([1u8]); hasher.update((value.len() as Word).to_be_bytes()); hasher.update(&value); @@ -50,5 +44,5 @@ pub fn compute_state_hash( } } let digest: [u8; 32] = hasher.finalize().into(); - Ok(Bytes32::from(digest)) + Bytes32::from(digest) } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 6da89b49313..17e5b4d4bee 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -12,29 +12,19 @@ use crate::{ }, refs::ContractRef, storage_access_recorder::{ - ContractAccesses, + ContractAccessesWithValues, StorageAccessRecorder, }, }; use fuel_core_storage::{ - ContractsAssetKey, - ContractsStateKey, StorageAsMut, StorageAsRef, - blueprint::BlueprintCodec, - codec::Decode, column::Column, - kv_store::{ - KeyValueInspect, - WriteOperation, - }, - structured_storage::TableWithBlueprint, + kv_store::KeyValueInspect, tables::{ Coins, ConsensusParametersVersions, - ContractsAssets, ContractsLatestUtxo, - ContractsState, FuelBlocks, Messages, ProcessedTransactions, @@ -45,7 +35,6 @@ use fuel_core_storage::{ IntoTransaction, Modifiable, ReadTransaction, - ReferenceBytesKey, StorageTransaction, WriteTransaction, }, @@ -1433,8 +1422,7 @@ where } else { Self::check_mint_amount(&mint, execution_data.coinbase)?; - let input = mint.input_contract().clone(); - let mut input = Input::Contract(input); + let mut input = Input::Contract(mint.input_contract().clone()); if self.options.forbid_fake_coins { self.verify_inputs_exist_and_values_match( @@ -1444,12 +1432,6 @@ where )?; } - self.compute_inputs( - core::slice::from_mut(&mut input), - storage_tx, - &Default::default(), - )?; - let (input, output) = self.execute_mint_with_vm( header, coinbase_contract_id, @@ -1641,7 +1623,12 @@ where .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; let (recorder, changes) = sub_block_db_commit.into_inner(); - let record = recorder.get_reads().finalize(&changes); + let (state_before, state_after) = recorder + .get_reads() + .finalize(&storage_tx, &changes) + .map_err(|err| ExecutorError::StorageError(err.to_string()))?; + + self.compute_inputs(core::slice::from_mut(input), storage_tx, &state_before)?; storage_tx.commit_changes(changes.clone())?; @@ -1660,9 +1647,7 @@ where *coinbase_id, core::slice::from_ref(input), outputs.as_mut_slice(), - &record, - storage_tx, - &changes, + &state_after, )?; let Input::Contract(input) = core::mem::take(input) else { return Err(ExecutorError::Other( @@ -1677,27 +1662,17 @@ where Ok((input, output)) } - fn update_tx_outputs( + fn update_tx_outputs( &self, tx_id: TxId, tx: &mut Tx, - record: &BTreeMap, - db: &TxStorageTransaction, - changes: &Changes, + record: &BTreeMap, ) -> ExecutorResult<()> where Tx: ExecutableTransaction, - T: KeyValueInspect, { let mut outputs = core::mem::take(tx.outputs_mut()); - self.compute_state_of_not_utxo_outputs( - tx_id, - tx.inputs(), - &mut outputs, - record, - db, - changes, - )?; + self.compute_state_of_not_utxo_outputs(tx_id, tx.inputs(), &mut outputs, record)?; *tx.outputs_mut() = outputs; Ok(()) } @@ -1994,24 +1969,19 @@ where let (recorder, changes) = sub_block_db_commit.into_inner(); let (storage_tx_recovered, record) = recorder.into_inner(); - let record = record.finalize(&changes); + let (state_before, state_after) = + record.finalize(&storage_tx_recovered, &changes)?; // We always need to update inputs with storage state before execution, // because VM zeroes malleable fields during the execution. - self.compute_inputs(tx.inputs_mut(), storage_tx_recovered, &record)?; + self.compute_inputs(tx.inputs_mut(), storage_tx_recovered, &state_before)?; // only commit state changes if execution was a success if !reverted { - self.update_tx_outputs(tx_id, &mut tx, &record, storage_tx, &changes)?; + self.update_tx_outputs(tx_id, &mut tx, &state_after)?; storage_tx.commit_changes(changes.clone())?; } else { - self.update_tx_outputs( - tx_id, - &mut tx, - &record, - storage_tx, - &Changes::default(), - )?; + self.update_tx_outputs(tx_id, &mut tx, &state_before)?; } Ok((reverted, state, tx, receipts.to_vec())) @@ -2211,7 +2181,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, - record: &BTreeMap, + record: &BTreeMap, ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2252,19 +2222,11 @@ where *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); - let empty = ContractAccesses::default(); + let empty = ContractAccessesWithValues::default(); let for_contract = record.get(contract_id).unwrap_or(&empty); - *balance_root = compute_balances_hash(&for_contract.assets, |key| { - db.storage::() - .get(&ContractsAssetKey::new(contract_id, key)) - .map(|v| v.map_or(0, |c| c.into_owned())) - })?; - *state_root = compute_state_hash(&for_contract.slots, |key| { - db.storage::() - .get(&ContractsStateKey::new(contract_id, key)) - .map(|v| v.map(|c| c.0.clone())) - })?; + *balance_root = compute_balances_hash(&for_contract.assets); + *state_root = compute_state_hash(&for_contract.slots); } _ => {} } @@ -2277,18 +2239,13 @@ where /// Computes all zeroed or variable outputs. /// In production mode, updates the outputs with computed values. /// In validation mode, compares the outputs with computed inputs. - fn compute_state_of_not_utxo_outputs( + fn compute_state_of_not_utxo_outputs( &self, tx_id: TxId, inputs: &[Input], outputs: &mut [Output], - record: &BTreeMap, - db: &TxStorageTransaction, - changes: &Changes, - ) -> ExecutorResult<()> - where - T: KeyValueInspect, - { + record: &BTreeMap, + ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { let contract_id = @@ -2304,71 +2261,12 @@ where }) }; - let empty = ContractAccesses::default(); + let empty = ContractAccessesWithValues::default(); let for_contract = record.get(contract_id).unwrap_or(&empty); - let empty = BTreeMap::default(); - let asset_changes = changes - .get(&Column::ContractsAssets.as_u32()) - .unwrap_or(&empty); - let state_changes = changes - .get(&Column::ContractsState.as_u32()) - .unwrap_or(&empty); - - contract_output.balance_root = compute_balances_hash( - &for_contract.assets, - |key| { - let column_key = ContractsAssetKey::new(contract_id, key); - let column_key = - ReferenceBytesKey::from(column_key.as_ref().to_vec()); - if let Some(changed_value) = - asset_changes.get(&column_key).map(|value| { - match value { - WriteOperation::Insert(v) => { - < - ::Blueprint as BlueprintCodec> - ::ValueCodec::decode(v) - .map_err(|err| ExecutorError::StorageError(err.to_string())) - }, - WriteOperation::Remove => Ok(0), - } - }) - { - return changed_value; - } - db.storage::() - .get(&ContractsAssetKey::new(contract_id, key)) - .map(|v| v.map_or(0, |c| c.into_owned())) - .map_err(|err| ExecutorError::StorageError(err.to_string())) - }, - )?; - contract_output.state_root = compute_state_hash( - &for_contract.slots, - |key| { - let column_key = ContractsStateKey::new(contract_id, key); - let column_key = - ReferenceBytesKey::from(column_key.as_ref().to_vec()); - if let Some(changed_value) = - state_changes.get(&column_key).map(|value| { - match value { - WriteOperation::Insert(v) => { - Some(< - ::Blueprint as BlueprintCodec> - ::ValueCodec::decode(v) - .map_err(|err| ExecutorError::StorageError(err.to_string()))) - }, - WriteOperation::Remove => None, - } - }) - { - return changed_value.transpose(); - } - db.storage::() - .get(&ContractsStateKey::new(contract_id, key)) - .map(|v| v.map(|c| c.0.clone())) - .map_err(|err| ExecutorError::StorageError(err.to_string())) - }, - )?; + contract_output.balance_root = + compute_balances_hash(&for_contract.assets); + contract_output.state_root = compute_state_hash(&for_contract.slots); } } Ok(()) diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 4ce013a57b7..15531e072f1 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -2,11 +2,21 @@ use fuel_core_storage::{ ContractsAssetKey, ContractsStateKey, Result as StorageResult, + StorageAsRef, + StorageInspect, + blueprint::BlueprintCodec, + codec::Decode, column::Column, kv_store::{ KeyValueInspect, StorageColumn, Value, + WriteOperation, + }, + structured_storage::TableWithBlueprint, + tables::{ + ContractsAssets, + ContractsState, }, transactional::Changes, }; @@ -33,6 +43,7 @@ use alloc::{ BTreeSet, }, sync::Arc, + vec::Vec, }; #[derive(Debug, Clone, Default)] @@ -41,6 +52,12 @@ pub(crate) struct ContractAccesses { pub slots: BTreeSet, } +#[derive(Debug, Clone, Default)] +pub(crate) struct ContractAccessesWithValues { + pub assets: BTreeMap, + pub slots: BTreeMap>>, +} + #[derive(Debug, Clone, Default)] pub(crate) struct ReadsPerContract { pub per_contract: BTreeMap, @@ -66,17 +83,109 @@ impl ReadsPerContract { } } - /// Go through the changes of a transaction and mark all changed keys as accessed. - pub(crate) fn finalize( + /// Returns slot values before and after applying the changes + pub(crate) fn finalize( mut self, + storage: S, changes: &Changes, - ) -> BTreeMap { + ) -> StorageResult<( + BTreeMap, + BTreeMap, + )> + where + S: StorageInspect + + StorageInspect + + StorageAsRef, + { + // Mark all changed keys as accessed for (change_column, change) in changes { for (key, _) in change.iter() { self.mark(key, *change_column); } } - self.per_contract + + // Fetch original values from the storage + let before: BTreeMap = self + .per_contract + .into_iter() + .map(|(contract_id, accesses)| { + let assets = accesses + .assets + .into_iter() + .map(|asset_id| { + let value = storage + .storage::() + .get(&ContractsAssetKey::new(&contract_id, &asset_id)) + .map(|v| v.map_or(0, |c| c.into_owned()))?; + Ok((asset_id, value)) + }) + .collect::>()?; + let slots = accesses + .slots + .into_iter() + .map(|slot_key| { + let value = storage + .storage::() + .get(&ContractsStateKey::new(&contract_id, &slot_key)) + .map(|v| v.map(|c| c.0.clone()))?; + Ok((slot_key, value)) + }) + .collect::>()?; + Ok((contract_id, ContractAccessesWithValues { assets, slots })) + }) + .collect::>()?; + + // Update final values from the changes + let mut after: BTreeMap = before.clone(); + + for (key, value) in changes + .get(&Column::ContractsAssets.as_u32()) + .iter() + .flat_map(|it| it.iter()) + { + let key = ContractsAssetKey::from_slice(key).unwrap(); + let entry = after + .get_mut(key.contract_id()) + .expect("Inserted by marking step above") + .assets + .get_mut(key.asset_id()) + .expect("Inserted by marking step above"); + *entry = match value { + WriteOperation::Insert(v) => { + < + ::Blueprint as BlueprintCodec> + ::ValueCodec::decode(v)? + } + WriteOperation::Remove => { + 0 + } + } + } + for (key, value) in changes + .get(&Column::ContractsState.as_u32()) + .iter() + .flat_map(|it| it.iter()) + { + let key = ContractsStateKey::from_slice(key).unwrap(); + let entry = after + .get_mut(key.contract_id()) + .expect("Inserted by marking step above") + .slots + .get_mut(key.state_key()) + .expect("Inserted by marking step above"); + *entry = match value { + WriteOperation::Insert(v) => { + Some(< + ::Blueprint as BlueprintCodec> + ::ValueCodec::decode(v)?) + } + WriteOperation::Remove => { + None + } + }; + } + + Ok((before, after)) } } From f54ea2210c46fb2485e9efe7a023e73224a0294f Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 29 Sep 2025 18:55:45 +0300 Subject: [PATCH 14/22] Clippy --- crates/services/executor/src/contract_state_hash.rs | 2 +- crates/services/executor/src/executor.rs | 4 ++-- crates/services/executor/src/storage_access_recorder.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/services/executor/src/contract_state_hash.rs b/crates/services/executor/src/contract_state_hash.rs index aefed9524ba..55757d2572f 100644 --- a/crates/services/executor/src/contract_state_hash.rs +++ b/crates/services/executor/src/contract_state_hash.rs @@ -38,7 +38,7 @@ pub fn compute_state_hash(accessed: &BTreeMap>>) -> Byte if let Some(value) = value { hasher.update([1u8]); hasher.update((value.len() as Word).to_be_bytes()); - hasher.update(&value); + hasher.update(value); } else { hasher.update([0u8]); } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 17e5b4d4bee..83a0e7a83f8 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1968,9 +1968,9 @@ where Self::update_input_used_gas(predicate_gas_used, tx_id, &mut tx)?; let (recorder, changes) = sub_block_db_commit.into_inner(); - let (storage_tx_recovered, record) = recorder.into_inner(); + let (storage_tx_recovered, record) = recorder.as_inner(); let (state_before, state_after) = - record.finalize(&storage_tx_recovered, &changes)?; + record.finalize(storage_tx_recovered, &changes)?; // We always need to update inputs with storage state before execution, // because VM zeroes malleable fields during the execution. diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 15531e072f1..71c23b483e8 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -208,7 +208,7 @@ where } } - pub fn into_inner(&self) -> (&S, ReadsPerContract) { + pub fn as_inner(&self) -> (&S, ReadsPerContract) { (&self.storage, self.record.lock().clone()) } From cb4268e2aa8e4de4fc2cb1cd81ec3a738d34aa9e Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Tue, 30 Sep 2025 11:59:18 +0300 Subject: [PATCH 15/22] Simplify type conversions --- crates/services/executor/src/contract_state_hash.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/services/executor/src/contract_state_hash.rs b/crates/services/executor/src/contract_state_hash.rs index 55757d2572f..4ca07cf0758 100644 --- a/crates/services/executor/src/contract_state_hash.rs +++ b/crates/services/executor/src/contract_state_hash.rs @@ -25,8 +25,7 @@ pub fn compute_balances_hash(accessed: &BTreeMap) -> Bytes32 { hasher.update(key); hasher.update(value.to_be_bytes()); } - let digest: [u8; 32] = hasher.finalize().into(); - Bytes32::from(digest) + Bytes32::new(hasher.finalize().into()) } /// Computes a hash of all contract state slots that were read or modified. @@ -43,6 +42,5 @@ pub fn compute_state_hash(accessed: &BTreeMap>>) -> Byte hasher.update([0u8]); } } - let digest: [u8; 32] = hasher.finalize().into(); - Bytes32::from(digest) + Bytes32::new(hasher.finalize().into()) } From c1ec86f5bce6f00e11e008468a6da264a0e6a276 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Tue, 30 Sep 2025 11:59:49 +0300 Subject: [PATCH 16/22] Fix mint tests after roots are properly updated --- crates/fuel-core/src/executor.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 5cafbc77a27..de847233ac2 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -488,21 +488,32 @@ mod tests { assert!(expected_fee_amount_1 > 0); let first_mint; + let mut h = Sha256::new(); + h.update(consensus_parameters.base_asset_id().as_ref()); + h.update(&0u64.to_be_bytes()); + let input_balances_hash = Bytes32::new(h.finalize().into()); + + let mut h = Sha256::new(); + h.update(consensus_parameters.base_asset_id().as_ref()); + h.update(&expected_fee_amount_1.to_be_bytes()); + let output_balances_hash = Bytes32::new(h.finalize().into()); + + let empty_hash = Bytes32::new(Sha256::digest(&[]).into()); + if let Some(mint) = block.transactions()[1].as_mint() { assert_eq!( mint.tx_pointer(), &TxPointer::new(*block.header().height(), 1) ); - let empty_sha: [u8; 32] = Sha256::digest([]).into(); assert_eq!(mint.mint_asset_id(), &AssetId::BASE); assert_eq!(mint.mint_amount(), &expected_fee_amount_1); assert_eq!(mint.input_contract().contract_id, recipient); - assert_eq!(mint.input_contract().balance_root, Bytes32::new(empty_sha)); - assert_eq!(mint.input_contract().state_root, Bytes32::new(empty_sha)); + assert_eq!(mint.input_contract().balance_root, input_balances_hash); + assert_eq!(mint.input_contract().state_root, empty_hash); assert_eq!(mint.input_contract().utxo_id, UtxoId::default()); assert_eq!(mint.input_contract().tx_pointer, TxPointer::default()); - assert_ne!(mint.output_contract().balance_root, Bytes32::new(empty_sha)); - assert_eq!(mint.output_contract().state_root, Bytes32::new(empty_sha)); + assert_eq!(mint.output_contract().balance_root, output_balances_hash); + assert_eq!(mint.output_contract().state_root, empty_hash); assert_eq!(mint.output_contract().input_index, 0); first_mint = mint.clone(); } else { From f48fbeded2d6e11aa9978c4638ae7d32a38e7b8f Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Tue, 30 Sep 2025 11:23:14 +0300 Subject: [PATCH 17/22] Add a trait to iterate through changes to storage and use it --- .../executor/src/storage_access_recorder.rs | 54 ++-- crates/storage/src/iter.rs | 276 ++++++++++++++++++ crates/storage/src/iter/changes_iterator.rs | 91 ++++++ crates/storage/src/kv_store.rs | 2 + 4 files changed, 386 insertions(+), 37 deletions(-) diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 71c23b483e8..8819aa24db4 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -4,21 +4,24 @@ use fuel_core_storage::{ Result as StorageResult, StorageAsRef, StorageInspect, - blueprint::BlueprintCodec, - codec::Decode, column::Column, + iter::{ + IteratorOverTableWrites, + changes_iterator::ChangesIterator, + }, kv_store::{ KeyValueInspect, StorageColumn, Value, - WriteOperation, }, - structured_storage::TableWithBlueprint, tables::{ ContractsAssets, ContractsState, }, - transactional::Changes, + transactional::{ + Changes, + StorageChanges, + }, }; use fuel_core_types::fuel_tx::{ AssetId, @@ -138,51 +141,28 @@ impl ReadsPerContract { // Update final values from the changes let mut after: BTreeMap = before.clone(); - for (key, value) in changes - .get(&Column::ContractsAssets.as_u32()) - .iter() - .flat_map(|it| it.iter()) - { - let key = ContractsAssetKey::from_slice(key).unwrap(); + let sc = StorageChanges::Changes(changes.clone()); + let ci = ChangesIterator::new(&sc); + + for item in ci.iter_all::(None) { + let (key, value) = item?; let entry = after .get_mut(key.contract_id()) .expect("Inserted by marking step above") .assets .get_mut(key.asset_id()) .expect("Inserted by marking step above"); - *entry = match value { - WriteOperation::Insert(v) => { - < - ::Blueprint as BlueprintCodec> - ::ValueCodec::decode(v)? - } - WriteOperation::Remove => { - 0 - } - } + *entry = value.unwrap_or(0); } - for (key, value) in changes - .get(&Column::ContractsState.as_u32()) - .iter() - .flat_map(|it| it.iter()) - { - let key = ContractsStateKey::from_slice(key).unwrap(); + for item in ci.iter_all::(None) { + let (key, value) = item?; let entry = after .get_mut(key.contract_id()) .expect("Inserted by marking step above") .slots .get_mut(key.state_key()) .expect("Inserted by marking step above"); - *entry = match value { - WriteOperation::Insert(v) => { - Some(< - ::Blueprint as BlueprintCodec> - ::ValueCodec::decode(v)?) - } - WriteOperation::Remove => { - None - } - }; + *entry = value.map(|data| data.0); } Ok((before, after)) diff --git a/crates/storage/src/iter.rs b/crates/storage/src/iter.rs index de3c15b3051..6917c52d77d 100644 --- a/crates/storage/src/iter.rs +++ b/crates/storage/src/iter.rs @@ -12,8 +12,10 @@ use crate::{ }, kv_store::{ KVItem, + KVWriteItem, KeyItem, KeyValueInspect, + WriteOperation, }, structured_storage::TableWithBlueprint, transactional::ReferenceBytesKey, @@ -126,6 +128,58 @@ where } } +/// A trait for iterating over writes to a store. +#[impl_tools::autoimpl(for &T, &mut T, Box)] +pub trait IterableStoreWrites: KeyValueInspect { + /// Returns an iterator over the values in the storage. + fn iter_store_writes( + &self, + column: Self::Column, + prefix: Option<&[u8]>, + start: Option<&[u8]>, + direction: IterDirection, + ) -> BoxedIter; + + /// Returns an iterator over keys in the storage. + fn iter_store_write_keys( + &self, + column: Self::Column, + prefix: Option<&[u8]>, + start: Option<&[u8]>, + direction: IterDirection, + ) -> BoxedIter; +} + +#[cfg(feature = "std")] +impl IterableStoreWrites for std::sync::Arc +where + T: IterableStoreWrites, +{ + fn iter_store_writes( + &self, + column: Self::Column, + prefix: Option<&[u8]>, + start: Option<&[u8]>, + direction: IterDirection, + ) -> BoxedIter { + use core::ops::Deref; + self.deref() + .iter_store_writes(column, prefix, start, direction) + } + + fn iter_store_write_keys( + &self, + column: Self::Column, + prefix: Option<&[u8]>, + start: Option<&[u8]>, + direction: IterDirection, + ) -> BoxedIter { + use core::ops::Deref; + self.deref() + .iter_store_write_keys(column, prefix, start, direction) + } +} + /// A trait for iterating over the `Mappable` table. pub trait IterableTable where @@ -341,6 +395,228 @@ pub trait IteratorOverTable { impl IteratorOverTable for S {} +/// A trait for iterating over writes to a `Mappable` table. +pub trait IterableTableWrites +where + M: Mappable, +{ + /// Returns an iterator over the all keys in the table with a prefix after a specific start key. + fn iter_table_writes_keys

( + &self, + prefix: Option

, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter> + where + P: AsRef<[u8]>; + + /// Returns an iterator over the all entries in the table with a prefix after a specific start key. + #[allow(clippy::type_complexity)] + fn iter_table_writes

( + &self, + prefix: Option

, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter)>> + where + P: AsRef<[u8]>; +} + +impl IterableTableWrites for S +where + M: TableWithBlueprint, + M::Blueprint: BlueprintInspect, + S: IterableStoreWrites, +{ + fn iter_table_writes_keys

( + &self, + prefix: Option

, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter> + where + P: AsRef<[u8]>, + { + #[allow(clippy::redundant_closure)] + // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 + let encoder = start + .map(|start| >::KeyCodec::encode(start)); + + let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); + + IterableStoreWrites::iter_store_write_keys( + self, + M::column(), + prefix.as_ref().map(|p| p.as_ref()), + start.as_ref().map(|cow| cow.as_ref()), + direction.unwrap_or_default(), + ) + .map(|res| { + res.and_then(|key| { + let key = + >::KeyCodec::decode(key.as_slice()) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + Ok(key) + }) + }) + .into_boxed() + } + + fn iter_table_writes

( + &self, + prefix: Option

, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter)>> + where + P: AsRef<[u8]>, + { + #[allow(clippy::redundant_closure)] + // false positive: https://github.com/rust-lang/rust-clippy/issues/14215 + let encoder = start + .map(|start| >::KeyCodec::encode(start)); + + let start = encoder.as_ref().map(|encoder| encoder.as_bytes()); + + IterableStoreWrites::iter_store_writes( + self, + M::column(), + prefix.as_ref().map(|p| p.as_ref()), + start.as_ref().map(|cow| cow.as_ref()), + direction.unwrap_or_default(), + ) + .map(|val| { + val.and_then(|(key, value)| { + let key = + >::KeyCodec::decode(key.as_slice()) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?; + let value = match value { + WriteOperation::Insert(value) => Some( + >::ValueCodec::decode(&value) + .map_err(|e| crate::Error::Codec(anyhow::anyhow!(e)))?, + ), + WriteOperation::Remove => None, + }; + Ok((key, value)) + }) + }) + .into_boxed() + } +} + +/// A helper trait to provide a user-friendly API over table iteration. +#[allow(clippy::type_complexity)] +pub trait IteratorOverTableWrites { + /// Returns an iterator over the all keys in the table. + fn iter_all_keys( + &self, + direction: Option, + ) -> BoxedIter> + where + M: Mappable, + Self: IterableTableWrites, + { + self.iter_all_filtered_keys::(None, None, direction) + } + + /// Returns an iterator over the all keys in the table with the specified prefix. + fn iter_all_by_prefix_keys( + &self, + prefix: Option

, + ) -> BoxedIter> + where + M: Mappable, + P: AsRef<[u8]>, + Self: IterableTableWrites, + { + self.iter_all_filtered_keys::(prefix, None, None) + } + + /// Returns an iterator over the all keys in the table after a specific start key. + fn iter_all_by_start_keys( + &self, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter> + where + M: Mappable, + Self: IterableTableWrites, + { + self.iter_all_filtered_keys::(None, start, direction) + } + + /// Returns an iterator over the all keys in the table with a prefix after a specific start key. + fn iter_all_filtered_keys( + &self, + prefix: Option

, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter> + where + M: Mappable, + P: AsRef<[u8]>, + Self: IterableTableWrites, + { + self.iter_table_writes_keys(prefix, start, direction) + } + + /// Returns an iterator over the all entries in the table. + fn iter_all( + &self, + direction: Option, + ) -> BoxedIter)>> + where + M: Mappable, + Self: IterableTableWrites, + { + self.iter_all_filtered::(None, None, direction) + } + + /// Returns an iterator over the all entries in the table with the specified prefix. + fn iter_all_by_prefix( + &self, + prefix: Option

, + ) -> BoxedIter)>> + where + M: Mappable, + P: AsRef<[u8]>, + Self: IterableTableWrites, + { + self.iter_all_filtered::(prefix, None, None) + } + + /// Returns an iterator over the all entries in the table after a specific start key. + #[allow(clippy::type_complexity)] + fn iter_all_by_start( + &self, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter)>> + where + M: Mappable, + Self: IterableTableWrites, + { + self.iter_all_filtered::(None, start, direction) + } + + /// Returns an iterator over the all entries in the table with a prefix after a specific start key. + fn iter_all_filtered( + &self, + prefix: Option

, + start: Option<&M::Key>, + direction: Option, + ) -> BoxedIter)>> + where + M: Mappable, + P: AsRef<[u8]>, + Self: IterableTableWrites, + { + self.iter_table_writes(prefix, start, direction) + } +} + +impl IteratorOverTableWrites for S {} + /// Returns an iterator over the values in the `BTreeMap`. pub fn iterator<'a, V>( tree: &'a BTreeMap, diff --git a/crates/storage/src/iter/changes_iterator.rs b/crates/storage/src/iter/changes_iterator.rs index bf346ee314f..2b828d1fdd8 100644 --- a/crates/storage/src/iter/changes_iterator.rs +++ b/crates/storage/src/iter/changes_iterator.rs @@ -6,9 +6,11 @@ use crate::{ IntoBoxedIter, IterDirection, IterableStore, + IterableStoreWrites, }, kv_store::{ KVItem, + KVWriteItem, KeyValueInspect, StorageColumn, Value, @@ -183,3 +185,92 @@ where } } } + +impl IterableStoreWrites for ChangesIterator<'_, Column> +where + Column: StorageColumn, +{ + fn iter_store_writes( + &self, + column: Self::Column, + prefix: Option<&[u8]>, + start: Option<&[u8]>, + direction: IterDirection, + ) -> BoxedIter { + match self.changes { + StorageChanges::Changes(changes) => { + if let Some(tree) = changes.get(&column.id()) { + crate::iter::iterator(tree, prefix, start, direction) + .map(|(key, value)| (key.clone().into(), value.clone())) + .map(Ok) + .into_boxed() + } else { + core::iter::empty().into_boxed() + } + } + StorageChanges::ChangesList(changes_list) => { + let column = column.id(); + + let mut iterators_list = Vec::with_capacity(changes_list.len()); + + for changes in changes_list.iter() { + let iter = changes.get(&column).map(|tree| { + crate::iter::iterator(tree, prefix, start, direction) + .map(|(key, value)| (key.clone().into(), value.clone())) + .map(Ok) + }); + if let Some(iter) = iter { + iterators_list.push(iter); + } + } + + iterators_list.into_iter().flatten().into_boxed() + } + } + } + + fn iter_store_write_keys( + &self, + column: Self::Column, + prefix: Option<&[u8]>, + start: Option<&[u8]>, + direction: IterDirection, + ) -> BoxedIter { + match self.changes { + StorageChanges::Changes(changes) => { + if let Some(tree) = changes.get(&column.id()) { + crate::iter::iterator(tree, prefix, start, direction) + .filter_map(|(key, value)| match value { + WriteOperation::Insert(_) => Some(key.clone().into()), + WriteOperation::Remove => None, + }) + .map(Ok) + .into_boxed() + } else { + core::iter::empty().into_boxed() + } + } + StorageChanges::ChangesList(changes_list) => { + let column = column.id(); + + let mut iterators_list = Vec::with_capacity(changes_list.len()); + + for changes in changes_list.iter() { + let iter = changes.get(&column).map(|tree| { + crate::iter::iterator(tree, prefix, start, direction) + .filter_map(|(key, value)| match value { + WriteOperation::Insert(_) => Some(key.clone().into()), + WriteOperation::Remove => None, + }) + .map(Ok) + }); + if let Some(iter) = iter { + iterators_list.push(iter); + } + } + + iterators_list.into_iter().flatten().into_boxed() + } + } + } +} diff --git a/crates/storage/src/kv_store.rs b/crates/storage/src/kv_store.rs index 78930db578a..888b798cab1 100644 --- a/crates/storage/src/kv_store.rs +++ b/crates/storage/src/kv_store.rs @@ -19,6 +19,8 @@ pub type Value = alloc::sync::Arc<[u8]>; /// The pair of key and value from the storage. pub type KVItem = StorageResult<(Key, Value)>; +/// The pair of key and optional value from the storage. +pub type KVWriteItem = StorageResult<(Key, WriteOperation)>; /// The key from the storage. pub type KeyItem = StorageResult; From de3a48ebb4186812ec480d3d15035bddba876efe Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Tue, 30 Sep 2025 22:32:32 +0300 Subject: [PATCH 18/22] clippy --- crates/fuel-core/src/executor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index de847233ac2..2146ecd1fbc 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -490,15 +490,15 @@ mod tests { let mut h = Sha256::new(); h.update(consensus_parameters.base_asset_id().as_ref()); - h.update(&0u64.to_be_bytes()); + h.update(0u64.to_be_bytes()); let input_balances_hash = Bytes32::new(h.finalize().into()); let mut h = Sha256::new(); h.update(consensus_parameters.base_asset_id().as_ref()); - h.update(&expected_fee_amount_1.to_be_bytes()); + h.update(expected_fee_amount_1.to_be_bytes()); let output_balances_hash = Bytes32::new(h.finalize().into()); - let empty_hash = Bytes32::new(Sha256::digest(&[]).into()); + let empty_hash = Bytes32::new(Sha256::digest([]).into()); if let Some(mint) = block.transactions()[1].as_mint() { assert_eq!( From 00fed41300368e420fa5d4c9cee2c0bcd4ec5b06 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Tue, 30 Sep 2025 23:55:45 +0300 Subject: [PATCH 19/22] Fix typos (ci) --- .../consensus_module/poa/src/service_test/trigger_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs index e989ea26d9a..2d167fd1741 100644 --- a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs @@ -474,7 +474,7 @@ async fn interval_trigger_produces_blocks_in_the_future_when_time_rewinds() { // The fist block should be produced after the given block time. assert_eq!(first_block_time, start_time + block_time.as_secs()); - // Even though the real time clock rewinded, the second block is produced with a future timestamp + // Even though the real time clock rewound, the second block is produced with a future timestamp // similarly to how it works when time is lagging. assert_eq!(second_block_time, start_time + block_time.as_secs() * 2); } From 2d64df0564cff832afd3a8b65a2dbc48c914669a Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 1 Oct 2025 00:26:48 +0300 Subject: [PATCH 20/22] More typo fixes (ci) --- tests/tests/da_compression.rs | 2 +- tests/tests/relayer.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests/da_compression.rs b/tests/tests/da_compression.rs index deba78d3739..87e54b0de1c 100644 --- a/tests/tests/da_compression.rs +++ b/tests/tests/da_compression.rs @@ -264,7 +264,7 @@ async fn da_compression__starts_and_compresses_blocks_correctly_from_empty_datab } #[tokio::test] -async fn da_compression__db_can_be_rewinded() { +async fn da_compression__db_can_be_rewound() { // given let rollback_target_height = 0; let blocks_to_produce = 10; diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs index b662ba7af92..6e98f030644 100644 --- a/tests/tests/relayer.rs +++ b/tests/tests/relayer.rs @@ -721,7 +721,7 @@ async fn balances_and_coins_to_spend_never_return_retryable_messages() { } #[tokio::test] -async fn relayer_db_can_be_rewinded() { +async fn relayer_db_can_be_rewound() { // Given let rollback_target_height = 0; let num_da_blocks = 10; From ea2597c9b27fa39e73af29a51367ea37e6391b4f Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 6 Oct 2025 16:51:23 +0300 Subject: [PATCH 21/22] Expose storage changes to slots and balances from the executor --- .../src/graphql_api/worker_service.rs | 2 +- .../consensus_module/poa/src/service.rs | 2 + .../consensus_module/poa/src/service_test.rs | 3 + .../service_test/manually_produce_tests.rs | 1 + .../poa/src/service_test/trigger_tests.rs | 5 +- crates/services/executor/src/executor.rs | 169 ++++++++++-------- .../executor/src/storage_access_recorder.rs | 36 ++-- crates/services/producer/src/mocks.rs | 3 + .../wasm-executor/src/utils.rs | 6 + crates/types/src/services/executor.rs | 37 ++-- 10 files changed, 156 insertions(+), 108 deletions(-) diff --git a/crates/fuel-core/src/graphql_api/worker_service.rs b/crates/fuel-core/src/graphql_api/worker_service.rs index 9157c7bd4f4..d23ac5c0a1d 100644 --- a/crates/fuel-core/src/graphql_api/worker_service.rs +++ b/crates/fuel-core/src/graphql_api/worker_service.rs @@ -453,7 +453,7 @@ fn persist_transaction_status( where T: OffChainDatabaseTransaction, { - for TransactionExecutionStatus { id, result } in import_result.tx_status.iter() { + for TransactionExecutionStatus { id, result, .. } in import_result.tx_status.iter() { let status = from_executor_to_status(&import_result.sealed_block.entity, result.clone()); diff --git a/crates/services/consensus_module/poa/src/service.rs b/crates/services/consensus_module/poa/src/service.rs index 6545f32f653..91ecf186ade 100644 --- a/crates/services/consensus_module/poa/src/service.rs +++ b/crates/services/consensus_module/poa/src/service.rs @@ -378,6 +378,7 @@ where skipped_transactions, tx_status, events, + .. }, changes, ) = self @@ -438,6 +439,7 @@ where skipped_transactions, tx_status, events, + .. }, changes, ) = self diff --git a/crates/services/consensus_module/poa/src/service_test.rs b/crates/services/consensus_module/poa/src/service_test.rs index f3df747eb38..e3a185e24de 100644 --- a/crates/services/consensus_module/poa/src/service_test.rs +++ b/crates/services/consensus_module/poa/src/service_test.rs @@ -146,6 +146,7 @@ impl TestContextBuilder { block: Default::default(), skipped_transactions: Default::default(), tx_status: Default::default(), + tx_storage_states: Default::default(), events: Default::default(), }, Default::default(), @@ -329,6 +330,7 @@ impl BlockProducer for FakeBlockProducer { block: Default::default(), skipped_transactions: Default::default(), tx_status: Default::default(), + tx_storage_states: Default::default(), events: Default::default(), }, Default::default(), @@ -348,6 +350,7 @@ impl BlockProducer for FakeBlockProducer { block: block.clone(), skipped_transactions: Default::default(), tx_status: Default::default(), + tx_storage_states: Default::default(), events: Default::default(), }, Default::default(), diff --git a/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs b/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs index 555ec50680e..b8968cce35d 100644 --- a/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs @@ -89,6 +89,7 @@ async fn can_manually_produce_block( block, skipped_transactions: Default::default(), tx_status: Default::default(), + tx_storage_states: Default::default(), events: Default::default(), }, Default::default(), diff --git a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs index 2d167fd1741..48ef0027288 100644 --- a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs @@ -113,7 +113,10 @@ impl DefaultContext { Ok(UncommittedResult::new( ExecutionResult { block, - ..Default::default() + skipped_transactions: Default::default(), + tx_status: Default::default(), + tx_storage_states: Default::default(), + events: Default::default(), }, Default::default(), )) diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 83a0e7a83f8..5338499edb0 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -11,10 +11,7 @@ use crate::{ TransactionsSource, }, refs::ContractRef, - storage_access_recorder::{ - ContractAccessesWithValues, - StorageAccessRecorder, - }, + storage_access_recorder::StorageAccessRecorder, }; use fuel_core_storage::{ StorageAsMut, @@ -139,11 +136,13 @@ use fuel_core_types::{ services::{ block_producer::Components, executor::{ + ContractAccessesWithValues, Error as ExecutorError, Event as ExecutorEvent, ExecutionResult, ForcedTransactionFailure, Result as ExecutorResult, + StorageAccessesWithValues, TransactionExecutionResult, TransactionExecutionStatus, TransactionValidityError, @@ -165,16 +164,10 @@ use tracing::{ }; #[cfg(feature = "std")] -use std::{ - borrow::Cow, - collections::BTreeMap, -}; +use std::borrow::Cow; #[cfg(not(feature = "std"))] -use alloc::{ - borrow::Cow, - collections::BTreeMap, -}; +use alloc::borrow::Cow; #[cfg(feature = "alloc")] use alloc::{ @@ -321,6 +314,7 @@ pub struct ExecutionData { found_mint: bool, message_ids: Vec, tx_status: Vec, + tx_storage_states: Vec<(ContractAccessesWithValues, ContractAccessesWithValues)>, events: Vec, changes: Changes, pub skipped_transactions: Vec<(TxId, ExecutorError)>, @@ -329,19 +323,7 @@ pub struct ExecutionData { impl ExecutionData { pub fn new() -> Self { - ExecutionData { - coinbase: 0, - used_gas: 0, - used_size: 0, - tx_count: 0, - found_mint: false, - message_ids: Vec::new(), - tx_status: Vec::new(), - events: Vec::new(), - changes: Default::default(), - skipped_transactions: Vec::new(), - event_inbox_root: Default::default(), - } + Self::default() } } @@ -420,6 +402,7 @@ where changes, events, tx_status, + tx_storage_states, skipped_transactions, coinbase, used_gas, @@ -447,6 +430,7 @@ where block, skipped_transactions, tx_status, + tx_storage_states, events, }; @@ -877,12 +861,12 @@ where gas_price: Word, coinbase_contract_id: ContractId, memory: &mut MemoryInstance, - ) -> ExecutorResult<()> + ) -> ExecutorResult<(ContractAccessesWithValues, ContractAccessesWithValues)> where W: KeyValueInspect, { let tx_count = execution_data.tx_count; - let tx = { + let (tx, storage_before, storage_after) = { let mut tx_st_transaction = storage_tx .write_transaction() .with_policy(ConflictPolicy::Overwrite); @@ -906,7 +890,7 @@ where .checked_add(1) .ok_or(ExecutorError::TooManyTransactions)?; - Ok(()) + Ok((storage_before, storage_after)) } #[tracing::instrument(skip_all)] @@ -1274,7 +1258,11 @@ where execution_data: &mut ExecutionData, storage_tx: &mut TxStorageTransaction, memory: &mut MemoryInstance, - ) -> ExecutorResult + ) -> ExecutorResult<( + Transaction, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where T: KeyValueInspect, { @@ -1405,7 +1393,11 @@ where gas_price: Word, execution_data: &mut ExecutionData, storage_tx: &mut TxStorageTransaction, - ) -> ExecutorResult + ) -> ExecutorResult<( + Transaction, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where T: KeyValueInspect, { @@ -1417,36 +1409,41 @@ where let (mut mint, _) = checked_mint.into(); Self::check_gas_price(&mint, gas_price)?; - if mint.input_contract().contract_id == ContractId::zeroed() { - Self::verify_mint_for_empty_contract(&mint)?; - } else { - Self::check_mint_amount(&mint, execution_data.coinbase)?; + let (storage_before, storage_after) = + if mint.input_contract().contract_id == ContractId::zeroed() { + Self::verify_mint_for_empty_contract(&mint)?; + (Default::default(), Default::default()) + } else { + Self::check_mint_amount(&mint, execution_data.coinbase)?; - let mut input = Input::Contract(mint.input_contract().clone()); + let mut input = Input::Contract(mint.input_contract().clone()); - if self.options.forbid_fake_coins { - self.verify_inputs_exist_and_values_match( - storage_tx, - core::slice::from_ref(&input), - header.da_height, - )?; - } + if self.options.forbid_fake_coins { + self.verify_inputs_exist_and_values_match( + storage_tx, + core::slice::from_ref(&input), + header.da_height, + )?; + } - let (input, output) = self.execute_mint_with_vm( - header, - coinbase_contract_id, - execution_data, - storage_tx, - &coinbase_id, - &mut mint, - &mut input, - )?; + let (input, output, storage_before, storage_after) = self + .execute_mint_with_vm( + header, + coinbase_contract_id, + execution_data, + storage_tx, + &coinbase_id, + &mut mint, + &mut input, + )?; - *mint.input_contract_mut() = input; - *mint.output_contract_mut() = output; - } + *mint.input_contract_mut() = input; + *mint.output_contract_mut() = output; + (storage_before, storage_after) + }; - Self::store_mint_tx(mint, execution_data, coinbase_id, storage_tx) + let tx = Self::store_mint_tx(mint, execution_data, coinbase_id, storage_tx)?; + Ok((tx, storage_before, storage_after)) } #[allow(clippy::too_many_arguments)] @@ -1459,7 +1456,11 @@ where execution_data: &mut ExecutionData, storage_tx: &mut TxStorageTransaction, memory: &mut MemoryInstance, - ) -> ExecutorResult + ) -> ExecutorResult<( + Transaction, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where Tx: ExecutableTransaction + Cacheable + Send + Sync + 'static, ::Metadata: CheckedMetadataTrait + Send + Sync, @@ -1471,14 +1472,15 @@ where checked_tx = self.extra_tx_checks(checked_tx, header, storage_tx, memory)?; } - let (reverted, state, tx, receipts) = self.attempt_tx_execution_with_vm( - checked_tx, - header, - coinbase_contract_id, - gas_price, - storage_tx, - memory, - )?; + let (reverted, state, tx, receipts, storage_before, storage_after) = self + .attempt_tx_execution_with_vm( + checked_tx, + header, + coinbase_contract_id, + gas_price, + storage_tx, + memory, + )?; self.spend_input_utxos(tx.inputs(), storage_tx, reverted, execution_data)?; @@ -1505,7 +1507,7 @@ where tx_id, )?; - Ok(tx.into()) + Ok((tx.into(), storage_before, storage_after)) } fn check_mint_amount(mint: &Mint, expected_amount: u64) -> ExecutorResult<()> { @@ -1596,7 +1598,12 @@ where coinbase_id: &TxId, mint: &mut Mint, input: &mut Input, - ) -> ExecutorResult<(input::contract::Contract, output::contract::Contract)> + ) -> ExecutorResult<( + input::contract::Contract, + output::contract::Contract, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where T: KeyValueInspect, { @@ -1659,14 +1666,14 @@ where "The output of the `Mint` transaction is not a contract".to_string(), )) }; - Ok((input, output)) + Ok((input, output, state_before, state_after)) } fn update_tx_outputs( &self, tx_id: TxId, tx: &mut Tx, - record: &BTreeMap, + record: &ContractAccessesWithValues, ) -> ExecutorResult<()> where Tx: ExecutableTransaction, @@ -1840,7 +1847,14 @@ where gas_price: Word, storage_tx: &mut TxStorageTransaction, memory: &mut MemoryInstance, - ) -> ExecutorResult<(bool, ProgramState, Tx, Vec)> + ) -> ExecutorResult<( + bool, + ProgramState, + Tx, + Vec, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where Tx: ExecutableTransaction + Cacheable, ::Metadata: CheckedMetadataTrait + Send + Sync, @@ -1984,7 +1998,14 @@ where self.update_tx_outputs(tx_id, &mut tx, &state_before)?; } - Ok((reverted, state, tx, receipts.to_vec())) + Ok(( + reverted, + state, + tx, + receipts.to_vec(), + state_before, + state_after, + )) } fn verify_inputs_exist_and_values_match( @@ -2181,7 +2202,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, - record: &BTreeMap, + record: &ContractAccessesWithValues, ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2222,7 +2243,7 @@ where *utxo_id = *utxo_info.utxo_id(); *tx_pointer = utxo_info.tx_pointer(); - let empty = ContractAccessesWithValues::default(); + let empty = StorageAccessesWithValues::default(); let for_contract = record.get(contract_id).unwrap_or(&empty); *balance_root = compute_balances_hash(&for_contract.assets); @@ -2244,7 +2265,7 @@ where tx_id: TxId, inputs: &[Input], outputs: &mut [Output], - record: &BTreeMap, + record: &ContractAccessesWithValues, ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { @@ -2261,7 +2282,7 @@ where }) }; - let empty = ContractAccessesWithValues::default(); + let empty = StorageAccessesWithValues::default(); let for_contract = record.get(contract_id).unwrap_or(&empty); contract_output.balance_root = diff --git a/crates/services/executor/src/storage_access_recorder.rs b/crates/services/executor/src/storage_access_recorder.rs index 8819aa24db4..574e09c41af 100644 --- a/crates/services/executor/src/storage_access_recorder.rs +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -23,10 +23,16 @@ use fuel_core_storage::{ StorageChanges, }, }; -use fuel_core_types::fuel_tx::{ - AssetId, - Bytes32, - ContractId, +use fuel_core_types::{ + fuel_tx::{ + AssetId, + Bytes32, + ContractId, + }, + services::executor::{ + ContractAccessesWithValues, + StorageAccessesWithValues, + }, }; use parking_lot::Mutex; @@ -46,24 +52,17 @@ use alloc::{ BTreeSet, }, sync::Arc, - vec::Vec, }; #[derive(Debug, Clone, Default)] -pub(crate) struct ContractAccesses { +pub(crate) struct StorageAccesses { pub assets: BTreeSet, pub slots: BTreeSet, } -#[derive(Debug, Clone, Default)] -pub(crate) struct ContractAccessesWithValues { - pub assets: BTreeMap, - pub slots: BTreeMap>>, -} - #[derive(Debug, Clone, Default)] pub(crate) struct ReadsPerContract { - pub per_contract: BTreeMap, + pub per_contract: BTreeMap, } impl ReadsPerContract { @@ -91,10 +90,7 @@ impl ReadsPerContract { mut self, storage: S, changes: &Changes, - ) -> StorageResult<( - BTreeMap, - BTreeMap, - )> + ) -> StorageResult<(ContractAccessesWithValues, ContractAccessesWithValues)> where S: StorageInspect + StorageInspect @@ -108,7 +104,7 @@ impl ReadsPerContract { } // Fetch original values from the storage - let before: BTreeMap = self + let before: BTreeMap = self .per_contract .into_iter() .map(|(contract_id, accesses)| { @@ -134,12 +130,12 @@ impl ReadsPerContract { Ok((slot_key, value)) }) .collect::>()?; - Ok((contract_id, ContractAccessesWithValues { assets, slots })) + Ok((contract_id, StorageAccessesWithValues { assets, slots })) }) .collect::>()?; // Update final values from the changes - let mut after: BTreeMap = before.clone(); + let mut after: BTreeMap = before.clone(); let sc = StorageChanges::Changes(changes.clone()); let ci = ChangesIterator::new(&sc); diff --git a/crates/services/producer/src/mocks.rs b/crates/services/producer/src/mocks.rs index 5b7325d8193..d9f11160b40 100644 --- a/crates/services/producer/src/mocks.rs +++ b/crates/services/producer/src/mocks.rs @@ -145,6 +145,7 @@ impl BlockProducer> for MockExecutor { block, skipped_transactions: vec![], tx_status: vec![], + tx_storage_states: vec![], events: vec![], }, Default::default(), @@ -172,6 +173,7 @@ impl BlockProducer> for FailingMockExecutor { block, skipped_transactions: vec![], tx_status: vec![], + tx_storage_states: vec![], events: vec![], }, Default::default(), @@ -200,6 +202,7 @@ impl BlockProducer> for MockExecutorWithCapture { block, skipped_transactions: vec![], tx_status: vec![], + tx_storage_states: vec![], events: vec![], }, Default::default(), diff --git a/crates/services/upgradable-executor/wasm-executor/src/utils.rs b/crates/services/upgradable-executor/wasm-executor/src/utils.rs index 66cc20ebfb8..a1e1c9fe456 100644 --- a/crates/services/upgradable-executor/wasm-executor/src/utils.rs +++ b/crates/services/upgradable-executor/wasm-executor/src/utils.rs @@ -153,6 +153,7 @@ pub fn convert_to_v1_execution_result( block, skipped_transactions, tx_status, + tx_storage_states, events, } = result; @@ -165,6 +166,7 @@ pub fn convert_to_v1_execution_result( block, skipped_transactions, tx_status, + tx_storage_states, events, }; @@ -184,6 +186,7 @@ pub fn convert_from_v1_execution_result( block, skipped_transactions, tx_status, + tx_storage_states, events, } = result; @@ -196,6 +199,7 @@ pub fn convert_from_v1_execution_result( block, skipped_transactions, tx_status, + tx_storage_states, events, }; @@ -219,6 +223,7 @@ pub fn convert_from_v0_execution_result( block, skipped_transactions, tx_status, + tx_storage_states, events, } = result; @@ -231,6 +236,7 @@ pub fn convert_from_v0_execution_result( block, skipped_transactions, tx_status, + tx_storage_states, events, }; diff --git a/crates/types/src/services/executor.rs b/crates/types/src/services/executor.rs index f7dcb7cc4db..89479fd3212 100644 --- a/crates/types/src/services/executor.rs +++ b/crates/types/src/services/executor.rs @@ -22,6 +22,7 @@ use crate::{ ValidityError, }, fuel_types::{ + AssetId, BlockHeight, Bytes32, ContractId, @@ -36,8 +37,12 @@ use crate::{ services::Uncommitted, }; +#[cfg(not(feature = "alloc"))] +use std::collections::BTreeMap; + #[cfg(feature = "alloc")] use alloc::{ + collections::BTreeMap, string::String, vec::Vec, }; @@ -56,6 +61,7 @@ pub type UncommittedValidationResult = /// The result of transactions execution for block production. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] +#[cfg_attr(any(test, feature = "test-helpers"), derive(Default))] pub struct ExecutionResult { /// Created block during the execution of transactions. It contains only valid transactions. pub block: Block, @@ -64,22 +70,12 @@ pub struct ExecutionResult { pub skipped_transactions: Vec<(TxId, E)>, /// The status of the transactions execution included into the block. pub tx_status: Vec, + /// Storage state before and after executing each tx, for all accessed contracts. + pub tx_storage_states: Vec<(ContractAccessesWithValues, ContractAccessesWithValues)>, /// The list of all events generated during the execution of the block. pub events: Vec, } -#[cfg(any(test, feature = "test-helpers"))] -impl Default for ExecutionResult { - fn default() -> Self { - Self { - block: Block::default(), - skipped_transactions: Default::default(), - tx_status: Default::default(), - events: Default::default(), - } - } -} - /// The result of the validation of the block. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] @@ -96,6 +92,7 @@ impl UncommittedValidationResult { self, block: Block, skipped_transactions: Vec<(TxId, Error)>, + tx_storage_states: Vec<(ContractAccessesWithValues, ContractAccessesWithValues)>, ) -> UncommittedResult { let Self { result: ValidationResult { tx_status, events }, @@ -107,6 +104,7 @@ impl UncommittedValidationResult { skipped_transactions, tx_status, events, + tx_storage_states, }, changes, ) @@ -151,6 +149,21 @@ pub enum Event { }, } +/// Storage accesses, both reads and writes, with associated values. +/// A separate instance of this struct is kept for before and after execution. +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct StorageAccessesWithValues { + /// Contract balances by asset id. + pub assets: BTreeMap, + /// Contract storage slots by slot key. + pub slots: BTreeMap>>, +} + +/// Storage accesses per contract, both reads and writes, with associated values. +/// A separate instance of this struct is kept for before and after execution. +pub type ContractAccessesWithValues = BTreeMap; + /// Known failure modes for processing forced transactions #[derive(Debug, derive_more::Display)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] From 6d3a48e2845991e39829ab1088266ec1ac4a9c40 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Thu, 16 Oct 2025 11:57:33 +0300 Subject: [PATCH 22/22] Use no-default-features for sha2 dep --- crates/services/executor/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/executor/Cargo.toml b/crates/services/executor/Cargo.toml index d3e9984792f..1ba436e02f6 100644 --- a/crates/services/executor/Cargo.toml +++ b/crates/services/executor/Cargo.toml @@ -32,7 +32,7 @@ fuel-core-types = { workspace = true, default-features = false, features = [ ] } parking_lot = { workspace = true } serde = { workspace = true } -sha2 = "0.10" +sha2 = { version = "0.10", default-features = false } tracing = { workspace = true } [dev-dependencies]