Conversation
cbceea8 to
af48366
Compare
…e#373) Spawn a separate task on the cpu-bound blocking task for performing the actual state trie computation to avoid blocking the async executor.
| let global_class_cache = class_cache.build_global()?; | ||
| // Try to use existing global cache if already initialized (useful for tests with multiple nodes) | ||
| // Otherwise, build and initialize a new global cache | ||
| let global_class_cache = match ClassCache::try_global() { |
There was a problem hiding this comment.
change - to run two katana instances in parallel in one test
|
|
||
| let indices = provider.block_body_indices(block_id)?.ok_or(BlockNotFound)?; | ||
| let tx_hashes = provider.transaction_hashes_in_range(indices.into())?; | ||
| let traces = self |
| pub fn get_block_transactions_traces( | ||
| &self, | ||
| block_id: BlockHashOrNumber, | ||
| ) -> Result<Option<TraceBlockTransactionsResponse>, BackendClientError> { |
| BlockHashOrNumber::Num(n) => ConfirmedBlockIdOrTag::Number(n), | ||
| }; | ||
|
|
||
| self.dedup_request( |
| &self, | ||
| block_id: BlockHashOrNumber, | ||
| ) -> ProviderResult<Option<Vec<TypedTransactionExecutionInfo>>>; | ||
| ) -> ProviderResult<Option<Vec<TxTraceWithHash>>>; |
| ) -> ProviderResult<Option<Vec<TxTraceWithHash>>> { | ||
| if let Some(index) = self.block_body_indices(block_id)? { | ||
| let traces = self.transaction_executions_in_range(index.into())?; | ||
| let traces = self.transaction_executions_in_range(index.clone().into())?; |
| .get::<tables::ContractInfoChangeSet>(addr)? | ||
| .ok_or(ProviderError::MissingContractInfoChangeSet { address: addr })?; | ||
| let new_change_set = | ||
| if let Some(mut change_set) = self.0.get::<tables::ContractInfoChangeSet>(addr)? { |
There was a problem hiding this comment.
fix - karyi- already on main
| Err(err) => Err(err), | ||
| } | ||
| let local_latest = match self.local_db.latest_number() { | ||
| Ok(num) => num, |
There was a problem hiding this comment.
fix - to run fork without any block
| let fork_point = self.block_id(); | ||
| let latest_num = self.latest_number()?; | ||
|
|
||
| if latest_num > fork_point { |
There was a problem hiding this comment.
fix - to run fork without any block
| &self, | ||
| block_id: BlockHashOrNumber, | ||
| ) -> ProviderResult<Option<Vec<TypedTransactionExecutionInfo>>> { | ||
| ) -> ProviderResult<Option<Vec<TxTraceWithHash>>> { |
| executions: Vec<TypedTransactionExecutionInfo>, | ||
| ) -> ProviderResult<()> { | ||
| // BUGFIX: Before inserting state updates, ensure all contracts referenced in nonce_updates | ||
| // have their ContractInfo in local_db. For forked contracts, the class_hash may only exist |
| let local_latest = match self.local_provider.0.latest_number() { | ||
| Ok(num) => num, | ||
| Err(ProviderError::MissingLatestBlockNumber) => fork_point, | ||
| Err(err) => return Err(err), |
There was a problem hiding this comment.
fix to run fork without genesis
| // TEMPFIX: | ||
| // | ||
| // This check is required due to the limitation on how we're storing updates for | ||
| // contracts that were deployed before the fork point. For those contracts, |
| if let res @ Some(..) = self.local_provider.nonce(address)? { | ||
| Ok(res) | ||
| if let Some(nonce) = self.local_provider.nonce(address)? { | ||
| // TEMPFIX: |
| let fork_point = self.fork_provider.block_id; | ||
| let latest_block_number = match self.local_provider.0.latest_number() { | ||
| Ok(num) => num, | ||
| // return the fork block number if local db return this error. this can only happen whne |
There was a problem hiding this comment.
fix to run forking without genesis
| Err(ProviderError::MissingLatestBlockNumber) => self.fork_provider.block_id, | ||
| Err(err) => return Err(err), | ||
| }; | ||
| let latest_block_number = self.latest_block_number()?; |
There was a problem hiding this comment.
fix to run forking without genesis
| Err(ProviderError::MissingLatestBlockNumber) => self.fork_provider.block_id, | ||
| Err(err) => return Err(err), | ||
| }; | ||
| let latest_block_number = self.latest_block_number()?; |
There was a problem hiding this comment.
fix to run forking without genesis
| Err(ProviderError::MissingLatestBlockNumber) => self.fork_provider.block_id, | ||
| Err(err) => return Err(err), | ||
| }; | ||
| let latest_block_number = self.latest_block_number()?; |
There was a problem hiding this comment.
fix to run forking without genesis
| Err(ProviderError::MissingLatestBlockNumber) => self.fork_provider.block_id, | ||
| Err(err) => return Err(err), | ||
| }; | ||
| let latest_block_number = self.latest_block_number()?; |
There was a problem hiding this comment.
fix to run forking without genesis
| Err(ProviderError::MissingLatestBlockNumber) => self.fork_provider.block_id, | ||
| Err(err) => return Err(err), | ||
| }; | ||
| let latest_block_number = self.latest_block_number()?; |
There was a problem hiding this comment.
fix to run forking without genesis
| // TODO: this is technically wrong, we probably should insert the | ||
| // `ClassChangeHistory` entry on the state update level instead. | ||
| let entry = ContractClassChange::deployed(address, hash); | ||
|
|
There was a problem hiding this comment.
fix - this was changing historical class hashes
| let block_id = self.target_block(); | ||
|
|
||
| let provider_mut = self.fork_provider.db.provider_mut(); | ||
| provider_mut.tx().put::<tables::NonceChangeHistory>(block, entry)?; |
There was a problem hiding this comment.
fix - this was changing historical nonces
| return Ok(None); | ||
| } | ||
|
|
||
| if let class @ Some(..) = |
There was a problem hiding this comment.
fix - here we want to get values from fork point not the current fork block as it might not exist on main instance
|
|
||
| if let Some(compiled_hash) = | ||
| self.fork_provider.backend.get_compiled_class_hash(hash, self.local_provider.block())? | ||
| self.fork_provider.backend.get_compiled_class_hash(hash, self.fork_provider.block_id)? |
There was a problem hiding this comment.
fix - here we want to get values from fork point not the current fork block as it might not exist on main instance
| let provider_mut = self.fork_provider.db.provider_mut(); | ||
| provider_mut.tx().put::<tables::StorageChangeSet>(key, block_list)?; | ||
| provider_mut.tx().put::<tables::StorageChangeHistory>(block, change_entry)?; | ||
| provider_mut.commit()?; |
There was a problem hiding this comment.
fix - this was changing historical storage values
|
|
||
| assert_eq!(actual_block_env, Some(expected_block_env)); | ||
| let expected_executions: Vec<TxTraceWithHash> = expected_block | ||
| .body |
| .contracts_proof | ||
| .contract_leaves_data | ||
| .iter() | ||
| .zip(contract_addresses.iter()) |
There was a problem hiding this comment.
this zip might cause problems, need better solution
e3bd68b to
e897bbb
Compare
Description
This PR enables Katana to fork from a remote Starknet network and continue producing blocks locally with correct state root computation.
The core challenge solved here is maintaining valid Merkle proofs when working with partial state.
Why these changes were needed
When forking from a remote network, Katana doesn't have the complete state trie locally—it only has the data it explicitly fetches. This creates a fundamental problem:
State root computation requires full tries
To compute a valid state root after applying local transactions, you need access to the entire Merkle trie structure.
Fetching the entire state is impractical
A full Starknet state is large, making it impossible to download during fork initialization.
Naive approaches fail
Simply applying state updates locally without the underlying trie structure produces incorrect state roots, breaking consensus validation.
The solution: Partial Tries with Multiproofs
This PR introduces partial trie support, allowing Katana to:
On the first iteration, when creating a fork, we construct partial tries based solely on multiproofs fetched from the RPC, using its latest roots and the paths to the leaves we want to insert. On subsequent iterations, we use the locally constructed partial tries and their values. For nodes that do not exist in the partial tries but do exist on the RPC, we lazily fetch them using proofs. This ensures that the state root matches what the RPC’s state root would be if the same values were inserted there.
Why the API changed
The
TrieWritertrait now includes acompute_state_rootmethod that can be overridden byForkedProvider.This is necessary because:
SNOS compatibility
These changes also enable SNOS (Starknet OS) proof generation for forked chains.
SNOS requires access to storage proofs to generate validity proofs, which is now possible through the multiproof infrastructure.
Testing