tasm_lib/traits/
basic_snippet.rs

1//! Contains an interface for defining composable snippets of Triton assembly.
2
3use std::collections::HashMap;
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::hash::Hash;
7use std::hash::Hasher;
8
9use num_traits::ConstZero;
10use num_traits::Zero;
11use triton_vm::isa::instruction::AnInstruction;
12use triton_vm::isa::op_stack::NUM_OP_STACK_REGISTERS;
13use triton_vm::prelude::*;
14
15use crate::prelude::*;
16use crate::push_encodable;
17
18/// A (basic) snippet represents a piece of code that can be run in
19/// [Triton VM](triton_vm).
20///
21/// Generally speaking, it’s not possible to run a snippet stand-alone. Rather,
22/// snippets are (generally) intended to be used in combination with other
23/// snippets. Together, they can make up a full Triton VM program.
24///
25/// ### Example
26///
27/// ```
28/// # use tasm_lib::prelude::*;
29/// # use tasm_lib::triton_vm::prelude::*;
30/// /// Checks whether some u32 is odd.
31/// struct IsOdd;
32///
33/// impl BasicSnippet for IsOdd {
34///     fn parameters(&self) -> Vec<(DataType, String)> {
35///         vec![(DataType::U32, "x".to_string())]
36///     }
37///
38///     fn return_values(&self) -> Vec<(DataType, String)> {
39///         vec![(DataType::Bool, "x % 2".to_string())]
40///     }
41///
42///     fn entrypoint(&self) -> String {
43///         "is_odd".to_string()
44///     }
45///
46///     fn code(&self, _: &mut Library) -> Vec<LabelledInstruction> {
47///         triton_asm!(
48///             // BEFORE: _ x
49///             // AFTER:  _ (x%2)
50///             {self.entrypoint()}:
51///                 push 2   // _ x 2
52///                 pick 1   // _ 2 x
53///                 div_mod  // _ (x//2) (x%2)
54///                 pick 1   // _ (x%2) (x//2)
55///                 pop 1    // _ (x%2)
56///                 return
57///         )
58///     }
59/// }
60/// ```
61///
62/// ### A Caveat Regarding Type Safety
63///
64/// The methods [`parameters`](Self::parameters) and
65/// [`return_values`](Self::return_values) define some expected and returned
66/// [`DataType`]s, respectively. Note that there is no mechanism that enforces
67/// any guarantees about these types. Notably:
68/// - tasm-lib never checks any kind of type consistency or correctness,
69///   neither at run- nor compile-time
70/// - [Triton VM](triton_vm) does not know anything about types
71///
72/// While the types defined in these respective methods streamline a snippet’s
73/// testability and simplify the required reasoning when composing them, they
74/// are _only_ used as a development aid.
75///
76/// ### Dyn-Compatibility
77///
78/// This trait is [dyn-compatible] (previously known as “object safe”).
79///
80/// [dyn-compatible]: https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility
81pub trait BasicSnippet {
82    /// The parameters expected by this snippet.
83    ///
84    /// The parameters are expected to be on top of Triton VM’s stack when
85    /// the snippet is called. If this is not the case, behavior of the snippet
86    /// is generally undefined.
87    ///
88    /// See also the
89    /// [caveat regarding type safety](Self#a-caveat-regarding-type-safety).
90    fn parameters(&self) -> Vec<(DataType, String)>;
91
92    /// The (types of the) values this snippet computes.
93    ///
94    /// The return values are at the top of the stack when the snippet returns.
95    ///
96    /// See also the
97    /// [caveat regarding type safety](Self#a-caveat-regarding-type-safety).
98    fn return_values(&self) -> Vec<(DataType, String)>;
99
100    /// The name of the snippet as a possible target for Triton VM’s
101    /// instruction `call`.
102    fn entrypoint(&self) -> String;
103
104    /// The Triton Assembly that defines this snippet.
105    ///
106    /// Snippet authors are responsible for the following:
107    /// 1. Only use the pre-conditions
108    ///     - listed explicitly in the snippet’s documentation, and
109    ///     - implied by [`parameters`](Self::parameters).
110    /// 1. Uphold all post-conditions
111    ///     - listed explicitly in the snippet’s documentation, and
112    ///     - implied by [`return_values`](Self::return_values).
113    ///
114    /// Notably, no type checking of any kind is performed. See also the
115    /// [caveat regarding type safety](Self#a-caveat-regarding-type-safety).
116    fn code(&self, library: &mut Library) -> Vec<LabelledInstruction>;
117
118    /// Adds “type hints” to the [code](Self::code).
119    ///
120    /// This method should _not_ be overwritten by implementors of the trait.
121    ///
122    /// Note that type hints do not change the behavior of Triton VM in any
123    /// way. They are only useful in debuggers, like the
124    /// [Triton TUI](https://crates.io/crates/triton-tui).
125    fn annotated_code(&self, library: &mut Library) -> Vec<LabelledInstruction> {
126        fn generate_hints_for_input_values(inputs: Vec<(DataType, String)>) -> Vec<String> {
127            let mut input_hints = vec![];
128            let mut stack_depth = 0;
129            for (data_type, name) in inputs.into_iter().rev() {
130                let stack_size = data_type.stack_size();
131                if stack_size.is_zero() {
132                    continue;
133                }
134
135                let data_name = data_type.label_friendly_name();
136
137                // TODO: Remove this once. the Triton-VM parser becomes more
138                // permissive WRT variable names
139                let name = name
140                    .replace(|c: char| !c.is_alphanumeric(), "_")
141                    .to_ascii_lowercase();
142
143                input_hints.push(format!(
144                    "hint {name}: {data_name} = stack[{stack_depth}..{}]",
145                    stack_depth + stack_size
146                ));
147                stack_depth += stack_size;
148            }
149
150            input_hints
151        }
152
153        let code = self.code(library);
154        let Some((entrypoint, snippet_body)) = code.split_first() else {
155            return code;
156        };
157        let entrypoint = entrypoint.to_string();
158        let observed_entrypoint = entrypoint.trim_end_matches(':');
159        if *observed_entrypoint != self.entrypoint() {
160            return code;
161        }
162
163        let input_hints = generate_hints_for_input_values(self.parameters());
164
165        triton_asm! {
166            {observed_entrypoint}:
167                {&input_hints}
168                {&snippet_body}
169        }
170    }
171
172    #[cfg(test)]
173    fn link_for_isolated_run_populated_static_memory(
174        &self,
175        words_statically_allocated: u32,
176    ) -> Vec<LabelledInstruction> {
177        let mut library = Library::with_preallocated_memory(words_statically_allocated);
178        let entrypoint = self.entrypoint();
179        let function_body = self.annotated_code(&mut library);
180        let library_code = library.all_imports();
181
182        // The TASM code is always run through a function call, so the 1st instruction is a call to
183        // the function in question.
184        let code = triton_asm!(
185            call {entrypoint}
186            halt
187
188            {&function_body}
189            {&library_code}
190        );
191
192        code
193    }
194
195    #[doc(hidden)]
196    fn link_for_isolated_run(&self) -> Vec<LabelledInstruction> {
197        let mut library = Library::empty();
198        let entrypoint = self.entrypoint();
199        let function_body = self.annotated_code(&mut library);
200        let library_code = library.all_imports();
201
202        // The TASM code is always run through a function call, so the 1st instruction is a call to
203        // the function in question.
204        let code = triton_asm!(
205            call {entrypoint}
206            halt
207
208            {&function_body}
209            {&library_code}
210        );
211
212        code
213    }
214
215    /// Initial stack on program start, when the snippet runs in isolation.
216    #[doc(hidden)]
217    fn init_stack_for_isolated_run(&self) -> Vec<BFieldElement> {
218        let code = self.link_for_isolated_run();
219        let program = Program::new(&code);
220
221        let mut stack = vec![];
222        push_encodable(&mut stack, &program.hash());
223        stack.resize(NUM_OP_STACK_REGISTERS, BFieldElement::ZERO);
224
225        stack
226    }
227
228    /// The size difference of the stack as a result of executing this snippet.
229    fn stack_diff(&self) -> isize {
230        let io_size = |io: Vec<(DataType, _)>| -> isize {
231            let size = io.into_iter().map(|(ty, _)| ty.stack_size()).sum::<usize>();
232            size.try_into().unwrap()
233        };
234
235        io_size(self.return_values()) - io_size(self.parameters())
236    }
237
238    /// Contains an entry for every sign off.
239    ///
240    /// Many of the snippets defined in this TASM library are critical for the
241    /// consensus logic of the blockchain [Neptune Cash](https://neptune.cash).
242    /// Therefore, it is paramount that the snippets are free of errors. In order
243    /// to catch as many errors as possible, the snippets are reviewed by as many
244    /// developers as possible. The requirements of such a review are listed here.
245    ///
246    /// A reviewer can (and should) sign off on any snippet they have reviewed and
247    /// for which they found no defects. This is done by adding that snippet's
248    /// [fingerprint] (at the time) to the overriding implementation of this method
249    /// on that snippet.
250    ///
251    /// Together with the tools [`git blame`][blame] and cryptographic
252    /// [signing] of commits, this makes sign-offs traceable. It also guarantees
253    /// that changes to snippets that have already been signed-off are easy to
254    /// detect.
255    ///
256    /// # For Reviewers
257    ///
258    /// ## Modifying snippets
259    ///
260    /// While the primary intention of the review process is to _review_ a snippet,
261    /// there are circumstances under which modifying it is acceptable.
262    ///
263    /// Modifying a snippet to simplify reviewing that snippet is fair game. A
264    /// common example of this case is replacing a `swap`-juggle chain with a few
265    /// `pick`s & `place`s.
266    ///
267    /// Modifying a snippet in order to improve performance should only happen if
268    /// the performance impact is meaningful. The currently agreed-upon threshold
269    /// is 0.5% of at least one consensus program.
270    ///
271    /// It is acceptable, and can be desired, to modify a snippet by including
272    /// assumption checks. For example, if the snippet's pre-conditions require
273    /// some input to fall within a certain range, it is fine to add a corresponding
274    /// range check to the snippet.
275    /// Removing existing checks of such nature is considered bad practice.
276    ///
277    /// In either case, modifying a snippet that has already been reviewed and
278    /// signed off by someone else in a way that alters its [fingerprint] requires
279    /// their consent.
280    ///
281    /// ## Checklist
282    ///
283    /// Use the following checklist to guide your review. Signing off on a snippet
284    /// means that in your eyes, all points on this checklist are true.
285    ///
286    /// - the snippet's documentation lists pre- and post-conditions
287    /// - the snippet makes no assumptions outside the stated pre-conditions
288    /// - given all pre-conditions, all post-conditions are met
289    /// - whenever this snippet calls another snippet, all of that other snippet's
290    ///   pre-conditions are met
291    /// - all dynamic memory offsets are range-checked before they are used
292    /// - each field accessor is used at most once per struct instance, or
293    ///   range-checked before each use
294    /// - reading from non-deterministically initialized memory only happens from
295    ///   the region specified in the [memory convention]
296    /// - memory-writes only happen outside of page 0 (see [memory convention])
297    ///
298    /// ## Documentation Template
299    ///
300    /// If a snippet you are reviewing is not (properly) documented yet, you can use
301    /// the following template to document the type implementing [`BasicSnippet`].
302    ///
303    /// ````text
304    /// /// ### Behavior
305    /// ///
306    /// /// ```text
307    /// /// BEFORE: _
308    /// /// AFTER:  _
309    /// /// ```
310    /// ///
311    /// /// ### Preconditions
312    /// ///
313    /// /// - condition
314    /// ///
315    /// /// ### Postconditions
316    /// ///
317    /// /// - condition
318    /// ````
319    ///
320    /// ## Non-Unit Structs
321    ///
322    /// Most, but not all types implementing [`BasicSnippet`] are unit structs.
323    /// [Fingerprinting][fingerprint] gets more difficult for non-unit structs.
324    /// In such cases, a default instantiation should be selected and signed off.
325    ///
326    /// ## Overriding this Method
327    ///
328    /// This default implementation _is_ intended to be overridden for any snippet
329    /// that has been signed off, but _should not_ call the [fingerprint] method.
330    ///
331    /// [fingerprint]: SignedOffSnippet::fingerprint
332    /// [memory convention]: crate::memory
333    /// [blame]: https://git-scm.com/docs/git-blame
334    /// [signing]: https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work
335    fn sign_offs(&self) -> HashMap<Reviewer, SignOffFingerprint> {
336        HashMap::default()
337    }
338}
339
340/// Extension trait for [`BasicSnippet`] related to
341/// [signing off](BasicSnippet::sign_offs). Contains methods that are callable,
342/// but for which the provided default implementation cannot be overridden.
343///
344/// ### Dyn-Compatibility
345///
346/// This trait is [dyn-compatible] (previously known as “object safe”).
347///
348/// [dyn-compatible]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
349//
350// Because `#[final]` trait methods are in pre-RFC phase [0], and trait
351// sealing [1] would be clumsy, use this workaround.
352//
353// [0]: https://internals.rust-lang.org/t/pre-rfc-final-trait-methods/18407
354// [1]: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust
355pub trait SignedOffSnippet: BasicSnippet {
356    /// The unique fingerprint as used for [signing off][BasicSnippet::sign_offs] on
357    /// this snippet.
358    fn fingerprint(&self) -> SignOffFingerprint {
359        let mut hasher = std::hash::DefaultHasher::new();
360        triton_vm::proof::CURRENT_VERSION.hash(&mut hasher);
361
362        for instruction in self.code(&mut Library::new()) {
363            let LabelledInstruction::Instruction(instruction) = instruction else {
364                continue;
365            };
366
367            if let AnInstruction::Call(_) = instruction {
368                AnInstruction::Call("").opcode().hash(&mut hasher);
369            } else {
370                instruction.hash(&mut hasher)
371            }
372        }
373
374        SignOffFingerprint(hasher.finish())
375    }
376
377    /// Panics if any [sign-offs](BasicSnippet::sign_offs) disagree with the actual
378    /// [fingerprint](Self::fingerprint).
379    fn assert_all_sign_offs_are_up_to_date(&self) {
380        let fingerprint = self.fingerprint();
381        let mut out_of_date_sign_offs = self
382            .sign_offs()
383            .into_iter()
384            .filter(|(_, fp)| fp != &fingerprint)
385            .peekable();
386
387        if out_of_date_sign_offs.peek().is_none() {
388            return;
389        }
390
391        let name = self.entrypoint();
392        for (reviewer, fp) in out_of_date_sign_offs {
393            eprintln!("reviewer {reviewer} of snippet “{name}” has signed off on fingerprint {fp}")
394        }
395        panic!("A sign-off is out of date. Current fingerprint of “{name}”: {fingerprint}");
396    }
397}
398
399// Blanket implementation conflicts with any other implementation, making the
400// provided defaults final.
401impl<T: BasicSnippet + ?Sized> SignedOffSnippet for T {}
402
403#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
404pub struct Reviewer(pub &'static str);
405
406impl Display for Reviewer {
407    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
408        write!(f, "{}", self.0)
409    }
410}
411
412/// A fingerprint as used for [signing off][BasicSnippet::sign_offs] snippets.
413///
414/// While this fingerprint can be used to distinguish [`BasicSnippet`]s, it is
415/// not cryptographically secure.
416#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
417pub struct SignOffFingerprint(pub(crate) u64);
418
419impl Display for SignOffFingerprint {
420    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
421        write!(f, "0x{:x}", self.0)
422    }
423}
424
425impl From<u64> for SignOffFingerprint {
426    fn from(value: u64) -> Self {
427        Self(value)
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    macro_rules! dummy_snippet {
436        ($name:ident: $($instr:tt)+) => {
437            #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
438            struct $name;
439
440            impl BasicSnippet for $name {
441                fn parameters(&self) -> Vec<(DataType, String)> { vec![] }
442                fn return_values(&self) -> Vec<(DataType, String)> { vec![] }
443                fn entrypoint(&self) -> String {
444                    stringify!($name).to_ascii_lowercase()
445                }
446
447                fn code(&self, _: &mut Library) -> Vec<LabelledInstruction> {
448                    triton_asm!($($instr)+)
449                }
450            }
451        };
452    }
453
454    dummy_snippet!(DummySnippet: dummysnippet: push 14 push 14 pop 2 return);
455
456    #[test]
457    fn init_stack_agrees_with_tvm() {
458        // Verify that our assumptions about the initial stack at program start
459        // agrees with Triton VM.
460        let calculated_init_stack = DummySnippet.init_stack_for_isolated_run();
461        let program = DummySnippet.link_for_isolated_run();
462        let program = Program::new(&program);
463        let init_vm_state = VMState::new(program, Default::default(), Default::default());
464
465        assert_eq!(init_vm_state.op_stack.stack, calculated_init_stack);
466    }
467
468    #[test]
469    fn defined_traits_are_dyn_compatible() {
470        fn basic_snippet_is_dyn_compatible(snippet: Box<dyn BasicSnippet>) {
471            snippet.fingerprint();
472        }
473
474        fn signed_off_snippet_is_dyn_compatible(snippet: Box<dyn SignedOffSnippet>) {
475            snippet.fingerprint();
476        }
477
478        basic_snippet_is_dyn_compatible(Box::new(DummySnippet));
479        signed_off_snippet_is_dyn_compatible(Box::new(DummySnippet));
480    }
481
482    #[test]
483    fn call_targets_dont_influence_snippet_fingerprints() {
484        dummy_snippet!(SomeLabel: call some_label);
485        dummy_snippet!(OtherLabel: call other_label);
486
487        assert_eq!(SomeLabel.fingerprint(), OtherLabel.fingerprint());
488    }
489
490    #[test]
491    fn instruction_arguments_do_influence_snippet_fingerprints() {
492        dummy_snippet!(Push20: push 20);
493        dummy_snippet!(Push42: push 42);
494
495        assert_ne!(Push20.fingerprint(), Push42.fingerprint());
496    }
497}