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}