tasm_lib/traits/
basic_snippet.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
use std::collections::HashMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::hash::Hash;
use std::hash::Hasher;

use num_traits::ConstZero;
use num_traits::Zero;
use triton_vm::isa::instruction::AnInstruction;
use triton_vm::isa::op_stack::NUM_OP_STACK_REGISTERS;
use triton_vm::prelude::*;

use crate::prelude::*;
use crate::push_encodable;

/// ### Dyn-Compatibility
///
/// This trait is [dyn-compatible] (previously known as “object safe”).
///
/// [dyn-compatible]: https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility
pub trait BasicSnippet {
    fn inputs(&self) -> Vec<(DataType, String)>;
    fn outputs(&self) -> Vec<(DataType, String)>;
    fn entrypoint(&self) -> String;
    fn code(&self, library: &mut Library) -> Vec<LabelledInstruction>;

    fn annotated_code(&self, library: &mut Library) -> Vec<LabelledInstruction> {
        fn generate_hints_for_input_values(inputs: Vec<(DataType, String)>) -> Vec<String> {
            let mut input_hints = vec![];
            let mut stack_depth = 0;
            for (data_type, name) in inputs.into_iter().rev() {
                let stack_size = data_type.stack_size();
                if stack_size.is_zero() {
                    continue;
                }

                let data_name = data_type.label_friendly_name();

                // TODO: Remove this once. the Triton-VM parser becomes more
                // permissive WRT variable names
                let name = name
                    .replace(|c: char| !c.is_alphanumeric(), "_")
                    .to_ascii_lowercase();

                input_hints.push(format!(
                    "hint {name}: {data_name} = stack[{stack_depth}..{}]",
                    stack_depth + stack_size
                ));
                stack_depth += stack_size;
            }

            input_hints
        }

        let code = self.code(library);
        let Some((entrypoint, snippet_body)) = code.split_first() else {
            return code;
        };
        let entrypoint = entrypoint.to_string();
        let observed_entrypoint = entrypoint.trim_end_matches(':');
        if *observed_entrypoint != self.entrypoint() {
            return code;
        }

        let input_hints = generate_hints_for_input_values(self.inputs());

        triton_asm! {
            {observed_entrypoint}:
                {&input_hints}
                {&snippet_body}
        }
    }

    #[cfg(test)]
    fn link_for_isolated_run_populated_static_memory(
        &self,
        words_statically_allocated: u32,
    ) -> Vec<LabelledInstruction> {
        let mut library = Library::with_preallocated_memory(words_statically_allocated);
        let entrypoint = self.entrypoint();
        let function_body = self.annotated_code(&mut library);
        let library_code = library.all_imports();

        // The TASM code is always run through a function call, so the 1st instruction is a call to
        // the function in question.
        let code = triton_asm!(
            call {entrypoint}
            halt

            {&function_body}
            {&library_code}
        );

        code
    }

    fn link_for_isolated_run(&self) -> Vec<LabelledInstruction> {
        let mut library = Library::empty();
        let entrypoint = self.entrypoint();
        let function_body = self.annotated_code(&mut library);
        let library_code = library.all_imports();

        // The TASM code is always run through a function call, so the 1st instruction is a call to
        // the function in question.
        let code = triton_asm!(
            call {entrypoint}
            halt

            {&function_body}
            {&library_code}
        );

        code
    }

    /// Initial stack on program start, when the snippet runs in isolation.
    fn init_stack_for_isolated_run(&self) -> Vec<BFieldElement> {
        let code = self.link_for_isolated_run();
        let program = Program::new(&code);

        let mut stack = vec![];
        push_encodable(&mut stack, &program.hash());
        stack.resize(NUM_OP_STACK_REGISTERS, BFieldElement::ZERO);

        stack
    }

    fn stack_diff(&self) -> isize {
        let io_size = |io: Vec<(DataType, _)>| -> isize {
            let size = io.into_iter().map(|(ty, _)| ty.stack_size()).sum::<usize>();
            size.try_into().unwrap()
        };

        io_size(self.outputs()) - io_size(self.inputs())
    }

