diff --git a/CHANGELOG.md b/CHANGELOG.md index b34aa87bb1..28d74b7bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next release +- dev: impl get_state_updates using get_transaction_re_execution_state_diff - feat: support strk as fee token - dev: pallet test for estimate_fee that skip validation - feat: add versioned constants to pallet constants diff --git a/crates/client/db/src/da_db.rs b/crates/client/db/src/da_db.rs index cf9199ef5c..2cf5bbfcb6 100644 --- a/crates/client/db/src/da_db.rs +++ b/crates/client/db/src/da_db.rs @@ -6,8 +6,6 @@ use sp_database::Database; // Starknet use starknet_api::block::BlockHash; use starknet_api::hash::StarkFelt; -use starknet_api::state::ThinStateDiff; -use uuid::Uuid; use crate::{DbError, DbHash}; @@ -18,40 +16,6 @@ pub struct DaDb { // TODO: purge old cairo job keys impl DaDb { - pub fn state_diff(&self, block_hash: &BlockHash) -> Result { - match self.db.get(crate::columns::DA, block_hash.0.bytes()) { - Some(raw) => Ok(ThinStateDiff::decode(&mut &raw[..])?), - None => Err(DbError::ValueNotInitialized(crate::columns::DA, block_hash.to_string())), - } - } - - pub fn store_state_diff(&self, block_hash: &BlockHash, diff: &ThinStateDiff) -> Result<(), DbError> { - let mut transaction = sp_database::Transaction::new(); - - transaction.set(crate::columns::DA, block_hash.0.bytes(), &diff.encode()); - - self.db.commit(transaction)?; - - Ok(()) - } - - pub fn cairo_job(&self, block_hash: &BlockHash) -> Result, DbError> { - match self.db.get(crate::columns::DA, block_hash.0.bytes()) { - Some(raw) => Ok(Some(Uuid::from_slice(&raw[..])?)), - None => Ok(None), - } - } - - pub fn update_cairo_job(&self, block_hash: &BlockHash, job_id: Uuid) -> Result<(), DbError> { - let mut transaction = sp_database::Transaction::new(); - - transaction.set(crate::columns::DA, block_hash.0.bytes(), &job_id.into_bytes()); - - self.db.commit(transaction)?; - - Ok(()) - } - pub fn last_proved_block(&self) -> Result { match self.db.get(crate::columns::DA, crate::static_keys::LAST_PROVED_BLOCK) { Some(raw) => { diff --git a/crates/client/rpc/src/lib.rs b/crates/client/rpc/src/lib.rs index 0a5c819b20..f8427451ba 100644 --- a/crates/client/rpc/src/lib.rs +++ b/crates/client/rpc/src/lib.rs @@ -52,7 +52,6 @@ use sp_blockchain::HeaderBackend; use sp_core::H256; use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; use sp_runtime::transaction_validity::InvalidTransaction; -use starknet_api::block::BlockHash; use starknet_api::core::Nonce; use starknet_api::hash::StarkFelt; use starknet_api::transaction::{Calldata, Fee, TransactionHash, TransactionVersion}; @@ -70,6 +69,7 @@ use starknet_core::types::{ TransactionExecutionStatus, TransactionFinalityStatus, TransactionReceipt, }; use starknet_core::utils::get_selector_from_name; +use trace_api::get_previous_block_substrate_hash; use crate::constants::{MAX_EVENTS_CHUNK_SIZE, MAX_EVENTS_KEYS}; use crate::types::RpcEventFilter; @@ -206,22 +206,6 @@ where Ok(starknet_block.header().block_number) } - - /// Returns the state diff for the given block. - /// - /// # Arguments - /// - /// * `starknet_block_hash` - The hash of the block containing the state diff (starknet block). - fn get_state_diff(&self, starknet_block_hash: &BlockHash) -> Result { - let state_diff = self.backend.da().state_diff(starknet_block_hash).map_err(|e| { - error!("Failed to retrieve state diff from cache for block with hash {}: {e}", starknet_block_hash); - StarknetRpcApiError::InternalServerError - })?; - - let rpc_state_diff = to_rpc_state_diff(state_diff); - - Ok(rpc_state_diff) - } } /// Taken from https://github.com/paritytech/substrate/blob/master/client/rpc/src/author/mod.rs#L78 @@ -1162,12 +1146,15 @@ where FieldElement::default() }; - let starknet_block_hash = BlockHash(starknet_block.header().hash().into()); + let block_transactions = starknet_block.transactions(); - let state_diff = self.get_state_diff(&starknet_block_hash).map_err(|e| { - error!("Failed to get state diff. Starknet block hash: {starknet_block_hash}, error: {e}"); - StarknetRpcApiError::InternalServerError - })?; + let previous_block_substrate_hash = get_previous_block_substrate_hash(self, substrate_block_hash)?; + + let state_diff = self.get_transaction_re_execution_state_diff( + previous_block_substrate_hash, + vec![], + block_transactions.clone(), + )?; let state_update = StateUpdate { block_hash: starknet_block.header().hash().into(), diff --git a/crates/client/rpc/src/trace_api.rs b/crates/client/rpc/src/trace_api.rs index 95685cb46f..86a2a0ca6e 100644 --- a/crates/client/rpc/src/trace_api.rs +++ b/crates/client/rpc/src/trace_api.rs @@ -250,6 +250,42 @@ where })?) } + pub fn get_transaction_re_execution_state_diff( + &self, + previous_block_substrate_hash: B::Hash, + transactions_before: Vec, + transactions_to_trace: Vec, + ) -> RpcResult { + let commitment_state_diff = self + .client + .runtime_api() + .get_transaction_re_execution_state_diff( + previous_block_substrate_hash, + transactions_before, + transactions_to_trace, + ) + .map_err(|e| { + error!("Failed to execute runtime API call: {e}"); + StarknetRpcApiError::InternalServerError + })? + .map_err(|e| { + error!("Failed to reexecute the block transactions: {e:?}"); + StarknetRpcApiError::InternalServerError + })? + .map_err(|_| { + error!( + "One of the transaction failed during it's reexecution. This should not happen, as the block has \ + already been executed successfully in the past. There is a bug somewhere." + ); + StarknetRpcApiError::InternalServerError + })?; + + Ok(blockifier_to_rpc_state_diff_types(commitment_state_diff).map_err(|e| { + error!("Failed to get state diff from reexecution info, error: {e}"); + StarknetRpcApiError::InternalServerError + })?) + } + fn execution_info_to_transaction_trace( execution_infos: Vec<(TransactionExecutionInfo, CommitmentStateDiff)>, block_transactions: &[Transaction], @@ -446,7 +482,7 @@ fn tx_execution_infos_to_tx_trace( Ok(tx_trace) } -fn get_previous_block_substrate_hash( +pub fn get_previous_block_substrate_hash( starknet: &Starknet, substrate_block_hash: B::Hash, ) -> Result diff --git a/crates/pallets/starknet/runtime_api/src/lib.rs b/crates/pallets/starknet/runtime_api/src/lib.rs index f0a6a055d4..94bcdddeeb 100644 --- a/crates/pallets/starknet/runtime_api/src/lib.rs +++ b/crates/pallets/starknet/runtime_api/src/lib.rs @@ -16,15 +16,15 @@ use mp_felt::Felt252Wrapper; pub extern crate alloc; use alloc::vec::Vec; -use mp_simulations::{InternalSubstrateError, SimulationError, SimulationFlags, TransactionSimulationResult}; +use mp_simulations::{ + InternalSubstrateError, ReExecutionResult, SimulationError, SimulationFlags, TransactionSimulationResult, +}; use sp_api::BlockT; use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector, Nonce}; use starknet_api::hash::StarkFelt; use starknet_api::state::StorageKey; use starknet_api::transaction::{Calldata, Event as StarknetEvent, MessageToL1, TransactionHash}; -type ReExecutionResult = Result)>, SimulationError>; - sp_api::decl_runtime_apis! { pub trait StarknetRuntimeApi { /// Returns the nonce associated with the given address in the given block @@ -73,6 +73,8 @@ sp_api::decl_runtime_apis! { /// If any of the transactions (from both arguments) fails, an error is returned. fn re_execute_transactions(transactions_before: Vec, transactions_to_trace: Vec, with_state_diff: bool) -> Result; + fn get_transaction_re_execution_state_diff(transactions_before: Vec, transactions: Vec) -> Result, InternalSubstrateError>; + fn get_index_and_tx_for_tx_hash(xts: Vec<::Extrinsic>, tx_hash: TransactionHash) -> Option<(u32, Transaction)>; fn get_events_for_tx_by_hash(tx_hash: TransactionHash) -> Vec; diff --git a/crates/pallets/starknet/src/simulations.rs b/crates/pallets/starknet/src/simulations.rs index 3e86f35df1..3454365d11 100644 --- a/crates/pallets/starknet/src/simulations.rs +++ b/crates/pallets/starknet/src/simulations.rs @@ -7,7 +7,9 @@ use blockifier::transaction::objects::TransactionExecutionInfo; use blockifier::transaction::transaction_execution::Transaction; use blockifier::transaction::transactions::{ExecutableTransaction, L1HandlerTransaction}; use frame_support::storage; -use mp_simulations::{InternalSubstrateError, SimulationError, SimulationFlags, TransactionSimulationResult}; +use mp_simulations::{ + InternalSubstrateError, ReExecutionResult, SimulationError, SimulationFlags, TransactionSimulationResult, +}; use mp_transactions::execution::{ commit_transactional_state, execute_l1_handler_transaction, run_non_revertible_transaction, run_revertible_transaction, MutRefState, SetArbitraryNonce, @@ -19,8 +21,6 @@ use starknet_api::transaction::TransactionVersion; use crate::blockifier_state_adapter::BlockifierStateAdapter; use crate::{log, Config, Error, Pallet}; -type ReExecutionResult = Result)>, SimulationError>; - impl Pallet { pub fn estimate_fee( transactions: Vec, @@ -269,6 +269,55 @@ impl Pallet { Ok(execution_infos) } + pub fn get_transaction_re_execution_state_diff( + transactions_before: Vec, + transactions_to_trace: Vec, + ) -> Result, InternalSubstrateError> { + storage::transactional::with_transaction(|| { + let res = Self::get_transaction_re_execution_state_diff_inner(transactions_before, transactions_to_trace); + storage::TransactionOutcome::Rollback(Result::<_, DispatchError>::Ok(Ok(res))) + }) + .map_err(|e| { + log::error!("Failed to reexecute a tx: {:?}", e); + InternalSubstrateError::FailedToCreateATransactionalStorageExecution + })? + } + + fn get_transaction_re_execution_state_diff_inner( + transactions_before: Vec, + transactions_to_trace: Vec, + ) -> Result { + let block_context = Self::get_block_context(); + let mut state = BlockifierStateAdapter::::default(); + + transactions_before.iter().try_for_each(|tx| { + Self::execute_transaction(tx, &mut state, &block_context, &SimulationFlags::default()).map_err(|e| { + log::error!("Failed to reexecute a tx: {}", e); + SimulationError::from(e) + })?; + Ok::<(), SimulationError>(()) + })?; + + let mut transactional_state = CachedState::new(MutRefState::new(&mut state), GlobalContractCache::new(1)); + + transactions_to_trace.iter().try_for_each(|tx| { + Self::execute_transaction(tx, &mut transactional_state, &block_context, &SimulationFlags::default()) + .map_err(|e| { + log::error!("Failed to reexecute a tx: {}", e); + SimulationError::from(e) + })?; + Ok::<(), SimulationError>(()) + })?; + + let state_diff = transactional_state.to_state_diff(); + commit_transactional_state(transactional_state).map_err(|e| { + log::error!("Failed to commit state changes: {:?}", e); + SimulationError::from(e) + })?; + + Ok(state_diff) + } + fn execute_transaction( transaction: &Transaction, state: &mut S, diff --git a/crates/primitives/simulations/src/lib.rs b/crates/primitives/simulations/src/lib.rs index 143e41addb..bdf482e52c 100644 --- a/crates/primitives/simulations/src/lib.rs +++ b/crates/primitives/simulations/src/lib.rs @@ -1,3 +1,4 @@ +use blockifier::state::cached_state::CommitmentStateDiff; use blockifier::state::errors::StateError; use blockifier::transaction::errors::TransactionExecutionError; use blockifier::transaction::objects::TransactionExecutionInfo; @@ -32,6 +33,7 @@ impl From for SimulationError { } } +pub type ReExecutionResult = Result)>, SimulationError>; pub type TransactionSimulationResult = Result; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index f10c0e3e30..81eba9ef77 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -33,7 +33,9 @@ pub use frame_support::weights::{IdentityFee, Weight}; pub use frame_support::{construct_runtime, parameter_types, StorageValue}; pub use frame_system::Call as SystemCall; use mp_felt::Felt252Wrapper; -use mp_simulations::{InternalSubstrateError, SimulationError, SimulationFlags, TransactionSimulationResult}; +use mp_simulations::{ + InternalSubstrateError, ReExecutionResult, SimulationError, SimulationFlags, TransactionSimulationResult, +}; use pallet_grandpa::{fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList}; /// Import the Starknet pallet. pub use pallet_starknet; @@ -279,10 +281,14 @@ impl_runtime_apis! { Starknet::estimate_fee(transactions, &simulation_flags) } - fn re_execute_transactions(transactions_before: Vec, transactions_to_trace: Vec, with_state_diff: bool) -> Result)>, SimulationError>, InternalSubstrateError> { + fn re_execute_transactions(transactions_before: Vec, transactions_to_trace: Vec, with_state_diff: bool) -> Result { Starknet::re_execute_transactions(transactions_before, transactions_to_trace, with_state_diff) } + fn get_transaction_re_execution_state_diff( transactions_before: Vec, transactions_to_trace: Vec) -> Result, InternalSubstrateError> { + Starknet::get_transaction_re_execution_state_diff(transactions_before, transactions_to_trace) + } + fn estimate_message_fee(message: L1HandlerTransaction) -> Result, InternalSubstrateError> { Starknet::estimate_message_fee(message) } diff --git a/starknet-rpc-test/Cargo.toml b/starknet-rpc-test/Cargo.toml index 75bfa4feb1..520cdf0c05 100644 --- a/starknet-rpc-test/Cargo.toml +++ b/starknet-rpc-test/Cargo.toml @@ -119,6 +119,10 @@ path = "estimate_message_fee.rs" name = "starknet_simulate_transaction" path = "simulate_transaction.rs" +[[test]] +name = "starknet_get_state_update" +path = "get_state_update.rs" + [[test]] name = "starknet_trace_block_transactions" path = "trace_block.rs" diff --git a/starknet-rpc-test/add_declare_transaction.rs b/starknet-rpc-test/add_declare_transaction.rs index 4ed1654a21..bda00555ab 100644 --- a/starknet-rpc-test/add_declare_transaction.rs +++ b/starknet-rpc-test/add_declare_transaction.rs @@ -100,6 +100,21 @@ async fn fail_execution_step_with_no_storage_change(madara: &ThreadSafeMadaraCli let included_txs = rpc.get_block_transaction_count(BlockId::Number(block_number)).await?; assert_eq!(included_txs, 1); + // add some balance back for other tests + let account = build_single_owner_account(&rpc, SIGNER_PRIVATE, ARGENT_CONTRACT_ADDRESS, true); + let balance = + read_erc20_balance(&rpc, FieldElement::from_hex_be(ETH_FEE_TOKEN_ADDRESS).unwrap(), account.address()).await; + { + let mut madara_write_lock = madara.write().await; + let txs = madara_write_lock + .create_block_with_txs(vec![Transaction::Execution(account.transfer_tokens_u256( + oz_account.address(), + U256 { low: FieldElement::ONE, high: balance[1] }, + None, + ))]) + .await?; + assert!(txs[0].is_ok()); + } Ok(()) } diff --git a/starknet-rpc-test/get_state_update.rs b/starknet-rpc-test/get_state_update.rs new file mode 100644 index 0000000000..437b9659de --- /dev/null +++ b/starknet-rpc-test/get_state_update.rs @@ -0,0 +1,164 @@ +use std::collections::HashMap; + +use anyhow::anyhow; +use assert_matches::assert_matches; +use rstest::rstest; +use starknet_accounts::{Account, ConnectedAccount}; +use starknet_core::types::{BlockId, BlockTag, DeclaredClassItem, MaybePendingStateUpdate, StarknetError}; +use starknet_core::utils::get_storage_var_address; +use starknet_ff::FieldElement; +use starknet_providers::Provider; +use starknet_providers::ProviderError::StarknetError as StarknetProviderError; +use starknet_rpc_test::constants::{ + ACCOUNT_CONTRACT_ADDRESS, ARGENT_CONTRACT_ADDRESS, OZ_CONTRACT_ADDRESS, SEQUENCER_CONTRACT_ADDRESS, SIGNER_PRIVATE, +}; +use starknet_rpc_test::fixtures::{madara, ThreadSafeMadaraClient}; +use starknet_rpc_test::utils::{build_single_owner_account, read_erc20_balance, AccountActions}; +use starknet_rpc_test::Transaction; +use starknet_test_utils::constants::ETH_FEE_TOKEN_ADDRESS; + +#[rstest] +#[tokio::test] +async fn fail_non_existing_block(madara: &ThreadSafeMadaraClient) -> Result<(), anyhow::Error> { + let rpc = madara.get_starknet_client().await; + + assert_matches!( + rpc.get_state_update(BlockId::Hash(FieldElement::ZERO)).await, + Err(StarknetProviderError(StarknetError::BlockNotFound)) + ); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn returns_correct_state_diff_transfer(madara: &ThreadSafeMadaraClient) -> Result<(), anyhow::Error> { + let rpc = madara.get_starknet_client().await; + + let recipient = FieldElement::from_hex_be(ACCOUNT_CONTRACT_ADDRESS).unwrap(); + let fee_token_address = FieldElement::from_hex_be(ETH_FEE_TOKEN_ADDRESS).unwrap(); + let account_alice = build_single_owner_account(&rpc, SIGNER_PRIVATE, ARGENT_CONTRACT_ADDRESS, true); + let account_bob = build_single_owner_account(&rpc, SIGNER_PRIVATE, OZ_CONTRACT_ADDRESS, true); + + let nonce = account_alice.get_nonce().await?.try_into()?; + { + let mut madara_write_lock = madara.write().await; + let txs = madara_write_lock + .create_block_with_txs(vec![ + Transaction::Execution(account_alice.transfer_tokens(recipient, FieldElement::ONE, Some(nonce))), + Transaction::Execution(account_bob.transfer_tokens(recipient, FieldElement::ONE, None)), + ]) + .await?; + txs.iter().for_each(|tx| assert!(tx.is_ok())); + } + + let state_update = match rpc.get_state_update(BlockId::Tag(BlockTag::Latest)).await.unwrap() { + MaybePendingStateUpdate::Update(update) => update, + MaybePendingStateUpdate::PendingUpdate(_) => { + return Err(anyhow!("Expected update, got pending update")); + } + }; + let block_hash_and_number = rpc.block_hash_and_number().await?; + + assert_eq!(state_update.block_hash, block_hash_and_number.block_hash); + assert_eq!(state_update.old_root, FieldElement::ZERO); + assert_eq!(state_update.new_root, FieldElement::ZERO); + + let storage_diff = &state_update.state_diff.storage_diffs[0]; + let mut storage_diff_map: HashMap<&FieldElement, &FieldElement> = HashMap::from_iter( + storage_diff + .storage_entries + .iter() + .map(|item| (&item.key, &item.value)) + .collect::>(), + ); + for account_address in + [account_alice.address(), account_bob.address(), FieldElement::from_hex_be(SEQUENCER_CONTRACT_ADDRESS).unwrap()] + { + let balance = read_erc20_balance(&rpc, fee_token_address, account_address).await[0]; // omit the second part since it probably won't change + let key = get_storage_var_address("ERC20_balances", &[account_address]).unwrap(); + assert_eq!(storage_diff_map.remove(&key).unwrap(), &balance); + } + assert!(storage_diff_map.is_empty()); + assert_eq!(state_update.state_diff.nonces.len(), 2); + let mut nonce_map: HashMap<&FieldElement, &FieldElement> = HashMap::from_iter( + state_update + .state_diff + .nonces + .iter() + .map(|item| (&item.contract_address, &item.nonce)) + .collect::>(), + ); + + assert_eq!(storage_diff.address, FieldElement::from_hex_be(ETH_FEE_TOKEN_ADDRESS).unwrap()); + for account_address in [account_alice.address(), account_bob.address()] { + let account_new_nonce = rpc.get_nonce(BlockId::Tag(BlockTag::Latest), account_address).await?; + assert_eq!(*nonce_map.remove(&account_address).unwrap(), account_new_nonce); + } + assert!(nonce_map.is_empty()); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn returns_correct_state_diff_declare(madara: &ThreadSafeMadaraClient) -> Result<(), anyhow::Error> { + let rpc = madara.get_starknet_client().await; + + let account = build_single_owner_account(&rpc, SIGNER_PRIVATE, ARGENT_CONTRACT_ADDRESS, true); + let (declare_tx, expected_class_hash, expected_compiled_class_hash) = account.declare_contract( + "../starknet-rpc-test/contracts/counter6/counter6.contract_class.json", + "../starknet-rpc-test/contracts/counter6/counter6.compiled_contract_class.json", + None, + ); + + { + let mut madara_write_lock = madara.write().await; + + let txs = madara_write_lock.create_block_with_txs(vec![Transaction::Declaration(declare_tx)]).await?; + assert!(txs[0].is_ok()); + }; + let state_update = match rpc.get_state_update(BlockId::Tag(BlockTag::Latest)).await.unwrap() { + MaybePendingStateUpdate::Update(update) => update, + MaybePendingStateUpdate::PendingUpdate(_) => { + return Err(anyhow!("Expected update, got pending update")); + } + }; + let block_hash = rpc.block_hash_and_number().await?.block_hash; + + assert_eq!(state_update.block_hash, block_hash); + assert_eq!(state_update.old_root, FieldElement::ZERO); + assert_eq!(state_update.new_root, FieldElement::ZERO); + assert_eq!(state_update.state_diff.declared_classes.len(), 1); + assert_eq!( + state_update.state_diff.declared_classes[0], + DeclaredClassItem { class_hash: expected_class_hash, compiled_class_hash: expected_compiled_class_hash } + ); + + assert_eq!(state_update.state_diff.nonces.len(), 1); + assert_eq!( + state_update.state_diff.storage_diffs[0].address, + FieldElement::from_hex_be(ETH_FEE_TOKEN_ADDRESS).unwrap() + ); + let account_new_nonce = rpc.get_nonce(BlockId::Tag(BlockTag::Latest), account.address()).await?; + assert_eq!(state_update.state_diff.nonces[0].nonce, account_new_nonce); + assert_eq!(state_update.state_diff.nonces[0].contract_address, account.address()); + + let storage_diff = &state_update.state_diff.storage_diffs[0]; + let mut storage_diff_map: HashMap<&FieldElement, &FieldElement> = HashMap::from_iter( + storage_diff + .storage_entries + .iter() + .map(|item| (&item.key, &item.value)) + .collect::>(), + ); + let fee_token_address = FieldElement::from_hex_be(ETH_FEE_TOKEN_ADDRESS).unwrap(); + for account_address in [account.address(), FieldElement::from_hex_be(SEQUENCER_CONTRACT_ADDRESS).unwrap()] { + let balance = read_erc20_balance(&rpc, fee_token_address, account_address).await[0]; // omit the second part since it probably won't change + let key = get_storage_var_address("ERC20_balances", &[account_address]).unwrap(); + assert_eq!(storage_diff_map.remove(&key).unwrap(), &balance); + } + assert!(storage_diff_map.is_empty()); + + Ok(()) +}