refactor(store): get account vault details from account state forest#1981
refactor(store): get account vault details from account state forest#1981
Conversation
GetAccount.details.vault_details now comes from AccountStateForest instead of select_account_vault_at_block(). I added a historical vault enumeration API to AccountStateForest and switched the call site in State::fetch_public_account_details() to use it. The rest of the details path is unchanged: header, storage header, code lookup, and storage map assembly still use the existing SQLite logic.
There was a problem hiding this comment.
Pull request overview
Refactors the store’s GetAccount path to reconstruct account vault assets from the in-memory AccountStateForest instead of querying SQLite, aligning with #1978’s performance goals by reducing DB round-trips.
Changes:
- Switch
State::fetch_public_account_details()vault-details lookup fromselect_account_vault_at_block()toAccountStateForest::get_vault_details(). - Add
AccountStateForest::get_vault_details(account_id, block_num)API to enumerate historical vault contents. - Add unit tests for latest/historical vault enumeration and
MAX_RETURN_ENTRIESlimit handling; update changelog entry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| crates/store/src/state/mod.rs | Uses AccountStateForest for vault_details reconstruction (and updates tracing spans). |
| crates/store/src/account_state_forest/mod.rs | Adds get_vault_details() vault enumeration at a given block. |
| crates/store/src/account_state_forest/tests.rs | Adds tests covering historical/latest vault assets and limit-exceeded behavior. |
| CHANGELOG.md | Notes the GetAccount vault-assets optimization for the next release. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let assets = entries | ||
| .map(|entry| Asset::from_key_value_words(entry.key, entry.value)) | ||
| .collect::<Result<Vec<_>, _>>()?; | ||
|
|
There was a problem hiding this comment.
get_vault_details() collects all vault entries into a Vec before applying AccountVaultDetails::MAX_RETURN_ENTRIES. For large vaults this can allocate and parse far more than needed (and holds the forest read-lock longer), even though the response will be LimitExceeded. Consider iterating with an early cutoff (e.g., stop after MAX_RETURN_ENTRIES + 1 entries) and return LimitExceeded without collecting the full vault.
| let assets = entries | |
| .map(|entry| Asset::from_key_value_words(entry.key, entry.value)) | |
| .collect::<Result<Vec<_>, _>>()?; | |
| let mut assets = Vec::new(); | |
| for entry in entries.take(AccountVaultDetails::MAX_RETURN_ENTRIES + 1) { | |
| assets.push(Asset::from_key_value_words(entry.key, entry.value)?); | |
| } |
There was a problem hiding this comment.
I think this is a valid concern and the way to deal with this is to first check how many entries a given tree has, and iterate over the entries only if the number of entries is under the threshold. Checking the number of entries used to be expensive, but with 0xMiden/crypto#916 it should now be much cheaper (though, I don't remember if 0xMiden/crypto#916 made it into v0.24 release of miden-crypto or not).
There was a problem hiding this comment.
That optimization is in v0.24, but we're still on miden-crypto v0.23 with main AFAIU. Does that mean that the entry_count() call is too expensive to use as a pre-check here?
There was a problem hiding this comment.
The v0.23 implementation iterates over all entries to get the count - so, using it would result in two passes over the entries here in most cases. We could try to take this hit now. We could also merge this code into main and when porting back into next update it to use the entry_count() which will become efficient once we update to the next version of miden-crypto (hopefully, in a couple of weeks).
There was a problem hiding this comment.
Since we're pulling entries by using an iterator we can make sure we never fetch more than MAX_RETURN_ENTRIES + 1 entries from the tree. I've also added a TODO for the follow-up work.
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
LGTM, maybe we do want that pre-check that co-pilot suggested?
| let vault_assets = | ||
| self.db.select_account_vault_at_block(account_id, block_num).await?; |
There was a problem hiding this comment.
Do we still need select_account_vault_at_block() function after this PR? Or could we get rid of entirely?
There was a problem hiding this comment.
It's still used by tests checking that upsert_accounts() does the right thing. Since we're still keeping all vault data in SQLite I'd keep these tests and just move select_account_vault_at_block() into the tests module so that it can still be used by tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .map_err(|err| { | ||
| DatabaseError::DataCorrupted(format!( | ||
| "failed to reconstruct vault for account {account_id} at block {block_num}: {err}" | ||
| )) |
There was a problem hiding this comment.
get_vault_details() errors are all being flattened into DatabaseError::DataCorrupted(...), which loses the original error kind (e.g. Merkle vs asset parsing vs missing root) and makes troubleshooting harder. Consider matching on WitnessError and mapping each variant to the closest DatabaseError variant (e.g. MerkleError/AssetError/StorageMapError), using DataCorrupted only for the truly invariant-violating cases (like RootNotFound).
| .map_err(|err| { | |
| DatabaseError::DataCorrupted(format!( | |
| "failed to reconstruct vault for account {account_id} at block {block_num}: {err}" | |
| )) | |
| .map_err(|err| match err { | |
| WitnessError::MerkleError(err) => DatabaseError::MerkleError(err), | |
| WitnessError::AssetError(err) => DatabaseError::AssetError(err), | |
| WitnessError::StorageMapError(err) => DatabaseError::StorageMapError(err), | |
| WitnessError::RootNotFound(root) => DatabaseError::DataCorrupted(format!( | |
| "failed to reconstruct vault for account {account_id} at block {block_num}: missing root {root}" | |
| )), |
As outlined in #1978 we can avoid some DB queries since we have most account details in
AccountStateForest. We were already doing that for specific storage map key requests, but account vault assets were still queried from the database even though we do have those inAccountStateForestas well.This PR implements the
vault_detailslookup to useAccountStateForestinstead ofselect_account_vault_at_block(). I added a historical vault enumeration API toAccountStateForestand switched the call site inState::fetch_public_account_details()to use it. The rest of the details path is unchanged.