    /// Contains an entry for every sign off.
    ///
    /// Many of the snippets defined in this TASM library are critical for the
    /// consensus logic of the blockchain [Neptune Cash](https://neptune.cash).
    /// Therefore, it is paramount that the snippets are free of errors. In order
    /// to catch as many errors as possible, the snippets are reviewed by as many
    /// developers as possible. The requirements of such a review are listed here.
    ///
    /// A reviewer can (and should) sign off on any snippet they have reviewed and
    /// for which they found no defects. This is done by adding that snippet's
    /// [fingerprint] (at the time) to the overriding implementation of this method
    /// on that snippet.
    ///
    /// Together with the tools [`git blame`][blame] and cryptographic
    /// [signing] of commits, this makes sign-offs traceable. It also guarantees
    /// that changes to snippets that have already been signed-off are easy to
    /// detect.
    ///
    /// # For Reviewers
    ///
    /// ## Modifying snippets
    ///
    /// While the primary intention of the review process is to _review_ a snippet,
    /// there are circumstances under which modifying it is acceptable.
    ///
    /// Modifying a snippet to simplify reviewing that snippet is fair game. A
    /// common example of this case is replacing a `swap`-juggle chain with a few
    /// `pick`s & `place`s.
    ///
    /// Modifying a snippet in order to improve performance should only happen if
    /// the performance impact is meaningful. The currently agreed-upon threshold
    /// is 0.5% of at least one consensus program.
    ///
    /// It is acceptable, and can be desired, to modify a snippet by including
    /// assumption checks. For example, if the snippet's pre-conditions require
    /// some input to fall within a certain range, it is fine to add a corresponding
    /// range check to the snippet.
    /// Removing existing checks of such nature is considered bad practice.
    ///
    /// In either case, modifying a snippet that has already been reviewed and
    /// signed off by someone else in a way that alters its [fingerprint] requires
    /// their consent.
    ///
    /// ## Checklist
    ///
    /// Use the following checklist to guide your review. Signing off on a snippet
    /// means that in your eyes, all points on this checklist are true.
    ///
    /// - the snippet's documentation lists pre- and post-conditions
    /// - the snippet makes no assumptions outside the stated pre-conditions
    /// - given all pre-conditions, all post-conditions are met
    /// - whenever this snippet calls another snippet, all of that other snippet's
    ///   pre-conditions are met
    /// - all dynamic memory offsets are range-checked before they are used
    /// - each field accessor is used at most once per struct instance, or
    ///   range-checked before each use
    /// - reading from non-deterministically initialized memory only happens from
    ///   the region specified in the [memory convention]
    /// - memory-writes only happen outside of page 0 (see [memory convention])
    ///
    /// ## Documentation Template
    ///
    /// If a snippet you are reviewing is not (properly) documented yet, you can use
    /// the following template to document the type implementing [`BasicSnippet`].
    ///
    /// ````text
    /// /// ### Behavior
    /// ///
    /// /// ```text
    /// /// BEFORE: _
    /// /// AFTER:  _
    /// /// ```
    /// ///
    /// /// ### Preconditions
    /// ///
    /// /// - condition
    /// ///
    /// /// ### Postconditions
    /// ///
    /// /// - condition
    /// ````
    ///
    /// ## Non-Unit Structs
    ///
    /// Most, but not all types implementing [`BasicSnippet`] are unit structs.
    /// [Fingerprinting][fingerprint] gets more difficult for non-unit structs.
    /// In such cases, a default instantiation should be selected and signed off.
    ///
    /// ## Overriding this Method
    ///
    /// This default implementation _is_ intended to be overridden for any snippet
    /// that has been signed off, but _should not_ call the [fingerprint] method.
    ///
    /// [fingerprint]: SignedOffSnippet::fingerprint
    /// [memory convention]: crate::memory
    /// [blame]: https://git-scm.com/docs/git-blame
    /// [signing]: https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work
    fn sign_offs(&self) -> HashMap<Reviewer, SignOffFingerprint> {
        HashMap::default()
    }
}

/// Extension trait for [`BasicSnippet`] related to
/// [signing off](BasicSnippet::sign_offs). Contains methods that are callable,
/// but for which the provided default implementation cannot be overridden.
///
/// ### Dyn-Compatibility
///
/// This trait is [dyn-compatible] (previously known as “object safe”).
///
/// [dyn-compatible]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
//
// Because `$[final]` trait methods are in pre-RFC phase [0], and trait
// sealing [1] would be clumsy, use this workaround.
//
// [0]: https://internals.rust-lang.org/t/pre-rfc-final-trait-methods/18407
// [1]: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust
pub trait SignedOffSnippet: BasicSnippet {
    /// The unique fingerprint as used for [signing off][BasicSnippet::sign_offs] on
    /// this snippet.
    fn fingerprint(&self) -> SignOffFingerprint {
        let mut hasher = std::hash::DefaultHasher::new();
        triton_vm::proof::CURRENT_VERSION.hash(&mut hasher);

        for instruction in self.code(&mut Library::new()) {
            let LabelledInstruction::Instruction(instruction) = instruction else {
                continue;
            };

            if let AnInstruction::Call(_) = instruction {
                AnInstruction::Call("").opcode().hash(&mut hasher);
            } else {
                instruction.hash(&mut hasher)
            }
        }

        SignOffFingerprint(hasher.finish())
    }

