miden_tx/executor/
exec_host.rs

1use alloc::collections::BTreeMap;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5use miden_lib::transaction::{EventId, TransactionAdviceInputs};
6use miden_objects::account::auth::PublicKeyCommitment;
7use miden_objects::account::{AccountCode, AccountDelta, AccountId, PartialAccount};
8use miden_objects::assembly::debuginfo::Location;
9use miden_objects::assembly::{SourceFile, SourceManagerSync, SourceSpan};
10use miden_objects::asset::{Asset, AssetVaultKey, AssetWitness, FungibleAsset};
11use miden_objects::block::BlockNumber;
12use miden_objects::crypto::merkle::SmtProof;
13use miden_objects::note::{NoteInputs, NoteMetadata, NoteRecipient};
14use miden_objects::transaction::{InputNote, InputNotes, OutputNote};
15use miden_objects::vm::AdviceMap;
16use miden_objects::{Felt, Hasher, Word};
17use miden_processor::{
18    AdviceMutation,
19    AsyncHost,
20    BaseHost,
21    EventError,
22    FutureMaybeSend,
23    MastForest,
24    ProcessState,
25};
26
27use crate::auth::{SigningInputs, TransactionAuthenticator};
28use crate::errors::TransactionKernelError;
29use crate::host::note_builder::OutputNoteBuilder;
30use crate::host::{
31    ScriptMastForestStore,
32    TransactionBaseHost,
33    TransactionEventData,
34    TransactionEventHandling,
35    TransactionProgress,
36};
37use crate::{AccountProcedureIndexMap, DataStore, DataStoreError};
38
39// TRANSACTION EXECUTOR HOST
40// ================================================================================================
41
42/// The transaction executor host is responsible for handling [`FutureMaybeSend`] requests made by
43/// the transaction kernel during execution. In particular, it responds to signature generation
44/// requests by forwarding the request to the contained [`TransactionAuthenticator`].
45///
46/// Transaction hosts are created on a per-transaction basis. That is, a transaction host is meant
47/// to support execution of a single transaction and is discarded after the transaction finishes
48/// execution.
49pub struct TransactionExecutorHost<'store, 'auth, STORE, AUTH>
50where
51    STORE: DataStore,
52    AUTH: TransactionAuthenticator,
53{
54    /// The underlying base transaction host.
55    base_host: TransactionBaseHost<'store, STORE>,
56
57    /// Serves signature generation requests from the transaction runtime for signatures which are
58    /// not present in the `generated_signatures` field.
59    authenticator: Option<&'auth AUTH>,
60
61    /// The reference block of the transaction.
62    ref_block: BlockNumber,
63
64    /// The foreign account code that was lazy loaded during transaction execution.
65    ///
66    /// This is required for re-executing the transaction, e.g. as part of transaction proving.
67    accessed_foreign_account_code: Vec<AccountCode>,
68
69    /// Contains generated signatures (as a message |-> signature map) required for transaction
70    /// execution. Once a signature was created for a given message, it is inserted into this map.
71    /// After transaction execution, these can be inserted into the advice inputs to re-execute the
72    /// transaction without having to regenerate the signature or requiring access to the
73    /// authenticator that produced it.
74    generated_signatures: BTreeMap<Word, Vec<Felt>>,
75
76    /// The source manager to track source code file span information, improving any MASM related
77    /// error messages.
78    source_manager: Arc<dyn SourceManagerSync>,
79}
80
81impl<'store, 'auth, STORE, AUTH> TransactionExecutorHost<'store, 'auth, STORE, AUTH>
82where
83    STORE: DataStore + Sync,
84    AUTH: TransactionAuthenticator + Sync,
85{
86    // CONSTRUCTORS
87    // --------------------------------------------------------------------------------------------
88
89    /// Creates a new [`TransactionExecutorHost`] instance from the provided inputs.
90    pub fn new(
91        account: &PartialAccount,
92        input_notes: InputNotes<InputNote>,
93        mast_store: &'store STORE,
94        scripts_mast_store: ScriptMastForestStore,
95        acct_procedure_index_map: AccountProcedureIndexMap,
96        authenticator: Option<&'auth AUTH>,
97        ref_block: BlockNumber,
98        source_manager: Arc<dyn SourceManagerSync>,
99    ) -> Self {
100        let base_host = TransactionBaseHost::new(
101            account,
102            input_notes,
103            mast_store,
104            scripts_mast_store,
105            acct_procedure_index_map,
106        );
107
108        Self {
109            base_host,
110            authenticator,
111            ref_block,
112            accessed_foreign_account_code: Vec::new(),
113            generated_signatures: BTreeMap::new(),
114            source_manager,
115        }
116    }
117
118    // PUBLIC ACCESSORS
119    // --------------------------------------------------------------------------------------------
120
121    /// Returns a reference to the `tx_progress` field of this transaction host.
122    pub fn tx_progress(&self) -> &TransactionProgress {
123        self.base_host.tx_progress()
124    }
125
126    // EVENT HANDLERS
127    // --------------------------------------------------------------------------------------------
128
129    /// Handles a request for a foreign account by querying the data store for its account inputs.
130    async fn on_foreign_account_requested(
131        &mut self,
132        foreign_account_id: AccountId,
133    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
134        let foreign_account_inputs = self
135            .base_host
136            .store()
137            .get_foreign_account_inputs(foreign_account_id, self.ref_block)
138            .await
139            .map_err(|err| TransactionKernelError::GetForeignAccountInputs {
140                foreign_account_id,
141                ref_block: self.ref_block,
142                source: err,
143            })?;
144
145        let mut tx_advice_inputs = TransactionAdviceInputs::default();
146        tx_advice_inputs
147            .add_foreign_accounts([&foreign_account_inputs])
148            .map_err(|err| {
149                TransactionKernelError::other_with_source(
150                    format!(
151                        "failed to construct advice inputs for foreign account {}",
152                        foreign_account_inputs.id()
153                    ),
154                    err,
155                )
156            })?;
157
158        self.base_host
159            .load_foreign_account_code(foreign_account_inputs.code())
160            .map_err(|err| {
161                TransactionKernelError::other_with_source(
162                    format!(
163                        "failed to insert account procedures for foreign account {}",
164                        foreign_account_inputs.id()
165                    ),
166                    err,
167                )
168            })?;
169
170        // Add the foreign account's code to the list of accessed code.
171        self.accessed_foreign_account_code.push(foreign_account_inputs.code().clone());
172
173        Ok(tx_advice_inputs.into_advice_mutations().collect())
174    }
175
176    /// Pushes a signature to the advice stack as a response to the `AuthRequest` event.
177    ///
178    /// The signature is requested from the host's authenticator.
179    pub async fn on_auth_requested(
180        &mut self,
181        pub_key_hash: Word,
182        signing_inputs: SigningInputs,
183    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
184        let authenticator =
185            self.authenticator.ok_or(TransactionKernelError::MissingAuthenticator)?;
186
187        let signature: Vec<Felt> = authenticator
188            .get_signature(PublicKeyCommitment::from(pub_key_hash), &signing_inputs)
189            .await
190            .map_err(TransactionKernelError::SignatureGenerationFailed)?
191            .to_prepared_signature();
192
193        let signature_key = Hasher::merge(&[pub_key_hash, signing_inputs.to_commitment()]);
194
195        self.generated_signatures.insert(signature_key, signature.clone());
196
197        Ok(vec![AdviceMutation::extend_stack(signature)])
198    }
199
200    /// Handles the [`TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount`] and returns an error
201    /// if the account cannot pay the fee.
202    async fn on_before_tx_fee_removed_from_account(
203        &self,
204        fee_asset: FungibleAsset,
205    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
206        let asset_witness = self
207            .base_host
208            .store()
209            .get_vault_asset_witness(
210                self.base_host.initial_account_header().id(),
211                self.base_host.initial_account_header().vault_root(),
212                fee_asset.vault_key(),
213            )
214            .await
215            .map_err(|err| TransactionKernelError::GetVaultAssetWitness {
216                vault_root: self.base_host.initial_account_header().vault_root(),
217                asset_key: fee_asset.vault_key(),
218                source: err,
219            })?;
220
221        // Find fee asset in the witness or default to 0 if it isn't present.
222        let initial_fee_asset = asset_witness
223            .find(fee_asset.vault_key())
224            .and_then(|asset| match asset {
225                Asset::Fungible(fungible_asset) => Some(fungible_asset),
226                _ => None,
227            })
228            .unwrap_or(
229                FungibleAsset::new(fee_asset.faucet_id(), 0)
230                    .expect("fungible asset created from fee asset should be valid"),
231            );
232
233        // Compute the current balance of the native asset in the account based on the initial value
234        // and the delta.
235        let current_fee_asset = {
236            let fee_asset_amount_delta = self
237                .base_host
238                .account_delta_tracker()
239                .vault_delta()
240                .fungible()
241                .amount(&initial_fee_asset.faucet_id())
242                .unwrap_or(0);
243
244            // SAFETY: Initial native asset faucet ID should be a fungible faucet and amount should
245            // be less than MAX_AMOUNT as checked by the account delta.
246            let fee_asset_delta = FungibleAsset::new(
247                initial_fee_asset.faucet_id(),
248                fee_asset_amount_delta.unsigned_abs(),
249            )
250            .expect("faucet ID and amount should be valid");
251
252            // SAFETY: These computations are essentially the same as the ones executed by the
253            // transaction kernel, which should have aborted if they weren't valid.
254            if fee_asset_amount_delta > 0 {
255                initial_fee_asset
256                    .add(fee_asset_delta)
257                    .expect("transaction kernel should ensure amounts do not exceed MAX_AMOUNT")
258            } else {
259                initial_fee_asset
260                    .sub(fee_asset_delta)
261                    .expect("transaction kernel should ensure amount is not negative")
262            }
263        };
264
265        // Return an error if the balance in the account does not cover the fee.
266        if current_fee_asset.amount() < fee_asset.amount() {
267            return Err(TransactionKernelError::InsufficientFee {
268                account_balance: current_fee_asset.amount(),
269                tx_fee: fee_asset.amount(),
270            });
271        }
272
273        Ok(asset_witness_to_advice_mutation(asset_witness))
274    }
275
276    /// Handles a request for a storage map witness by querying the data store for a merkle path.
277    ///
278    /// Note that we request witnesses against the _initial_ map root of the accounts. See also
279    /// [`Self::on_account_vault_asset_witness_requested`] for more on this topic.
280    async fn on_account_storage_map_witness_requested(
281        &self,
282        current_account_id: AccountId,
283        map_root: Word,
284        map_key: Word,
285    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
286        let storage_map_witness = self
287            .base_host
288            .store()
289            .get_storage_map_witness(current_account_id, map_root, map_key)
290            .await
291            .map_err(|err| TransactionKernelError::GetStorageMapWitness {
292                map_root,
293                map_key,
294                source: err,
295            })?;
296
297        // Get the nodes in the proof and insert them into the merkle store.
298        let merkle_store_ext =
299            AdviceMutation::extend_merkle_store(storage_map_witness.authenticated_nodes());
300
301        let smt_proof = SmtProof::from(storage_map_witness);
302        let map_ext = AdviceMutation::extend_map(AdviceMap::from_iter([(
303            smt_proof.leaf().hash(),
304            smt_proof.leaf().to_elements(),
305        )]));
306
307        Ok(vec![merkle_store_ext, map_ext])
308    }
309
310    /// Handles a request to an asset witness by querying the data store for a merkle path.
311    ///
312    /// ## Native Account
313    ///
314    /// For the native account we always request witnesses for the initial vault root, because the
315    /// data store only has the state of the account vault at the beginning of the transaction.
316    /// Since the vault root can change as the transaction progresses, this means the witnesses
317    /// may become _partially_ or fully outdated. To see why they can only be _partially_ outdated,
318    /// consider the following example:
319    ///
320    /// ```text
321    ///      A               A'
322    ///     / \             /  \
323    ///    B   C    ->    B'    C
324    ///   / \  / \       /  \  / \
325    ///  D  E F   G     D   E' F  G
326    /// ```
327    ///
328    /// Leaf E was updated to E', in turn updating nodes B and A. If we now request the merkle path
329    /// to G against root A (the initial vault root), we'll get nodes F and B. F is a node in the
330    /// updated tree, while B is not. We insert both into the merkle store anyway. Now, if the
331    /// transaction attempts to verify the merkle path to G, it can do so because F and B' are in
332    /// the merkle store. Note that B' is in the store because the transaction inserted it into the
333    /// merkle store as part of updating E, not because we inserted it. B is present in the store,
334    /// but is simply ignored for the purpose of verifying G's inclusion.
335    ///
336    /// ## Foreign Accounts
337    ///
338    /// Foreign accounts are read-only and so they cannot change throughout transaction execution.
339    /// This means their _current_ vault root is always equivalent to their _initial_ vault root.
340    /// So, for foreign accounts, just like for the native account, we also always request
341    /// witnesses for the initial vault root.
342    async fn on_account_vault_asset_witness_requested(
343        &self,
344        current_account_id: AccountId,
345        vault_root: Word,
346        asset_key: AssetVaultKey,
347    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
348        let asset_witness = self
349            .base_host
350            .store()
351            .get_vault_asset_witness(current_account_id, vault_root, asset_key)
352            .await
353            .map_err(|err| TransactionKernelError::GetVaultAssetWitness {
354                vault_root,
355                asset_key,
356                source: err,
357            })?;
358
359        Ok(asset_witness_to_advice_mutation(asset_witness))
360    }
361
362    /// Handles a request for a [`NoteScript`] by querying the [`DataStore`].
363    ///
364    /// The script is fetched from the data store and used to build a [`NoteRecipient`], which is
365    /// then used to create an [`OutputNoteBuilder`]. This function is only called for public notes
366    /// where the script is not already available in the advice provider.
367    async fn on_note_script_requested(
368        &mut self,
369        script_root: Word,
370        metadata: NoteMetadata,
371        recipient_digest: Word,
372        note_idx: usize,
373        note_inputs: NoteInputs,
374        serial_num: Word,
375    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
376        let note_script_result = self.base_host.store().get_note_script(script_root).await;
377
378        let (recipient, mutations) = match note_script_result {
379            Ok(note_script) => {
380                let script_felts: Vec<Felt> = (&note_script).into();
381                let recipient = NoteRecipient::new(serial_num, note_script, note_inputs);
382                let mutations = vec![AdviceMutation::extend_map(AdviceMap::from_iter([(
383                    script_root,
384                    script_felts,
385                )]))];
386
387                (Some(recipient), mutations)
388            },
389            Err(DataStoreError::NoteScriptNotFound(_)) if metadata.is_private() => {
390                (None, Vec::new())
391            },
392            Err(DataStoreError::NoteScriptNotFound(_)) => {
393                return Err(TransactionKernelError::other(format!(
394                    "note script with root {script_root} not found in data store for public note"
395                )));
396            },
397            Err(err) => {
398                return Err(TransactionKernelError::other_with_source(
399                    "failed to retrieve note script from data store",
400                    err,
401                ));
402            },
403        };
404
405        let note_builder = OutputNoteBuilder::new(metadata, recipient_digest, recipient)?;
406        self.base_host.insert_output_note_builder(note_idx, note_builder)?;
407
408        Ok(mutations)
409    }
410
411    /// Consumes `self` and returns the account delta, output notes, generated signatures and
412    /// transaction progress.
413    #[allow(clippy::type_complexity)]
414    pub fn into_parts(
415        self,
416    ) -> (
417        AccountDelta,
418        InputNotes<InputNote>,
419        Vec<OutputNote>,
420        Vec<AccountCode>,
421        BTreeMap<Word, Vec<Felt>>,
422        TransactionProgress,
423    ) {
424        let (account_delta, input_notes, output_notes, tx_progress) = self.base_host.into_parts();
425
426        (
427            account_delta,
428            input_notes,
429            output_notes,
430            self.accessed_foreign_account_code,
431            self.generated_signatures,
432            tx_progress,
433        )
434    }
435}
436
437// HOST IMPLEMENTATION
438// ================================================================================================
439
440impl<STORE, AUTH> BaseHost for TransactionExecutorHost<'_, '_, STORE, AUTH>
441where
442    STORE: DataStore,
443    AUTH: TransactionAuthenticator,
444{
445    fn get_label_and_source_file(
446        &self,
447        location: &Location,
448    ) -> (SourceSpan, Option<Arc<SourceFile>>) {
449        let source_manager = self.source_manager.as_ref();
450        let maybe_file = source_manager.get_by_uri(location.uri());
451        let span = source_manager.location_to_span(location.clone()).unwrap_or_default();
452        (span, maybe_file)
453    }
454}
455
456impl<STORE, AUTH> AsyncHost for TransactionExecutorHost<'_, '_, STORE, AUTH>
457where
458    STORE: DataStore + Sync,
459    AUTH: TransactionAuthenticator + Sync,
460{
461    fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend<Option<Arc<MastForest>>> {
462        let mast_forest = self.base_host.get_mast_forest(node_digest);
463        async move { mast_forest }
464    }
465
466    fn on_event(
467        &mut self,
468        process: &ProcessState,
469    ) -> impl FutureMaybeSend<Result<Vec<AdviceMutation>, EventError>> {
470        let event_id = EventId::from_felt(process.get_stack_item(0));
471
472        // TODO: Eventually, refactor this to let TransactionEvent contain the data directly, which
473        // should be cleaner.
474        let event_handling_result = self.base_host.handle_event(process, event_id);
475
476        async move {
477            let event_handling = event_handling_result?;
478            let event_data = match event_handling {
479                TransactionEventHandling::Unhandled(event) => event,
480                TransactionEventHandling::Handled(mutations) => {
481                    return Ok(mutations);
482                },
483            };
484
485            match event_data {
486                TransactionEventData::AuthRequest { pub_key_hash, signing_inputs } => self
487                    .on_auth_requested(pub_key_hash, signing_inputs)
488                    .await
489                    .map_err(EventError::from),
490                TransactionEventData::TransactionFeeComputed { fee_asset } => self
491                    .on_before_tx_fee_removed_from_account(fee_asset)
492                    .await
493                    .map_err(EventError::from),
494                TransactionEventData::ForeignAccount { account_id } => {
495                    self.on_foreign_account_requested(account_id).await.map_err(EventError::from)
496                },
497                TransactionEventData::AccountVaultAssetWitness {
498                    current_account_id,
499                    vault_root,
500                    asset_key,
501                } => self
502                    .on_account_vault_asset_witness_requested(
503                        current_account_id,
504                        vault_root,
505                        asset_key,
506                    )
507                    .await
508                    .map_err(EventError::from),
509                TransactionEventData::AccountStorageMapWitness {
510                    current_account_id,
511                    map_root,
512                    map_key,
513                } => self
514                    .on_account_storage_map_witness_requested(current_account_id, map_root, map_key)
515                    .await
516                    .map_err(EventError::from),
517                TransactionEventData::NoteData {
518                    note_idx,
519                    metadata,
520                    script_root,
521                    recipient_digest,
522                    note_inputs,
523                    serial_num,
524                } => self
525                    .on_note_script_requested(
526                        script_root,
527                        metadata,
528                        recipient_digest,
529                        note_idx,
530                        note_inputs,
531                        serial_num,
532                    )
533                    .await
534                    .map_err(EventError::from),
535            }
536        }
537    }
538}
539
540// HELPER FUNCTIONS
541// ================================================================================================
542
543/// Converts an [`AssetWitness`] into the set of advice mutations that need to be inserted in order
544/// to access the asset.
545fn asset_witness_to_advice_mutation(asset_witness: AssetWitness) -> Vec<AdviceMutation> {
546    // Get the nodes in the proof and insert them into the merkle store.
547    let merkle_store_ext = AdviceMutation::extend_merkle_store(asset_witness.authenticated_nodes());
548
549    let smt_proof = SmtProof::from(asset_witness);
550    let map_ext = AdviceMutation::extend_map(AdviceMap::from_iter([(
551        smt_proof.leaf().hash(),
552        smt_proof.leaf().to_elements(),
553    )]));
554
555    vec![merkle_store_ext, map_ext]
556}