feat: Add FPI support to NtxDataStore#1521
Conversation
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left some comments inline - the main ones are about making the code a bit more NTX builder-specific, and also adding helper endpoints on the store to retrieve asset/map witnesses (in a different PR).
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left a few more comments inline - all a pretty small.
bobbinth
left a comment
There was a problem hiding this comment.
All looks good! Thank you!
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
Code looks good; I still dislike how datastore forces arcs and mutex's despite being run effectively sequentially. But that's not this PRs problem.
| // The slot name that corresponds to the given account ID and map root must have been | ||
| // registered during previous calls of this data store. | ||
| let storage_slots = self.storage_slots.lock().await; | ||
| let Some(slot_name) = storage_slots.get(&(account_id, map_root)) else { | ||
| return Err(DataStoreError::other( | ||
| "requested storage slot has not been registered", | ||
| )); | ||
| }; |
There was a problem hiding this comment.
Are we essentially relying on knowing the order of operations of the caller of data store?
There was a problem hiding this comment.
nit: Seems like you can drop the lock by this point
| async move { | ||
| debug_assert_eq!(ref_block, self.reference_block.block_num()); | ||
|
|
||
| // Get foreign account inputs from store. | ||
| let account_inputs = | ||
| self.store.get_account_inputs(foreign_account_id, ref_block).await.map_err( | ||
| |err| DataStoreError::other_with_source("failed to get account inputs", err), | ||
| )?; | ||
|
|
||
| // Register slot names from the foreign account for later use. | ||
| self.register_storage_map_slots(foreign_account_id, account_inputs.storage().header()) | ||
| .await; | ||
|
|
||
| Ok(account_inputs) |
There was a problem hiding this comment.
Following up on the conversation we had during the call, I actually don't think we need to add foreign account code to the MAST store in this function (i.e., nothing needs to change). The reason for this is that transaction host does this after calling DataStore::get_foreign_account_inputs().
Technically, I think we didn't have to do this in the validator either - but in the validator, we are planning to change how re-execution works and so, I'd leave it as is.
cc @igamigo to confirm.
There was a problem hiding this comment.
Looking at the code, I think this is right. But also, not sure why the validator was erroring during client integration FPI tests then (which prompted #1561).
There was a problem hiding this comment.
Oh actually it's because it never even calls DataStore::get_foreign_account_inputs().
Although then I wonder how you plan on doing away with adding the MAST forest in the validator.
| /// Mapping of storage map roots to storage slot names observed during various calls. | ||
| /// | ||
| /// The registered slot names are subsequently used to retrieve storage map witnesses from the | ||
| /// store. We need this because the store interface (and the underling SMT forest) use storage | ||
| /// slot names, but the `DataStore` interface works with tree roots. To get around this problem | ||
| /// we populate this map when: | ||
| /// - The the native account is loaded (in `get_transaction_inputs()`). | ||
| /// - When a foreign account is loaded (in `get_foreign_account_inputs`). | ||
| /// | ||
| /// The assumption here are: | ||
| /// - Once an account is loaded, the mapping between `(account_id, map_root)` and slot names | ||
| /// do not change. This is always the case. | ||
| /// - New storage slots created during transaction execution will not be accesses in the same | ||
| /// transaction. The mechanism for adding new storage slots is not implemented yet, but the | ||
| /// plan for it is consistent with this assumption. | ||
| /// | ||
| /// One nuance worth mentioning: it is possible that there could be a root collision where an | ||
| /// account has two storage maps with the same root. In this case, the map will contain only a | ||
| /// single entry with the storage slot name that was added last. Thus, technically, requests | ||
| /// to the store could be "wrong", but given that two identical maps have identical witnesses | ||
| /// this does not cause issues in practice. | ||
| storage_slots: Arc<Mutex<BTreeMap<(AccountId, Word), StorageSlotName>>>, |
igamigo
left a comment
There was a problem hiding this comment.
Sorry for the delay, LGTM! I left some comments, not sure if it's worth addressing any of them right now
| async move { | ||
| debug_assert_eq!(ref_block, self.reference_block.block_num()); | ||
|
|
||
| // Get foreign account inputs from store. | ||
| let account_inputs = | ||
| self.store.get_account_inputs(foreign_account_id, ref_block).await.map_err( | ||
| |err| DataStoreError::other_with_source("failed to get account inputs", err), | ||
| )?; | ||
|
|
||
| // Register slot names from the foreign account for later use. | ||
| self.register_storage_map_slots(foreign_account_id, account_inputs.storage().header()) | ||
| .await; | ||
|
|
||
| Ok(account_inputs) |
There was a problem hiding this comment.
Looking at the code, I think this is right. But also, not sure why the validator was erroring during client integration FPI tests then (which prompted #1561).
| async move { | ||
| debug_assert_eq!(ref_block, self.reference_block.block_num()); | ||
|
|
||
| // Get foreign account inputs from store. | ||
| let account_inputs = | ||
| self.store.get_account_inputs(foreign_account_id, ref_block).await.map_err( | ||
| |err| DataStoreError::other_with_source("failed to get account inputs", err), | ||
| )?; | ||
|
|
||
| // Register slot names from the foreign account for later use. | ||
| self.register_storage_map_slots(foreign_account_id, account_inputs.storage().header()) | ||
| .await; | ||
|
|
||
| Ok(account_inputs) |
There was a problem hiding this comment.
Oh actually it's because it never even calls DataStore::get_foreign_account_inputs().
Although then I wonder how you plan on doing away with adding the MAST forest in the validator.
| self.inner.clone().get_vault_asset_witnesses(request).await?.into_inner(); | ||
|
|
||
| // Convert the response to domain type. | ||
| let mut asset_witnesses = Vec::new(); |
There was a problem hiding this comment.
nit: can use Vec::with_capacity here
| // The slot name that corresponds to the given account ID and map root must have been | ||
| // registered during previous calls of this data store. | ||
| let storage_slots = self.storage_slots.lock().await; | ||
| let Some(slot_name) = storage_slots.get(&(account_id, map_root)) else { | ||
| return Err(DataStoreError::other( | ||
| "requested storage slot has not been registered", | ||
| )); | ||
| }; |
There was a problem hiding this comment.
nit: Seems like you can drop the lock by this point
| TransactionMastStore, | ||
| TransactionProverError, | ||
| }; | ||
| use tokio::sync::Mutex; |
There was a problem hiding this comment.
Because we know this lock is low-contention and also not held across awaits, I think an std::sync::Mutex would be lower overhead and overall faster
Context
The NTX Builder's
NtxDataStoredoes not handle foreign accounts for the purposes of the following functions ofDataStoretrait:get_foreign_account_inputs()get_foreign_account_inputs()get_vault_asset_witnesses()Closes #1387.
Changes
NtxDataStoreto handle foreign accounts in the above-listed functions.get_account()like the RPC client.