Skip to main content

miden_client/transaction/request/
foreign.rs

1//! Contains structures and functions related to FPI (Foreign Procedure Invocation) transactions.
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::cmp::Ordering;
5
6use miden_protocol::account::{
7    AccountId,
8    PartialAccount,
9    PartialStorage,
10    PartialStorageMap,
11    StorageMap,
12    StorageMapKey,
13    StorageMapWitness,
14};
15use miden_protocol::asset::{AssetVault, PartialVault};
16use miden_protocol::crypto::merkle::smt::SmtProof;
17use miden_protocol::transaction::AccountInputs;
18use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
19
20use super::TransactionRequestError;
21use crate::rpc::domain::account::{
22    AccountDetails,
23    AccountProof,
24    AccountStorageRequirements,
25    StorageMapEntries,
26};
27
28// FOREIGN ACCOUNT
29// ================================================================================================
30
31/// Account types for foreign procedure invocation.
32#[derive(Clone, Debug, PartialEq, Eq)]
33#[allow(clippy::large_enum_variant)]
34pub enum ForeignAccount {
35    /// Account with public on-chain state (`Public` or `Network` storage mode) whose state and
36    /// code will be retrieved from the network at execution time. Declaring it upfront lets you
37    /// specify [`AccountStorageRequirements`] so the correct storage map entries are fetched in a
38    /// single RPC call. If not declared, the account is lazily loaded with empty storage
39    /// requirements, and any storage map accesses will trigger additional RPC calls during
40    /// execution.
41    Public(AccountId, AccountStorageRequirements),
42    /// Private account that requires a [`PartialAccount`] to be provided by the caller. An
43    /// account witness will be retrieved from the network at execution time so that it can be
44    /// used as inputs to the transaction kernel.
45    Private(PartialAccount),
46}
47
48impl ForeignAccount {
49    /// Creates a new [`ForeignAccount::Public`]. The account's components (code, storage header and
50    /// inclusion proof) will be retrieved at execution time, alongside particular storage slot
51    /// maps correspondent to keys passed in `indices`.
52    pub fn public(
53        account_id: AccountId,
54        storage_requirements: AccountStorageRequirements,
55    ) -> Result<Self, TransactionRequestError> {
56        if !account_id.has_public_state() {
57            return Err(TransactionRequestError::InvalidForeignAccountId(account_id));
58        }
59
60        Ok(Self::Public(account_id, storage_requirements))
61    }
62
63    /// Creates a new [`ForeignAccount::Private`]. A proof of the account's inclusion will be
64    /// retrieved at execution time.
65    pub fn private(account: impl Into<PartialAccount>) -> Result<Self, TransactionRequestError> {
66        let partial_account: PartialAccount = account.into();
67        if partial_account.id().has_public_state() {
68            return Err(TransactionRequestError::InvalidForeignAccountId(partial_account.id()));
69        }
70
71        Ok(Self::Private(partial_account))
72    }
73
74    pub fn storage_slot_requirements(&self) -> AccountStorageRequirements {
75        match self {
76            ForeignAccount::Public(_, account_storage_requirements) => {
77                account_storage_requirements.clone()
78            },
79            ForeignAccount::Private(_) => AccountStorageRequirements::default(),
80        }
81    }
82
83    /// Returns the foreign account's [`AccountId`].
84    pub fn account_id(&self) -> AccountId {
85        match self {
86            ForeignAccount::Public(account_id, _) => *account_id,
87            ForeignAccount::Private(partial_account) => partial_account.id(),
88        }
89    }
90}
91
92impl Ord for ForeignAccount {
93    fn cmp(&self, other: &Self) -> Ordering {
94        self.account_id().cmp(&other.account_id())
95    }
96}
97
98impl PartialOrd for ForeignAccount {
99    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
100        Some(self.cmp(other))
101    }
102}
103
104impl Serializable for ForeignAccount {
105    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
106        match self {
107            ForeignAccount::Public(account_id, storage_requirements) => {
108                target.write(0u8);
109                account_id.write_into(target);
110                storage_requirements.write_into(target);
111            },
112            ForeignAccount::Private(partial_account) => {
113                target.write(1u8);
114                partial_account.write_into(target);
115            },
116        }
117    }
118}
119
120impl Deserializable for ForeignAccount {
121    fn read_from<R: miden_tx::utils::serde::ByteReader>(
122        source: &mut R,
123    ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
124        let account_type: u8 = source.read_u8()?;
125        match account_type {
126            0 => {
127                let account_id = AccountId::read_from(source)?;
128                let storage_requirements = AccountStorageRequirements::read_from(source)?;
129                Ok(ForeignAccount::Public(account_id, storage_requirements))
130            },
131            1 => {
132                let foreign_inputs = PartialAccount::read_from(source)?;
133                Ok(ForeignAccount::Private(foreign_inputs))
134            },
135            _ => Err(DeserializationError::InvalidValue("Invalid account type".to_string())),
136        }
137    }
138}
139
140/// Converts an [`AccountProof`] to [`AccountInputs`].
141///
142/// The `storage_requirements` are needed to reassociate raw keys with the SMT proofs returned
143/// by the node (the node only sends hashed leaf keys, not the original raw keys).
144pub(crate) fn account_proof_into_inputs(
145    account_proof: AccountProof,
146    storage_requirements: &AccountStorageRequirements,
147) -> Result<AccountInputs, TransactionRequestError> {
148    let (witness, account_details) = account_proof.into_parts();
149
150    if let Some(AccountDetails {
151        header: account_header,
152        code,
153        storage_details,
154        vault_details,
155    }) = account_details
156    {
157        // discard slot indices - not needed for execution
158        let account_storage_map_details = storage_details.map_details;
159        let mut storage_map_proofs = Vec::with_capacity(account_storage_map_details.len());
160        for account_storage_detail in account_storage_map_details {
161            let partial_storage = match account_storage_detail.entries {
162                StorageMapEntries::AllEntries(entries) => {
163                    // Full map available - create from all entries
164                    let storage_entries_iter = entries.iter().map(|e| (e.key, e.value));
165                    PartialStorageMap::new_full(
166                        StorageMap::with_entries(storage_entries_iter)
167                            .map_err(TransactionRequestError::StorageMapError)?,
168                    )
169                },
170                StorageMapEntries::EntriesWithProofs(proofs) => {
171                    // Reassociate the proofs with the keys from storage requirements.
172                    let keys =
173                        storage_requirements.keys_for_slot(&account_storage_detail.slot_name);
174                    let witnesses = proofs_to_witnesses(proofs, keys)?;
175                    PartialStorageMap::with_witnesses(witnesses)?
176                },
177            };
178            storage_map_proofs.push(partial_storage);
179        }
180
181        let vault = AssetVault::new(&vault_details.assets)?;
182        return Ok(AccountInputs::new(
183            PartialAccount::new(
184                account_header.id(),
185                account_header.nonce(),
186                code,
187                PartialStorage::new(storage_details.header, storage_map_proofs)?,
188                PartialVault::new_full(vault),
189                None,
190            )?,
191            witness,
192        ));
193    }
194    Err(TransactionRequestError::ForeignAccountDataMissing)
195}
196
197/// Pairs each [`SmtProof`] with its corresponding key to produce [`StorageMapWitness`]es.
198///
199/// Proofs and keys are matched by position (the node returns proofs in the same order as
200/// the requested keys). [`StorageMapWitness::new`] validates each pair by hashing the key
201/// and checking that the proof's leaf covers it, so a mismatch will surface as a
202/// `StorageMapError::MissingKey` error.
203fn proofs_to_witnesses(
204    proofs: Vec<SmtProof>,
205    keys: &[StorageMapKey],
206) -> Result<Vec<StorageMapWitness>, TransactionRequestError> {
207    proofs
208        .into_iter()
209        .zip(keys)
210        .map(|(proof, key)| {
211            StorageMapWitness::new(proof, [*key]).map_err(TransactionRequestError::StorageMapError)
212        })
213        .collect()
214}