Skip to content

feat: Add FPI support to NtxDataStore#1521

Merged
bobbinth merged 41 commits intonextfrom
sergerad-ntx-builder-fpi
Jan 22, 2026
Merged

feat: Add FPI support to NtxDataStore#1521
bobbinth merged 41 commits intonextfrom
sergerad-ntx-builder-fpi

Conversation

@sergerad
Copy link
Collaborator

@sergerad sergerad commented Jan 16, 2026

Context

The NTX Builder's NtxDataStore does not handle foreign accounts for the purposes of the following functions of DataStore trait:

  • get_foreign_account_inputs()
  • get_foreign_account_inputs()
  • get_vault_asset_witnesses()

Closes #1387.

Changes

  • Updates NtxDataStore to handle foreign accounts in the above-listed functions.
  • Update NTX Builder store client to support get_account() like the RPC client.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@sergerad sergerad requested a review from bobbinth January 21, 2026 03:21
@sergerad sergerad requested a review from bobbinth January 21, 2026 21:29
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thank you! I left a few more comments inline - all a pretty small.

@sergerad sergerad requested a review from bobbinth January 22, 2026 03:27
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good! Thank you!

Copy link
Collaborator

@Mirko-von-Leipzig Mirko-von-Leipzig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +462 to +469
// 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",
));
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we essentially relying on knowing the order of operations of the caller of data store?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Seems like you can drop the lock by this point

Comment on lines +416 to +429
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 336 to 357
/// 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>>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded this comment. cc @Mirko-von-Leipzig.

@bobbinth bobbinth merged commit b7a458c into next Jan 22, 2026
7 checks passed
@bobbinth bobbinth deleted the sergerad-ntx-builder-fpi branch January 22, 2026 18:44
Copy link
Collaborator

@igamigo igamigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay, LGTM! I left some comments, not sure if it's worth addressing any of them right now

Comment on lines +416 to +429
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment on lines +416 to +429
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can use Vec::with_capacity here

Comment on lines +462 to +469
// 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",
));
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Seems like you can drop the lock by this point

TransactionMastStore,
TransactionProverError,
};
use tokio::sync::Mutex;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make FPI call possible during network transaction

4 participants