    /// Panics if any [sign-offs](BasicSnippet::sign_offs) disagree with the actual
    /// [fingerprint](Self::fingerprint).
    fn assert_all_sign_offs_are_up_to_date(&self) {
        let fingerprint = self.fingerprint();
        let mut out_of_date_sign_offs = self
            .sign_offs()
            .into_iter()
            .filter(|(_, fp)| fp != &fingerprint)
            .peekable();

        if out_of_date_sign_offs.peek().is_none() {
            return;
        }

        let name = self.entrypoint();
        for (reviewer, fp) in out_of_date_sign_offs {
            eprintln!("reviewer {reviewer} of snippet “{name}” has signed off on fingerprint {fp}")
        }
        panic!("A sign-off is out of date. Current fingerprint of “{name}”: {fingerprint}");
    }
}

// Blanket implementation conflicts with any other implementation, making the
// provided defaults final.
impl<T: BasicSnippet + ?Sized> SignedOffSnippet for T {}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Reviewer(pub &'static str);

impl Display for Reviewer {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// A fingerprint as used for [signing off][BasicSnippet::sign_offs] snippets.
///
/// While this fingerprint can be used to distinguish [`BasicSnippet`]s, it is
/// not cryptographically secure.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct SignOffFingerprint(pub(crate) u64);

impl Display for SignOffFingerprint {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "0x{:x}", self.0)
    }
}

impl From<u64> for SignOffFingerprint {
    fn from(value: u64) -> Self {
        Self(value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    macro_rules! dummy_snippet {
        ($name:ident: $($instr:tt)+) => {
            #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
            struct $name;

            impl BasicSnippet for $name {
                fn inputs(&self) -> Vec<(DataType, String)> { vec![] }
                fn outputs(&self) -> Vec<(DataType, String)> { vec![] }
                fn entrypoint(&self) -> String {
                    stringify!($name).to_ascii_lowercase()
                }

                fn code(&self, _: &mut Library) -> Vec<LabelledInstruction> {
                    triton_asm!($($instr)+)
                }
            }
        };
    }

    dummy_snippet!(DummySnippet: dummysnippet: push 14 push 14 pop 2 return);

    #[test]
    fn init_stack_agrees_with_tvm() {
        // Verify that our assumptions about the initial stack at program start
        // agrees with Triton VM.
        let calculated_init_stack = DummySnippet.init_stack_for_isolated_run();
        let program = DummySnippet.link_for_isolated_run();
        let program = Program::new(&program);
        let init_vm_state = VMState::new(program, Default::default(), Default::default());

        assert_eq!(init_vm_state.op_stack.stack, calculated_init_stack);
    }

    #[test]
    fn defined_traits_are_dyn_compatible() {
        fn basic_snippet_is_dyn_compatible(snippet: Box<dyn BasicSnippet>) {
            snippet.fingerprint();
        }

        fn signed_off_snippet_is_dyn_compatible(snippet: Box<dyn SignedOffSnippet>) {
            snippet.fingerprint();
        }

        basic_snippet_is_dyn_compatible(Box::new(DummySnippet));
        signed_off_snippet_is_dyn_compatible(Box::new(DummySnippet));
    }

    #[test]
    fn call_targets_dont_influence_snippet_fingerprints() {
        dummy_snippet!(SomeLabel: call some_label);
        dummy_snippet!(OtherLabel: call other_label);

        assert_eq!(SomeLabel.fingerprint(), OtherLabel.fingerprint());
    }

    #[test]
    fn instruction_arguments_do_influence_snippet_fingerprints() {
        dummy_snippet!(Push20: push 20);
        dummy_snippet!(Push42: push 42);

        assert_ne!(Push20.fingerprint(), Push42.fingerprint());
    }
}