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 diff --git a/Cargo.lock b/Cargo.lock index bbdd79a0358..161da55406e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3534,6 +3534,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha2 0.10.9", "strum 0.25.0", "strum_macros 0.25.3", "tempfile", @@ -3801,6 +3802,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 63701daf609..4728490ca3d 100644 --- a/crates/fuel-core/Cargo.toml +++ b/crates/fuel-core/Cargo.toml @@ -133,6 +133,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 571c9873527..0403330a4b0 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -73,13 +73,9 @@ mod tests { op, }, fuel_crypto::SecretKey, - fuel_merkle::{ - common::empty_sum_sha256, - sparse, - }, + fuel_merkle::common::empty_sum_sha256, fuel_tx::{ Bytes32, - Cacheable, ConsensusParameters, Create, DependentCost, @@ -108,7 +104,6 @@ mod tests { OutputContract, Outputs, Policies, - Script as ScriptField, TxPointer as TxPointerTraitTrait, }, input::{ @@ -168,6 +163,10 @@ mod tests { SeedableRng, prelude::StdRng, }; + use sha2::{ + Digest, + Sha256, + }; #[derive(Clone, Debug, Default)] struct Config { @@ -489,6 +488,18 @@ 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(), @@ -497,12 +508,12 @@ mod tests { 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, 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::zeroed()); - assert_eq!(mint.output_contract().state_root, Bytes32::zeroed()); + 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 { @@ -572,14 +583,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) @@ -588,14 +591,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"); @@ -1666,8 +1661,8 @@ mod tests { .transaction() .clone() .into(); - let db = &mut Database::default(); + let db = &mut Database::default(); let mut executor = create_executor( db.clone(), Config { @@ -1691,17 +1686,33 @@ 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. - let empty_state = (*sparse::empty_sum()).into(); + // 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 { .. } - )); - 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] @@ -1721,8 +1732,8 @@ mod tests { .transaction() .clone() .into(); - let db = &mut Database::default(); + let db = &mut Database::default(); let mut executor = create_executor( db.clone(), Config { @@ -1747,7 +1758,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, @@ -1761,8 +1771,17 @@ 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] @@ -1773,9 +1792,11 @@ mod tests { // Create a contract that modifies the state let (create, contract_id) = create_contract( + // Increment the slot matching the tx id by one vec![ - // Sets the state STATE[0x1; 32] = value of `RegId::PC`; - op::sww(0x1, 0x29, RegId::PC), + op::srw(0x10, 0x29, RegId::ZERO), + op::addi(0x10, 0x10, 1), + op::sww(RegId::ZERO, 0x29, 0x10), op::ret(1), ] .into_iter() @@ -1786,7 +1807,7 @@ mod tests { 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` @@ -1803,9 +1824,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() @@ -1848,39 +1867,78 @@ mod tests { block, tx_status, .. } = executor.produce_and_commit(block).unwrap(); - let empty_state = (*sparse::empty_sum()).into(); + assert!( + tx_status.iter().all(|s| (matches!( + s.result, + TransactionExecutionResult::Success { .. } + ))) + ); + 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!( + 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(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(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(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(), - executed_tx.outputs()[0].state_root() + Some(&Bytes32::new(expected_state_root)) ); - assert_ne!( - executed_tx.inputs()[0].balance_root(), - executed_tx.outputs()[0].balance_root() + + // Output state: slot tx_id with value 1 + let mut hasher = Sha256::new(); + 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_in_inputs_updated() { + 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. - // 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( + // Increment the slot matching the tx id by one vec![ - // Sets the state STATE[0x1; 32] = value of `RegId::PC`; - op::sww(0x1, 0x29, RegId::PC), + op::srw(0x10, 0x29, RegId::ZERO), + op::addi(0x10, 0x10, 1), + op::sww(RegId::ZERO, 0x29, 0x10), op::ret(1), ] .into_iter() @@ -1891,7 +1949,7 @@ mod tests { 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` @@ -1901,6 +1959,8 @@ mod tests { // 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() @@ -1908,9 +1968,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() @@ -1928,14 +1986,13 @@ mod tests { .build() .transaction() .clone(); - let db = Database::default(); - let consensus_parameters = ConsensusParameters::default(); + let db = Database::default(); let mut executor = create_executor( db.clone(), Config { forbid_fake_coins_default: false, - consensus_parameters: consensus_parameters.clone(), + ..Default::default() }, ); @@ -1950,49 +2007,501 @@ mod tests { transactions: vec![create.into(), modify_balance_and_state_tx.into()], }; - let ExecutionResult { block, .. } = executor.produce_and_commit(block).unwrap(); + 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 state_root = executed_tx.outputs()[0].state_root(); - let balance_root = executed_tx.outputs()[0].balance_root(); + 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(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)) + ); - let mut new_tx = executed_tx.clone(); - *new_tx.script_mut() = vec![]; - new_tx.precompute(&consensus_parameters.chain_id()).unwrap(); + // Output balances: 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.outputs()[0].balance_root(), + Some(&Bytes32::new(expected_balance_root)) + ); + + // Input state: empty slot tx_id + let mut hasher = Sha256::new(); + 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 2 + let mut hasher = Sha256::new(); + 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 + hasher.update({ + let mut value = [0u8; 32]; + value[..8].copy_from_slice(&2u64.to_be_bytes()); // the value is 2 + 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_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: 2.into(), + height: 1.into(), ..Default::default() }, ..Default::default() }, - transactions: vec![new_tx.into()], + transactions: vec![create.into(), tx_1.into(), tx_2.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(); + } = executor.produce_and_commit(block).unwrap(); assert!(matches!( - tx_status[1].result, + tx_status[3].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 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(); + 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(); + 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)) + ); + } + + /// 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() + { + 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 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` + 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_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(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let tx1 = builder + .script_gas_limit(10000) + .coin_input(AssetId::zeroed(), 10000) + .start_script(script1, script_data1) + .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 (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(), + 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(), tx1.into(), tx2.into()], + }; + + let ExecutionResult { + block, tx_status, .. + } = executor.produce_and_commit(block).unwrap(); + assert!( + tx_status.iter().all(|s| (matches!( + s.result, + TransactionExecutionResult::Success { .. } + ))) + ); + + // 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(); + 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(); + 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(); + 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(); + 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(), + Some(&Bytes32::new(expected_state_root)) + ); } #[test] @@ -2041,10 +2550,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/fuel-core/src/graphql_api/worker_service.rs b/crates/fuel-core/src/graphql_api/worker_service.rs index 350cc0361bc..0e4b6bcf79c 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/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/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/Cargo.toml b/crates/services/executor/Cargo.toml index 69f10891873..1ba436e02f6 100644 --- a/crates/services/executor/Cargo.toml +++ b/crates/services/executor/Cargo.toml @@ -32,6 +32,7 @@ fuel-core-types = { workspace = true, default-features = false, features = [ ] } parking_lot = { workspace = true } serde = { workspace = true } +sha2 = { version = "0.10", default-features = false } tracing = { workspace = true } [dev-dependencies] 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..4ca07cf0758 --- /dev/null +++ b/crates/services/executor/src/contract_state_hash.rs @@ -0,0 +1,46 @@ +#[cfg(feature = "std")] +use std::collections::BTreeMap; + +#[cfg(all(feature = "alloc", not(feature = "std")))] +use alloc::{ + collections::BTreeMap, + 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: &BTreeMap) -> Bytes32 { + let mut hasher = Sha256::new(); + for (key, value) in accessed { + hasher.update(key); + hasher.update(value.to_be_bytes()); + } + Bytes32::new(hasher.finalize().into()) +} + +/// 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: &BTreeMap>>) -> Bytes32 { + let mut hasher = Sha256::new(); + for (key, value) in accessed { + hasher.update(key); + if let Some(value) = value { + hasher.update([1u8]); + hasher.update((value.len() as Word).to_be_bytes()); + hasher.update(value); + } else { + hasher.update([0u8]); + } + } + Bytes32::new(hasher.finalize().into()) +} diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 8ea1e4176cb..ad7582bc8db 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -1,4 +1,8 @@ use crate::{ + contract_state_hash::{ + 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, @@ -131,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, @@ -312,6 +319,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)>, @@ -320,19 +328,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() } } @@ -415,6 +411,7 @@ where changes, events, tx_status, + tx_storage_states, skipped_transactions, coinbase, used_gas, @@ -442,6 +439,7 @@ where block, skipped_transactions, tx_status, + tx_storage_states, events, }; @@ -877,12 +875,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 +904,7 @@ where .checked_add(1) .ok_or(ExecutorError::TooManyTransactions)?; - Ok(()) + Ok((storage_before, storage_after)) } #[tracing::instrument(skip_all)] @@ -1274,7 +1272,11 @@ where execution_data: &mut ExecutionData, storage_tx: &mut TxStorageTransaction, memory: &mut MemoryInstance, - ) -> ExecutorResult + ) -> ExecutorResult<( + Transaction, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where T: KeyValueInspect, { @@ -1405,7 +1407,11 @@ where gas_price: Word, execution_data: &mut ExecutionData, storage_tx: &mut TxStorageTransaction, - ) -> ExecutorResult + ) -> ExecutorResult<( + Transaction, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where T: KeyValueInspect, { @@ -1417,39 +1423,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 input = mint.input_contract().clone(); - let mut input = Input::Contract(input); + 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)?; - if self.options.forbid_fake_coins { - self.verify_inputs_exist_and_values_match( - storage_tx, - core::slice::from_ref(&input), - header.da_height, - )?; - } + let mut input = Input::Contract(mint.input_contract().clone()); - self.compute_inputs(core::slice::from_mut(&mut input), storage_tx)?; + 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)] @@ -1462,7 +1470,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, @@ -1474,14 +1486,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)?; @@ -1508,7 +1521,7 @@ where tx_id, )?; - Ok(tx.into()) + Ok((tx.into(), storage_before, storage_after)) } fn check_mint_amount(mint: &Mint, expected_amount: u64) -> ExecutorResult<()> { @@ -1599,11 +1612,18 @@ 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, { - 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); @@ -1622,7 +1642,16 @@ 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 (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())?; let block_height = *header.height(); let output = *mint.output_contract(); @@ -1636,10 +1665,10 @@ where outputs.as_slice(), )?; self.compute_state_of_not_utxo_outputs( - outputs.as_mut_slice(), - core::slice::from_ref(input), *coinbase_id, - storage_tx, + core::slice::from_ref(input), + outputs.as_mut_slice(), + &state_after, )?; let Input::Contract(input) = core::mem::take(input) else { return Err(ExecutorError::Other( @@ -1651,26 +1680,20 @@ 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( + fn update_tx_outputs( &self, - storage_tx: &TxStorageTransaction, tx_id: TxId, tx: &mut Tx, + record: &ContractAccessesWithValues, ) -> 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(tx_id, tx.inputs(), &mut outputs, record)?; *tx.outputs_mut() = outputs; Ok(()) } @@ -1838,7 +1861,14 @@ where gas_price: Word, storage_tx: &mut TxStorageTransaction, memory: &mut MemoryInstance, - ) -> ExecutorResult<(bool, ProgramState, Tx, Arc>)> + ) -> ExecutorResult<( + bool, + ProgramState, + Tx, + Arc>, + ContractAccessesWithValues, + ContractAccessesWithValues, + )> where Tx: ExecutableTransaction + Cacheable, ::Metadata: CheckedMetadataTrait + Send + Sync, @@ -1846,7 +1876,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); @@ -1964,18 +1996,24 @@ 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.as_inner(); + 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)?; + self.compute_inputs(tx.inputs_mut(), storage_tx_recovered, &state_before)?; // only commit state changes if execution was a success if !reverted { - let changes = sub_block_db_commit.into_changes(); - storage_tx.commit_changes(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, &state_before)?; } - self.update_tx_outputs(storage_tx, tx_id, &mut tx)?; - Ok((reverted, state, tx, receipts)) + Ok((reverted, state, tx, receipts, state_before, state_after)) } fn verify_inputs_exist_and_values_match( @@ -2172,6 +2210,7 @@ where &self, inputs: &mut [Input], db: &TxStorageTransaction, + record: &ContractAccessesWithValues, ) -> ExecutorResult<()> where T: KeyValueInspect, @@ -2211,8 +2250,12 @@ 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()?; + + let empty = StorageAccessesWithValues::default(); + let for_contract = record.get(contract_id).unwrap_or(&empty); + + *balance_root = compute_balances_hash(&for_contract.assets); + *state_root = compute_state_hash(&for_contract.slots); } _ => {} } @@ -2225,16 +2268,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, - outputs: &mut [Output], - inputs: &[Input], tx_id: TxId, - db: &TxStorageTransaction, - ) -> ExecutorResult<()> - where - T: KeyValueInspect, - { + inputs: &[Input], + outputs: &mut [Output], + record: &ContractAccessesWithValues, + ) -> ExecutorResult<()> { for output in outputs { if let Output::Contract(contract_output) = output { let contract_id = @@ -2250,9 +2290,12 @@ where }) }; - let contract = ContractRef::new(db, *contract_id); - contract_output.balance_root = contract.balance_root()?; - contract_output.state_root = contract.state_root()?; + let empty = StorageAccessesWithValues::default(); + let for_contract = record.get(contract_id).unwrap_or(&empty); + + 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/lib.rs b/crates/services/executor/src/lib.rs index d28e5dbb612..f6c33113468 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_hash; +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..8cdf6401120 --- /dev/null +++ b/crates/services/executor/src/storage_access_recorder.rs @@ -0,0 +1,237 @@ +use fuel_core_storage::{ + ContractsAssetKey, + ContractsStateKey, + Result as StorageResult, + StorageAsRef, + StorageInspect, + column::Column, + iter::{ + IteratorOverTableWrites, + changes_iterator::ChangesIterator, + }, + kv_store::{ + KeyValueInspect, + StorageColumn, + Value, + }, + tables::{ + ContractsAssets, + ContractsState, + }, + transactional::{ + Changes, + StorageChanges, + }, +}; +use fuel_core_types::{ + fuel_tx::{ + AssetId, + Bytes32, + ContractId, + }, + services::executor::{ + ContractAccessesWithValues, + StorageAccessesWithValues, + }, +}; +use parking_lot::Mutex; + +#[cfg(feature = "std")] +use std::{ + collections::{ + BTreeMap, + BTreeSet, + }, + sync::Arc, +}; + +#[cfg(not(feature = "std"))] +use alloc::{ + collections::{ + BTreeMap, + BTreeSet, + }, + sync::Arc, +}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct StorageAccesses { + 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 + 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()); + } + } + + /// Returns slot values before and after applying the changes + pub(crate) fn finalize( + mut self, + storage: S, + changes: &Changes, + ) -> StorageResult<(ContractAccessesWithValues, ContractAccessesWithValues)> + 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); + } + } + + // 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.to_vec()))?; + Ok((slot_key, value)) + }) + .collect::>()?; + Ok((contract_id, StorageAccessesWithValues { assets, slots })) + }) + .collect::>()?; + + // Update final values from the changes + let mut after: BTreeMap = before.clone(); + + 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 = value.unwrap_or(0); + } + 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 = value.map(|data| data.0.to_vec()); + } + + Ok((before, after)) + } +} + +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(), + } + } + + pub fn as_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); + } +} + +impl KeyValueInspect for StorageAccessRecorder +where + S: KeyValueInspect, +{ + type Column = S::Column; + + fn get(&self, key: &[u8], column: Self::Column) -> StorageResult> { + self.mark(key, column.id()); + self.storage.get(key, column) + } + + fn exists(&self, key: &[u8], column: Self::Column) -> StorageResult { + self.mark(key, column.id()); + self.storage.exists(key, column) + } + + fn size_of_value( + &self, + key: &[u8], + column: Self::Column, + ) -> StorageResult> { + self.mark(key, column.id()); + self.storage.size_of_value(key, column) + } + + 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/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/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..6917c52d77d 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, @@ -9,8 +12,10 @@ use crate::{ }, kv_store::{ KVItem, + KVWriteItem, KeyItem, KeyValueInspect, + WriteOperation, }, structured_storage::TableWithBlueprint, transactional::ReferenceBytesKey, @@ -123,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 @@ -164,9 +221,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 +237,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 +255,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 +271,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)) }) }) @@ -340,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; 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()) diff --git a/crates/types/src/services/executor.rs b/crates/types/src/services/executor.rs index 364c584a171..9b78a278a00 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, sync::Arc, vec::Vec, @@ -59,6 +64,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, @@ -67,22 +73,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)] @@ -99,6 +95,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 }, @@ -110,6 +107,7 @@ impl UncommittedValidationResult { skipped_transactions, tx_status, events, + tx_storage_states, }, changes, ) @@ -154,6 +152,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))] 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 56088f4f568..195182689ab 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 0bbedd4e4b9..0e0d126e2b5 100644 --- a/tests/tests/tx.rs +++ b/tests/tests/tx.rs @@ -212,7 +212,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![]);