tasm_lib/traits/
basic_snippet.rs

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