Skip to main content

miden_base_sys/bindings/
tx.rs

1use miden_stdlib_sys::{Felt, Word, WordAligned};
2
3use super::types::{AccountId, AssetAmount, AssetId, BlockNumber};
4
5/// Number of input felt slots and of output felt slots of the protocol's FPI executor.
6pub const FOREIGN_PROCEDURE_SLOTS: usize = 16;
7
8/// Marker trait for raw FPI input array lengths supported by the protocol executor.
9#[doc(hidden)]
10pub trait SupportedForeignProcedureInputLen {}
11
12macro_rules! supported_foreign_procedure_input_len {
13    ($($len:expr),* $(,)?) => {
14        $(
15            impl SupportedForeignProcedureInputLen for [(); $len] {}
16        )*
17    };
18}
19
20supported_foreign_procedure_input_len!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
21
22/// Fully-padded input felts accepted by `execute_foreign_procedure`.
23///
24/// Slot `i` is the `i`-th felt from the top of the callee's stack, that is the `i`-th felt of its
25/// flattened `#! Inputs:` list. A `Word` passed as `word.into_elements()` reaches the callee as
26/// the same `Word`.
27#[derive(Clone, Copy, Debug)]
28#[repr(C)]
29pub struct ForeignProcedureInputs {
30    felts: [Felt; FOREIGN_PROCEDURE_SLOTS],
31}
32
33impl ForeignProcedureInputs {
34    /// Creates raw FPI inputs where `values[i]` fills input slot `i`, and zero-pads the unused
35    /// trailing slots.
36    ///
37    /// This is only implemented for input arrays with at most [`FOREIGN_PROCEDURE_SLOTS`] felts.
38    pub fn new<const N: usize>(values: [Felt; N]) -> Self
39    where
40        [(); N]: SupportedForeignProcedureInputLen,
41    {
42        let mut felts = [Felt::ZERO; FOREIGN_PROCEDURE_SLOTS];
43        felts[..N].copy_from_slice(&values);
44        Self { felts }
45    }
46}
47
48/// Fully-padded output felts returned by `execute_foreign_procedure`.
49///
50/// Slot `i` is the `i`-th felt from the top of the callee's stack on return, that is the `i`-th
51/// felt of its flattened `#! Outputs:` list. A `Word` the callee leaves on top reads back as
52/// `Word::new([get(0), get(1), get(2), get(3)])`.
53#[derive(Clone, Copy, Debug)]
54#[repr(C)]
55pub struct ForeignProcedureOutputs {
56    // The compiler stores the executor results consecutively, top of stack first.
57    felts: [Felt; FOREIGN_PROCEDURE_SLOTS],
58}
59
60impl ForeignProcedureOutputs {
61    /// Returns the output felt in slot `index`.
62    ///
63    /// # Panics
64    ///
65    /// Panics if `index` is greater than or equal to [`FOREIGN_PROCEDURE_SLOTS`].
66    pub fn get(&self, index: usize) -> Felt {
67        self.felts[index]
68    }
69}
70
71/// Canonical raw FPI argument tuple consumed by the compiler's indirect lowering.
72#[derive(Clone, Copy, Debug)]
73#[repr(C)]
74pub struct ForeignProcedureInvocation {
75    /// Packed flattened FPI arguments: account id, procedure root, and 16 input felts.
76    pub words: [Word; 6],
77}
78
79impl ForeignProcedureInvocation {
80    /// Creates a raw FPI invocation tuple from SDK account and procedure values.
81    pub fn new(
82        foreign_account_id: AccountId,
83        foreign_proc_root: Word,
84        inputs: ForeignProcedureInputs,
85    ) -> Self {
86        // The compiler reloads these as 22 consecutive felts in this order (account id prefix,
87        // account id suffix, procedure root, 16 inputs) and swaps prefix and suffix into the
88        // executor's operand order itself; see `fpi_indirect_return_via_pointer` (reload) and
89        // `store_fpi_prefix_locals` (swap) in the Wasm frontend.
90        let zero = Felt::ZERO;
91        Self {
92            words: [
93                Word::new([
94                    foreign_account_id.prefix,
95                    foreign_account_id.suffix,
96                    foreign_proc_root[0],
97                    foreign_proc_root[1],
98                ]),
99                Word::new([
100                    foreign_proc_root[2],
101                    foreign_proc_root[3],
102                    inputs.felts[0],
103                    inputs.felts[1],
104                ]),
105                Word::new([inputs.felts[2], inputs.felts[3], inputs.felts[4], inputs.felts[5]]),
106                Word::new([inputs.felts[6], inputs.felts[7], inputs.felts[8], inputs.felts[9]]),
107                Word::new([inputs.felts[10], inputs.felts[11], inputs.felts[12], inputs.felts[13]]),
108                Word::new([inputs.felts[14], inputs.felts[15], zero, zero]),
109            ],
110        }
111    }
112}
113
114#[allow(improper_ctypes)]
115unsafe extern "C" {
116    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
117    #[link_name = "miden::protocol::tx::get_reference_block_number"]
118    pub fn extern_tx_get_reference_block_number() -> Felt;
119    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
120    #[link_name = "miden::protocol::tx::get_reference_block_commitment"]
121    pub fn extern_tx_get_reference_block_commitment(ptr: *mut Word);
122    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
123    #[link_name = "miden::protocol::tx::get_block_commitment"]
124    pub fn extern_tx_get_block_commitment(block_number: Felt, ptr: *mut Word);
125    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
126    #[link_name = "miden::protocol::tx::get_block_timestamp"]
127    pub fn extern_tx_get_block_timestamp() -> Felt;
128    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
129    #[link_name = "miden::protocol::tx::get_input_notes_commitment"]
130    pub fn extern_tx_get_input_notes_commitment(ptr: *mut Word);
131    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
132    #[link_name = "miden::protocol::tx::get_output_notes_commitment"]
133    pub fn extern_tx_get_output_notes_commitment(ptr: *mut Word);
134    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
135    #[link_name = "miden::protocol::tx::get_num_input_notes"]
136    pub fn extern_tx_get_num_input_notes() -> Felt;
137    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
138    #[link_name = "miden::protocol::tx::get_num_output_notes"]
139    pub fn extern_tx_get_num_output_notes() -> Felt;
140    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
141    #[link_name = "miden::protocol::tx::get_expiration_block_delta"]
142    pub fn extern_tx_get_expiration_block_delta() -> Felt;
143    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
144    #[link_name = "miden::protocol::tx::update_expiration_block_delta"]
145    pub fn extern_tx_update_expiration_block_delta(delta: Felt);
146    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
147    #[link_name = "miden::protocol::tx::get_tx_script_root"]
148    pub fn extern_tx_get_tx_script_root(ptr: *mut Word);
149    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
150    #[link_name = "miden::protocol::tx::execute_foreign_procedure_indirect"]
151    pub fn extern_tx_execute_foreign_procedure(
152        invocation: *const ForeignProcedureInvocation,
153        ptr: *mut ForeignProcedureOutputs,
154    );
155    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
156    #[link_name = "miden::protocol::tx::compute_fee"]
157    fn extern_tx_compute_fee(
158        num_extra_cycles: Felt,
159        exclude_notes_commitment_0: Felt,
160        exclude_notes_commitment_1: Felt,
161        exclude_notes_commitment_2: Felt,
162        exclude_notes_commitment_3: Felt,
163    ) -> Felt;
164    #[cfg_attr(target_family = "wasm", linkage = "extern_weak")]
165    #[link_name = "miden::protocol::tx::get_fee_asset_id"]
166    fn extern_tx_get_fee_asset_id(ptr: *mut AssetId);
167}
168
169/// Returns the transaction reference block number.
170pub fn get_reference_block_number() -> BlockNumber {
171    BlockNumber {
172        // The transaction kernel guarantees block numbers fit in a u32.
173        inner: unsafe { extern_tx_get_reference_block_number() },
174    }
175}
176
177/// Returns the input notes commitment digest.
178pub fn get_input_notes_commitment() -> Word {
179    unsafe {
180        let mut ret_area = WordAligned::new(::core::mem::MaybeUninit::<Word>::uninit());
181        extern_tx_get_input_notes_commitment(ret_area.as_mut_ptr());
182        ret_area.into_inner().assume_init()
183    }
184}
185
186/// Returns the block commitment of the reference block.
187pub fn get_reference_block_commitment() -> Word {
188    unsafe {
189        let mut ret_area = WordAligned::new(::core::mem::MaybeUninit::<Word>::uninit());
190        extern_tx_get_reference_block_commitment(ret_area.as_mut_ptr());
191        ret_area.into_inner().assume_init()
192    }
193}
194
195/// Returns the commitment of the block with the given number.
196///
197/// Any block up to and including the reference block can be read; the transaction kernel aborts
198/// for later blocks.
199pub fn get_block_commitment(block_number: BlockNumber) -> Word {
200    unsafe {
201        let mut ret_area = WordAligned::new(::core::mem::MaybeUninit::<Word>::uninit());
202        extern_tx_get_block_commitment(block_number.as_felt(), ret_area.as_mut_ptr());
203        ret_area.into_inner().assume_init()
204    }
205}
206
207/// Returns the timestamp of the reference block, in seconds.
208pub fn get_block_timestamp() -> u32 {
209    // The transaction kernel guarantees block timestamps fit in a u32.
210    let timestamp = unsafe { extern_tx_get_block_timestamp() };
211    timestamp.as_canonical_u64() as u32
212}
213
214/// Returns the total number of input notes consumed by the transaction.
215pub fn get_num_input_notes() -> u32 {
216    // The transaction kernel guarantees note counts fit in a u32.
217    let count = unsafe { extern_tx_get_num_input_notes() };
218    count.as_canonical_u64() as u32
219}
220
221/// Returns the number of output notes created so far in the transaction.
222pub fn get_num_output_notes() -> u32 {
223    // The transaction kernel guarantees note counts fit in a u32.
224    let count = unsafe { extern_tx_get_num_output_notes() };
225    count.as_canonical_u64() as u32
226}
227
228/// Returns the transaction expiration block delta, or `0` if no expiration delta has been set.
229pub fn get_expiration_block_delta() -> u16 {
230    // Set deltas are kernel-bounded to 1..=u16::MAX; the kernel returns 0 for an unset delta.
231    let delta = unsafe { extern_tx_get_expiration_block_delta() };
232    delta.as_canonical_u64() as u16
233}
234
235/// Updates the transaction expiration block delta.
236///
237/// The transaction kernel accepts deltas in `1..=u16::MAX`.
238pub fn update_expiration_block_delta(delta: u16) {
239    unsafe {
240        extern_tx_update_expiration_block_delta(Felt::from(delta));
241    }
242}
243
244/// Returns the transaction script root.
245pub fn get_tx_script_root() -> Word {
246    unsafe {
247        let mut ret_area = WordAligned::new(::core::mem::MaybeUninit::<Word>::uninit());
248        extern_tx_get_tx_script_root(ret_area.as_mut_ptr());
249        ret_area.into_inner().assume_init()
250    }
251}
252
253/// Returns the output notes commitment digest.
254pub fn get_output_notes_commitment() -> Word {
255    unsafe {
256        let mut ret_area = WordAligned::new(::core::mem::MaybeUninit::<Word>::uninit());
257        extern_tx_get_output_notes_commitment(ret_area.as_mut_ptr());
258        ret_area.into_inner().assume_init()
259    }
260}
261
262/// Executes `foreign_proc_root` against `foreign_account_id` with raw felt inputs.
263///
264/// The protocol executor always consumes exactly 16 input felts and returns exactly 16 output
265/// felts. Both are ordered top of stack first, that is, in the order of the callee's flattened
266/// `#! Inputs:` and `#! Outputs:` lists. Callers whose target procedure uses fewer values can pass
267/// the actual values to [`ForeignProcedureInputs::new`], which pads the remaining input slots with
268/// zeroes. Callers whose target procedure returns fewer values should ignore the unused padded
269/// outputs.
270///
271/// # Panics
272///
273/// Propagates kernel errors if the foreign account ID is invalid, the foreign account inputs are
274/// not available to the transaction, or the procedure root is not exported by the foreign account.
275pub fn execute_foreign_procedure(
276    foreign_account_id: AccountId,
277    foreign_proc_root: Word,
278    inputs: ForeignProcedureInputs,
279) -> ForeignProcedureOutputs {
280    unsafe {
281        let invocation =
282            ForeignProcedureInvocation::new(foreign_account_id, foreign_proc_root, inputs);
283        let mut ret_area =
284            WordAligned::new(::core::mem::MaybeUninit::<ForeignProcedureOutputs>::uninit());
285        extern_tx_execute_foreign_procedure(&invocation, ret_area.as_mut_ptr());
286        ret_area.into_inner().assume_init()
287    }
288}
289
290/// Computes the fee the current transaction owes, denominated in the fee asset.
291///
292/// `num_extra_cycles` is added to the transaction's current cycle count before the fee is
293/// computed, so a caller can account for work that still lies ahead of it, such as signature
294/// verification or the epilogue. `exclude_notes_commitment` commits to the output-note indices
295/// that should be left out of the computation, or is the empty word when nothing is excluded.
296///
297/// # Panics
298///
299/// Panics if the computed fee exceeds the maximum asset amount.
300pub fn compute_fee(num_extra_cycles: u32, exclude_notes_commitment: Word) -> AssetAmount {
301    let fee = unsafe {
302        extern_tx_compute_fee(
303            Felt::from_u32(num_extra_cycles),
304            exclude_notes_commitment[0],
305            exclude_notes_commitment[1],
306            exclude_notes_commitment[2],
307            exclude_notes_commitment[3],
308        )
309    };
310    AssetAmount::try_from(fee).expect("transaction fee exceeds the maximum asset amount")
311}
312
313/// Returns the asset id that transaction fees are paid in, as of the transaction reference block.
314pub fn get_fee_asset_id() -> AssetId {
315    unsafe {
316        let mut ret_area = WordAligned::new(::core::mem::MaybeUninit::<AssetId>::uninit());
317        extern_tx_get_fee_asset_id(ret_area.as_mut_ptr());
318        ret_area.into_inner().assume_init()
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use miden_stdlib_sys::{Felt, Word, felt};
325
326    use super::{
327        AccountId, FOREIGN_PROCEDURE_SLOTS, ForeignProcedureInputs, ForeignProcedureInvocation,
328        ForeignProcedureOutputs,
329    };
330
331    /// Ensures `ForeignProcedureInputs::new` keeps `values[i]` in slot `i` and zero-pads the rest.
332    #[test]
333    fn inputs_keep_slot_order_and_zero_pad_trailing_slots() {
334        let inputs = ForeignProcedureInputs::new([felt!(7), felt!(8)]);
335        let mut expected = [Felt::ZERO; FOREIGN_PROCEDURE_SLOTS];
336        expected[0] = felt!(7);
337        expected[1] = felt!(8);
338        assert_eq!(inputs.felts, expected);
339    }
340
341    /// Ensures `ForeignProcedureOutputs::get` maps index `i` straight to slot `i`, with no index
342    /// arithmetic in between.
343    #[test]
344    fn outputs_get_reads_slots_in_order() {
345        let felts: [Felt; FOREIGN_PROCEDURE_SLOTS] =
346            core::array::from_fn(|i| Felt::from_u32(i as u32 + 1));
347        let outputs = ForeignProcedureOutputs { felts };
348        for (index, felt) in felts.iter().enumerate() {
349            assert_eq!(outputs.get(index), *felt);
350        }
351    }
352
353    /// Ensures the invocation packs the account id, the root and the inputs in slot order. The
354    /// byte layout the compiler reloads is pinned by the `fpi::raw` network tests.
355    #[test]
356    fn invocation_flattens_to_prefix_root_and_inputs() {
357        let account_id = AccountId::new(felt!(1), felt!(2));
358        let root = Word::new([felt!(3), felt!(4), felt!(5), felt!(6)]);
359        let inputs =
360            ForeignProcedureInputs::new(core::array::from_fn::<Felt, FOREIGN_PROCEDURE_SLOTS, _>(
361                |i| Felt::from_u32(i as u32 + 7),
362            ));
363        let invocation = ForeignProcedureInvocation::new(account_id, root, inputs);
364
365        let expected: [Felt; 24] = core::array::from_fn(|i| {
366            if i < 22 {
367                Felt::from_u32(i as u32 + 1)
368            } else {
369                Felt::ZERO
370            }
371        });
372        assert_eq!(Word::words_as_elements(&invocation.words), &expected[..]);
373    }
374}