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        // get the message that will be signed by the authenticator
188        let message = signing_inputs.to_commitment();
189
190        let signature: Vec<Felt> = authenticator
191            .get_signature(PublicKeyCommitment::from(pub_key_hash), &signing_inputs)
192            .await
193            .map_err(TransactionKernelError::SignatureGenerationFailed)?
194            .to_prepared_signature(message);
195
196        let signature_key = Hasher::merge(&[pub_key_hash, message]);
197        self.generated_signatures.insert(signature_key, signature.clone());
198
199        Ok(vec![AdviceMutation::extend_stack(signature)])
200    }
201
202    /// Handles the [`TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount`] and returns an error
203    /// if the account cannot pay the fee.
204    async fn on_before_tx_fee_removed_from_account(
205        &self,
206        fee_asset: FungibleAsset,
207    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
208        let asset_witness = self
209            .base_host
210            .store()
211            .get_vault_asset_witness(
212                self.base_host.initial_account_header().id(),
213                self.base_host.initial_account_header().vault_root(),
214                fee_asset.vault_key(),
215            )
216            .await
217            .map_err(|err| TransactionKernelError::GetVaultAssetWitness {
218                vault_root: self.base_host.initial_account_header().vault_root(),
219                asset_key: fee_asset.vault_key(),
220                source: err,
221            })?;
222
223        // Find fee asset in the witness or default to 0 if it isn't present.
224        let initial_fee_asset = asset_witness
225            .find(fee_asset.vault_key())
226            .and_then(|asset| match asset {
227                Asset::Fungible(fungible_asset) => Some(fungible_asset),
228                _ => None,
229            })
230            .unwrap_or(
231                FungibleAsset::new(fee_asset.faucet_id(), 0)
232                    .expect("fungible asset created from fee asset should be valid"),
233            );
234
235        // Compute the current balance of the native asset in the account based on the initial value
236        // and the delta.
237        let current_fee_asset = {
238            let fee_asset_amount_delta = self
239                .base_host
240                .account_delta_tracker()
241                .vault_delta()
242                .fungible()
243                .amount(&initial_fee_asset.faucet_id())
244                .unwrap_or(0);
245
246            // SAFETY: Initial native asset faucet ID should be a fungible faucet and amount should
247            // be less than MAX_AMOUNT as checked by the account delta.
248            let fee_asset_delta = FungibleAsset::new(
249                initial_fee_asset.faucet_id(),
250                fee_asset_amount_delta.unsigned_abs(),
251            )
252            .expect("faucet ID and amount should be valid");
253
254            // SAFETY: These computations are essentially the same as the ones executed by the
255            // transaction kernel, which should have aborted if they weren't valid.
256            if fee_asset_amount_delta > 0 {
257                initial_fee_asset
258                    .add(fee_asset_delta)
259                    .expect("transaction kernel should ensure amounts do not exceed MAX_AMOUNT")
260            } else {
261                initial_fee_asset
262                    .sub(fee_asset_delta)
263                    .expect("transaction kernel should ensure amount is not negative")
264            }
265        };
266
267        // Return an error if the balance in the account does not cover the fee.
268        if current_fee_asset.amount() < fee_asset.amount() {
269            return Err(TransactionKernelError::InsufficientFee {
270                account_balance: current_fee_asset.amount(),
271                tx_fee: fee_asset.amount(),
272            });
273        }
274
275        Ok(asset_witness_to_advice_mutation(asset_witness))
276    }
277
278    /// Handles a request for a storage map witness by querying the data store for a merkle path.
279    ///
280    /// Note that we request witnesses against the _initial_ map root of the accounts. See also
281    /// [`Self::on_account_vault_asset_witness_requested`] for more on this topic.
282    async fn on_account_storage_map_witness_requested(
283        &self,
284        current_account_id: AccountId,
285        map_root: Word,
286        map_key: Word,
287    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
288        let storage_map_witness = self
289            .base_host
290            .store()
291            .get_storage_map_witness(current_account_id, map_root, map_key)
292            .await
293            .map_err(|err| TransactionKernelError::GetStorageMapWitness {
294                map_root,
295                map_key,
296                source: err,
297            })?;
298
299        // Get the nodes in the proof and insert them into the merkle store.
300        let merkle_store_ext =
301            AdviceMutation::extend_merkle_store(storage_map_witness.authenticated_nodes());
302
303        let smt_proof = SmtProof::from(storage_map_witness);
304        let map_ext = AdviceMutation::extend_map(AdviceMap::from_iter([(
305            smt_proof.leaf().hash(),
306            smt_proof.leaf().to_elements(),
307        )]));
308
309        Ok(vec![merkle_store_ext, map_ext])
310    }
311
312    /// Handles a request to an asset witness by querying the data store for a merkle path.
313    ///
314    /// ## Native Account
315    ///
316    /// For the native account we always request witnesses for the initial vault root, because the
317    /// data store only has the state of the account vault at the beginning of the transaction.
318    /// Since the vault root can change as the transaction progresses, this means the witnesses
319    /// may become _partially_ or fully outdated. To see why they can only be _partially_ outdated,
320    /// consider the following example:
321    ///
322    /// ```text
323    ///      A               A'
324    ///     / \             /  \
325    ///    B   C    ->    B'    C
326    ///   / \  / \       /  \  / \
327    ///  D  E F   G     D   E' F  G
328    /// ```
329    ///
330    /// Leaf E was updated to E', in turn updating nodes B and A. If we now request the merkle path
331    /// to G against root A (the initial vault root), we'll get nodes F and B. F is a node in the
332    /// updated tree, while B is not. We insert both into the merkle store anyway. Now, if the
333    /// transaction attempts to verify the merkle path to G, it can do so because F and B' are in
334    /// the merkle store. Note that B' is in the store because the transaction inserted it into the
335    /// merkle store as part of updating E, not because we inserted it. B is present in the store,
336    /// but is simply ignored for the purpose of verifying G's inclusion.
337    ///
338    /// ## Foreign Accounts
339    ///
340    /// Foreign accounts are read-only and so they cannot change throughout transaction execution.
341    /// This means their _current_ vault root is always equivalent to their _initial_ vault root.
342    /// So, for foreign accounts, just like for the native account, we also always request
343    /// witnesses for the initial vault root.
344    async fn on_account_vault_asset_witness_requested(
345        &self,
346        current_account_id: AccountId,
347        vault_root: Word,
348        asset_key: AssetVaultKey,
349    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
350        let asset_witness = self
351            .base_host
352            .store()
353            .get_vault_asset_witness(current_account_id, vault_root, asset_key)
354            .await
355            .map_err(|err| TransactionKernelError::GetVaultAssetWitness {
356                vault_root,
357                asset_key,
358                source: err,
359            })?;
360
361        Ok(asset_witness_to_advice_mutation(asset_witness))
362    }
363
364    /// Handles a request for a [`NoteScript`] by querying the [`DataStore`].
365    ///
366    /// The script is fetched from the data store and used to build a [`NoteRecipient`], which is
367    /// then used to create an [`OutputNoteBuilder`]. This function is only called for public notes
368    /// where the script is not already available in the advice provider.
369    async fn on_note_script_requested(
370        &mut self,
371        script_root: Word,
372        metadata: NoteMetadata,
373        recipient_digest: Word,
374        note_idx: usize,
375        note_inputs: NoteInputs,
376        serial_num: Word,
377    ) -> Result<Vec<AdviceMutation>, TransactionKernelError> {
378        let note_script_result = self.base_host.store().get_note_script(script_root).await;
379
380        let (recipient, mutations) = match note_script_result {
381            Ok(note_script) => {
382                let script_felts: Vec<Felt> = (&note_script).into();
383                let recipient = NoteRecipient::new(serial_num, note_script, note_inputs);
384                let mutations = vec![AdviceMutation::extend_map(AdviceMap::from_iter([(
385                    script_root,
386                    script_felts,
387                )]))];
388
389                (Some(recipient), mutations)
390            },
391            Err(DataStoreError::NoteScriptNotFound(_)) if metadata.is_private() => {
392                (None, Vec::new())
393            },
394            Err(DataStoreError::NoteScriptNotFound(_)) => {
395                return Err(TransactionKernelError::other(format!(
396                    "note script with root {script_root} not found in data store for public note"
397                )));
398            },
399            Err(err) => {
400                return Err(TransactionKernelError::other_with_source(
401                    "failed to retrieve note script from data store",
402                    err,
403                ));
404            },
405        };
406
407        let note_builder = OutputNoteBuilder::new(metadata, recipient_digest, recipient)?;
408        self.base_host.insert_output_note_builder(note_idx, note_builder)?;
409
410        Ok(mutations)
411    }
412
413    /// Consumes `self` and returns the account delta, output notes, generated signatures and
414    /// transaction progress.
415    #[allow(clippy::type_complexity)]
416    pub fn into_parts(
417        self,
418    ) -> (
419        AccountDelta,
420        InputNotes<InputNote>,
421        Vec<OutputNote>,
422        Vec<AccountCode>,
423        BTreeMap<Word, Vec<Felt>>,
424        TransactionProgress,
425    ) {
426        let (account_delta, input_notes, output_notes, tx_progress) = self.base_host.into_parts();
427
428        (
429            account_delta,
430            input_notes,
431            output_notes,
432            self.accessed_foreign_account_code,
433            self.generated_signatures,
434            tx_progress,
435        )
436    }
437}
438
439// HOST IMPLEMENTATION
440// ================================================================================================
441
442impl<STORE, AUTH> BaseHost for TransactionExecutorHost<'_, '_, STORE, AUTH>
443where
444    STORE: DataStore,
445    AUTH: TransactionAuthenticator,
446{
447    fn get_label_and_source_file(
448        &self,
449        location: &Location,
450    ) -> (SourceSpan, Option<Arc<SourceFile>>) {
451        let source_manager = self.source_manager.as_ref();
452        let maybe_file = source_manager.get_by_uri(location.uri());
453        let span = source_manager.location_to_span(location.clone()).unwrap_or_default();
454        (span, maybe_file)
455    }
456}
457
458impl<STORE, AUTH> AsyncHost for TransactionExecutorHost<'_, '_, STORE, AUTH>
459where
460    STORE: DataStore + Sync,
461    AUTH: TransactionAuthenticator + Sync,
462{
463    fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend<Option<Arc<MastForest>>> {
464        let mast_forest = self.base_host.get_mast_forest(node_digest);
465        async move { mast_forest }
466    }
467
468    fn on_event(
469        &mut self,
470        process: &ProcessState,
471    ) -> impl FutureMaybeSend<Result<Vec<AdviceMutation>, EventError>> {
472        let event_id = EventId::from_felt(process.get_stack_item(0));
473
474        // TODO: Eventually, refactor this to let TransactionEvent contain the data directly, which
475        // should be cleaner.
476        let event_handling_result = self.base_host.handle_event(process, event_id);
477
478        async move {
479            let event_handling = event_handling_result?;
480            let event_data = match event_handling {
481                TransactionEventHandling::Unhandled(event) => event,
482                TransactionEventHandling::Handled(mutations) => {
483                    return Ok(mutations);
484                },
485            };
486
487            match event_data {
488                TransactionEventData::AuthRequest { pub_key_hash, signing_inputs } => self
489                    .on_auth_requested(pub_key_hash, signing_inputs)
490                    .await
491                    .map_err(EventError::from),
492                TransactionEventData::TransactionFeeComputed { fee_asset } => self
493                    .on_before_tx_fee_removed_from_account(fee_asset)
494                    .await
495                    .map_err(EventError::from),
496                TransactionEventData::ForeignAccount { account_id } => {
497                    self.on_foreign_account_requested(account_id).await.map_err(EventError::from)
498                },
499                TransactionEventData::AccountVaultAssetWitness {
500                    current_account_id,
501                    vault_root,
502                    asset_key,
503                } => self
504                    .on_account_vault_asset_witness_requested(
505                        current_account_id,
506                        vault_root,
507                        asset_key,
508                    )
509                    .await
510                    .map_err(EventError::from),
511                TransactionEventData::AccountStorageMapWitness {
512                    current_account_id,
513                    map_root,
514                    map_key,
515                } => self
516                    .on_account_storage_map_witness_requested(current_account_id, map_root, map_key)
517                    .await
518                    .map_err(EventError::from),
519                TransactionEventData::NoteData {
520                    note_idx,
521                    metadata,
522                    script_root,
523                    recipient_digest,
524                    note_inputs,
525                    serial_num,
526                } => self
527                    .on_note_script_requested(
528                        script_root,
529                        metadata,
530                        recipient_digest,
531                        note_idx,
532                        note_inputs,
533                        serial_num,
534                    )
535                    .await
536                    .map_err(EventError::from),
537            }
538        }
539    }
540}
541
542// HELPER FUNCTIONS
543// ================================================================================================
544
545/// Converts an [`AssetWitness`] into the set of advice mutations that need to be inserted in order
546/// to access the asset.
547fn asset_witness_to_advice_mutation(asset_witness: AssetWitness) -> Vec<AdviceMutation> {
548    // Get the nodes in the proof and insert them into the merkle store.
549    let merkle_store_ext = AdviceMutation::extend_merkle_store(asset_witness.authenticated_nodes());
550
551    let smt_proof = SmtProof::from(asset_witness);
552    let map_ext = AdviceMutation::extend_map(AdviceMap::from_iter([(
553        smt_proof.leaf().hash(),
554        smt_proof.leaf().to_elements(),
555    )]));
556
557    vec![merkle_store_ext, map_ext]
558}