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}