Skip to main content

tasm_lib/structure/
verify_nd_si_integrity.rs

1use std::fmt::Debug;
2use std::marker::PhantomData;
3
4use triton_vm::prelude::*;
5
6use crate::prelude::*;
7
8/// Verify size-indicator integrity of preloaded data, return size.
9///
10/// This snippet has a deliberately narrow responsibility. It certifies that the
11/// embedded *size indicators* of a `BFieldCodec`-encoded `T` in non-deterministic
12/// memory are mutually consistent — every field and element has the size it claims
13/// — so that subsequent `get_field`/`destructure` accesses land on the correct
14/// offsets. It additionally checks that the entire structure lies within the
15/// non-deterministic region (a valid `u32` address range).
16///
17/// It deliberately does **not** validate *leaf-value canonicality*. A value whose
18/// length is fixed by its type — e.g. a `u32`, a `bool`, or the coefficients of a
19/// `Polynomial` — is accepted regardless of its contents: a `u32` word may hold a
20/// value `>= 2^32`, a `bool` word a value other than `0`/`1`, and so on. Such a
21/// word still occupies the correct number of memory cells, which is all that
22/// "size-indicator integrity" concerns. This is therefore strictly *weaker* than
23/// `BFieldCodec::decode`, which also range-checks leaves; **passing this gate does
24/// not imply the data is a canonical encoding of `T`.**
25///
26/// A caller that consumes a leaf as an in-range value is responsible for enforcing
27/// that range itself. Most consumers do so for free — the u32/arithmetic
28/// instructions (`lt`, `split`, `pop_count`, …) crash on non-canonical words — but
29/// a raw word used as a hash preimage, an equality operand, or a `skiz`/`assert`
30/// discriminant does not self-defend. Treating "`VerifyNdSiIntegrity` passed" as
31/// "the witness is canonical" is a mistake.
32///
33/// Crashes the VM if the structure in question is not entirely contained within
34/// the non-deterministic section of memory as defined in the memory layout.
35#[derive(Clone, Debug)]
36pub struct VerifyNdSiIntegrity<PreloadedData: TasmObject + Clone + Debug> {
37    _phantom_data: PhantomData<PreloadedData>,
38}
39
40impl<T: TasmObject + Clone + Debug> Default for VerifyNdSiIntegrity<T> {
41    fn default() -> Self {
42        Self {
43            _phantom_data: PhantomData,
44        }
45    }
46}
47
48impl<T: TasmObject + Clone + Debug> BasicSnippet for VerifyNdSiIntegrity<T> {
49    fn parameters(&self) -> Vec<(DataType, String)> {
50        vec![(DataType::VoidPointer, "*struct".to_owned())]
51    }
52
53    fn return_values(&self) -> Vec<(DataType, String)> {
54        vec![(DataType::U32, "struct size".to_owned())]
55    }
56
57    fn entrypoint(&self) -> String {
58        let name = T::label_friendly_name();
59        format!("tasmlib_structure_verify_nd_si_integrity___{name}")
60    }
61
62    fn code(&self, library: &mut Library) -> Vec<LabelledInstruction> {
63        let entrypoint = self.entrypoint();
64        let si_integrity_check_code = T::compute_size_and_assert_valid_size_indicator(library);
65
66        triton_asm!(
67            {entrypoint}:
68                // _ *struct
69
70                dup 0
71                {&si_integrity_check_code}
72                // _ *struct calculated_size
73
74                /* Verify that both pointer and end of struct is in ND-region */
75                dup 1
76                // _ *struct calculated_size *struct
77
78                pop_count // verifies that `*struct` is a valid u32
79                pop 1
80                // _ *struct calculated_size
81
82                swap 1
83                dup 1
84                add
85                // _ calculated_size *end_of_struct
86
87                pop_count // verifies that end of struct is located in ND-region
88                pop 1
89                // _ calculated_size
90
91                return
92        )
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use std::fmt::Debug;
99
100    use arbitrary::Arbitrary;
101    use arbitrary::Unstructured;
102    use num_traits::ConstZero;
103    use twenty_first::util_types::mmr::mmr_successor_proof::MmrSuccessorProof;
104
105    use super::*;
106    use crate::memory::encode_to_memory;
107    use crate::neptune::neptune_like_types_for_tests::*;
108    use crate::test_prelude::*;
109
110    impl<T> VerifyNdSiIntegrity<T>
111    where
112        T: TasmObject + BFieldCodec + for<'a> Arbitrary<'a> + Debug + Clone,
113    {
114        fn initial_state(&self, address: BFieldElement, t: T) -> AccessorInitialState {
115            let mut memory = HashMap::default();
116            encode_to_memory(&mut memory, address, &t);
117
118            AccessorInitialState {
119                stack: [self.init_stack_for_isolated_run(), vec![address]].concat(),
120                memory,
121            }
122        }
123
124        fn prepare_random_object(&self, randomness: &[u8]) -> T {
125            let unstructured = Unstructured::new(randomness);
126            T::arbitrary_take_rest(unstructured).unwrap()
127        }
128    }
129
130    impl<T> Accessor for VerifyNdSiIntegrity<T>
131    where
132        T: TasmObject + for<'a> Arbitrary<'a> + Debug + Clone,
133    {
134        fn rust_shadow(
135            &self,
136            stack: &mut Vec<BFieldElement>,
137            memory: &HashMap<BFieldElement, BFieldElement>,
138        ) -> Result<(), RustShadowError> {
139            // A successful decode implies valid size indicators, so `decode` is
140            // used here as a convenient proxy for the property this snippet
141            // checks. Note it is *stricter* than the snippet (it also range-checks
142            // leaf values; see the struct docs), so it would reject some
143            // non-canonical inputs the TASM accepts. The test harness only ever
144            // generates canonical objects (via `Arbitrary`), so the two never
145            // diverge in practice — do not "fix" the snippet to match this.
146            let pointer = stack.pop().ok_or(RustShadowError::StackUnderflow)?;
147            let obj = T::decode_from_memory(memory, pointer)
148                .map_err(|_| RustShadowError::DecodingError)?;
149            let encoding_len = obj.encode().len();
150            let encoding_len: u32 = encoding_len
151                .try_into()
152                .map_err(|_| RustShadowError::UsizeToU32Error)?;
153
154            // Verify contained in ND-region
155            let start_address: u32 = pointer
156                .value()
157                .try_into()
158                .map_err(|_| RustShadowError::U64ToU32Error)?;
159            let _end_address = start_address
160                .checked_add(encoding_len)
161                .ok_or(RustShadowError::ArithmeticOverflow)?;
162
163            stack.push(bfe!(u64::from(encoding_len)));
164
165            Ok(())
166        }
167
168        fn pseudorandom_initial_state(
169            &self,
170            seed: [u8; 32],
171            _bench_case: Option<BenchmarkCase>,
172        ) -> AccessorInitialState {
173            let mut rng = StdRng::from_seed(seed);
174
175            let t: T = {
176                let mut randomness = [0u8; 100000];
177                rng.fill(&mut randomness);
178                self.prepare_random_object(&randomness)
179            };
180
181            let address: u32 = rng.random_range(0..(1 << 30));
182            let address = bfe!(address);
183            self.initial_state(address, t)
184        }
185
186        fn corner_case_initial_states(&self) -> Vec<AccessorInitialState> {
187            // This *should* always return `None` if `T: Option<S>`, and empty
188            // vec if type is Vec<T>. So some notion of "empty" or default.
189            let empty_struct: T = {
190                let unstructured = Unstructured::new(&[]);
191                T::arbitrary_take_rest(unstructured).unwrap()
192            };
193
194            println!("empty_struct:\n{empty_struct:?}");
195            let empty_struct_at_zero = self.initial_state(BFieldElement::ZERO, empty_struct);
196
197            vec![empty_struct_at_zero]
198        }
199    }
200
201    macro_rules! test_case {
202        (fn $test_name:ident for $t:ty) => {
203            #[macro_rules_attr::apply(test)]
204            fn $test_name() {
205                ShadowedAccessor::new(VerifyNdSiIntegrity::<$t>::default()).test();
206            }
207        };
208        (fn $test_name:ident for new type $t:ty: $($type_declaration:tt)*) => {
209            #[macro_rules_attr::apply(test)]
210            fn $test_name() {
211                #[derive(Debug, Clone, TasmObject, BFieldCodec, Arbitrary)]
212                $($type_declaration)*
213                ShadowedAccessor::new(VerifyNdSiIntegrity::<$t>::default()).test();
214            }
215        };
216    }
217
218    mod simple_struct {
219        use super::*;
220        use crate::test_helpers::negative_test;
221        use crate::test_helpers::test_assertion_failure;
222
223        #[derive(Debug, Clone, TasmObject, BFieldCodec, Arbitrary)]
224        struct TestStruct {
225            a: Vec<u128>,
226            b: Digest,
227            c: Vec<Digest>,
228        }
229
230        test_case! { fn test_pbt_simple_struct for TestStruct }
231
232        #[macro_rules_attr::apply(test)]
233        fn struct_not_contained_in_nd_region() {
234            let snippet = VerifyNdSiIntegrity::<TestStruct>::default();
235
236            let t = snippet.prepare_random_object(&[]);
237            let begin_address = bfe!((1u64 << 32) - 4);
238            let init_state = snippet.initial_state(begin_address, t.clone());
239
240            let actual_size = t.encode().len();
241            let end_address = begin_address + bfe!(actual_size as u64);
242            let expected_err =
243                InstructionError::OpStackError(OpStackError::FailedU32Conversion(end_address));
244            negative_test(
245                &ShadowedAccessor::new(snippet),
246                init_state.into(),
247                &[expected_err],
248            )
249        }
250
251        #[macro_rules_attr::apply(test)]
252        fn struct_does_not_start_in_nd_region() {
253            let snippet = VerifyNdSiIntegrity::<TestStruct>::default();
254
255            let begin_address = bfe!(-4);
256            let init_state =
257                snippet.initial_state(begin_address, snippet.prepare_random_object(&[]));
258            let expected_err =
259                InstructionError::OpStackError(OpStackError::FailedU32Conversion(begin_address));
260            negative_test(
261                &ShadowedAccessor::new(snippet),
262                init_state.into(),
263                &[expected_err],
264            )
265        }
266
267        #[macro_rules_attr::apply(test)]
268        fn lie_about_digest_vec_size() {
269            let snippet = VerifyNdSiIntegrity::<TestStruct>::default();
270
271            let begin_address = bfe!(4);
272            let mut init_state =
273                snippet.initial_state(begin_address, snippet.prepare_random_object(&[]));
274            let true_value = init_state.memory[&begin_address];
275            init_state
276                .memory
277                .insert(begin_address, true_value + bfe!(1));
278
279            test_assertion_failure(&ShadowedAccessor::new(snippet), init_state.into(), &[181])
280        }
281
282        #[macro_rules_attr::apply(test)]
283        fn lie_about_digest_vec_len() {
284            let snippet = VerifyNdSiIntegrity::<TestStruct>::default();
285
286            let begin_address = bfe!(4);
287            let mut init_state =
288                snippet.initial_state(begin_address, snippet.prepare_random_object(&[42u8; 20000]));
289            let vec_digest_len_indicator = begin_address + bfe!(1);
290            let true_value = init_state.memory[&vec_digest_len_indicator];
291            init_state
292                .memory
293                .insert(vec_digest_len_indicator, true_value + bfe!(1));
294
295            test_assertion_failure(&ShadowedAccessor::new(snippet), init_state.into(), &[181])
296        }
297    }
298
299    mod option_types {
300        use super::*;
301        use crate::test_helpers::test_assertion_failure;
302
303        #[derive(Debug, Clone, TasmObject, BFieldCodec, Arbitrary)]
304        struct StatSizedPayload {
305            a: Option<Digest>,
306        }
307
308        #[derive(Debug, Clone, TasmObject, BFieldCodec, Arbitrary, Default)]
309        struct DynSizedPayload {
310            a: Option<Vec<u128>>,
311            b: Digest,
312            c: Vec<Vec<BFieldElement>>,
313            d: Option<Vec<Option<BFieldElement>>>,
314        }
315
316        test_case! {fn test_option_stat_sized_elem for StatSizedPayload }
317        test_case! {fn test_option_dyn_sized_elem for DynSizedPayload }
318
319        #[macro_rules_attr::apply(test)]
320        fn lie_about_option_payload_field_size() {
321            let snippet = VerifyNdSiIntegrity::<DynSizedPayload>::default();
322
323            let begin_address = bfe!(4);
324            let randomness = rand::random::<[u8; 100_000]>();
325            let obj = snippet.prepare_random_object(&randomness);
326            let true_init_state = snippet.initial_state(begin_address, obj.clone());
327
328            /*  Lie about size of field 'd'*/
329            let mut manipulated_si_outer = true_init_state.clone();
330            let outer_option_payload_si_ptr = begin_address; // field size-indicator of `d` field
331            let true_value = true_init_state.memory[&outer_option_payload_si_ptr];
332            manipulated_si_outer
333                .memory
334                .insert(outer_option_payload_si_ptr, true_value + bfe!(1));
335
336            test_assertion_failure(
337                &ShadowedAccessor::new(snippet),
338                manipulated_si_outer.into(),
339                &[181],
340            );
341        }
342
343        #[macro_rules_attr::apply(test)]
344        fn illegal_discriminant_value_for_option() {
345            let snippet = VerifyNdSiIntegrity::<DynSizedPayload>::default();
346
347            let obj = DynSizedPayload::default();
348            let begin_address = bfe!(4);
349            let mut manipulated_init_state = snippet.initial_state(begin_address, obj.clone());
350            let option_discriminant_ptr = begin_address + bfe!(1);
351            manipulated_init_state
352                .memory
353                .insert(option_discriminant_ptr, bfe!(2));
354
355            test_assertion_failure(
356                &ShadowedAccessor::new(snippet.clone()),
357                manipulated_init_state.into(),
358                &[200],
359            );
360        }
361
362        #[macro_rules_attr::apply(test)]
363        fn lie_about_option_payload_size() {
364            let snippet = VerifyNdSiIntegrity::<DynSizedPayload>::default();
365
366            let obj = DynSizedPayload {
367                d: Some(vec![Some(bfe!(14)), None, Some(bfe!(15))]),
368                ..Default::default()
369            };
370            let begin_address = bfe!(4);
371            let true_init_state = snippet.initial_state(begin_address, obj.clone());
372
373            /*  Lie about size of payload of outer Some(...)*/
374            let mut add_one = true_init_state.clone();
375            let len_of_dyn_sized_list_elem_0 = begin_address + bfe!(3);
376            let true_value = true_init_state.memory[&len_of_dyn_sized_list_elem_0];
377            add_one
378                .memory
379                .insert(len_of_dyn_sized_list_elem_0, true_value + bfe!(1));
380
381            test_assertion_failure(
382                &ShadowedAccessor::new(snippet.clone()),
383                add_one.into(),
384                &[211],
385            );
386
387            let mut sub_one = true_init_state.clone();
388            sub_one
389                .memory
390                .insert(len_of_dyn_sized_list_elem_0, true_value - bfe!(1));
391
392            test_assertion_failure(&ShadowedAccessor::new(snippet), sub_one.into(), &[211]);
393        }
394    }
395
396    test_case! { fn test_stat_sized_tuple for new type TestStruct:
397        struct TestStruct { field: (Digest, XFieldElement) }
398    }
399
400    test_case! { fn test_dyn_state_sized_tuple_right_dyn for new type RightIsDyn:
401        struct RightIsDyn { field: (Digest, Vec<XFieldElement>) }
402    }
403
404    test_case! { fn test_dyn_state_sized_tuple_left_dyn for new type LeftIsDyn:
405        struct LeftIsDyn { field: (Vec<XFieldElement>, Digest) }
406    }
407
408    test_case! { fn test_dyn_state_sized_tuple_both_dyn for new type BothDyn:
409        struct BothDyn { field: (Vec<XFieldElement>, Vec<XFieldElement>) }
410    }
411
412    test_case! { fn proof for Proof }
413    test_case! { fn coin for CoinLookalike }
414    test_case! { fn utxo for UtxoLookalike }
415    test_case! { fn salted_utxos for SaltedUtxosLookalike }
416    test_case! { fn collect_lock_scripts_witness for CollectLockScriptsWitnessLookalike }
417    test_case! { fn collect_type_scripts_witness for CollectTypeScriptsWitnessLookalike }
418    test_case! { fn claim for Claim }
419    test_case! { fn neptune_coins for NeptuneCoinsLookalike }
420    test_case! { fn option_neptune_coins for Option<NeptuneCoinsLookalike> }
421    test_case! { fn chunk for ChunkLookalike }
422    test_case! { fn chunk_dictionary for ChunkDictionaryLookalike }
423    test_case! { fn proof_collection for ProofCollectionLookalike }
424    test_case! { fn absolute_index_set for AbsoluteIndexSetLookalike }
425    test_case! { fn removal_record for RemovalRecordLookalike }
426    test_case! { fn addition_record for AdditionRecordLookalike }
427    test_case! { fn public_announcement for PublicAnnouncementLookalike }
428    test_case! { fn timestamp for TimestampLookalike }
429    test_case! { fn transaction_kernel for TransactionKernelLookalike }
430    test_case! { fn kernel_to_outputs_witness for KernelToOutputsWitnessLookalike }
431    test_case! { fn merge_witness for MergeWitnessLookalike }
432    test_case! { fn ms_membership_proof for MsMembershipProofLookalike }
433    test_case! { fn removal_records_witness for RemovalRecordsIntegrityWitnessLookalike }
434    test_case! { fn update_witness for UpdateWitnessLookalike }
435    test_case! { fn lock_script_and_witness for LockScriptAndWitnessLookalike }
436    test_case! { fn mutator_set_accumulator for MutatorSetAccumulatorLookalike }
437    test_case! { fn primitive_witness for PrimitiveWitnessLookalike }
438    test_case! { fn mmr_successor_proof for MmrSuccessorProof }
439}
440
441#[cfg(test)]
442mod benches {
443    use super::*;
444    use crate::neptune::neptune_like_types_for_tests::ProofCollectionLookalike;
445    use crate::neptune::neptune_like_types_for_tests::TransactionKernelLookalike;
446    use crate::test_prelude::*;
447
448    #[macro_rules_attr::apply(test)]
449    fn bench_proof_collection_lookalike() {
450        ShadowedAccessor::new(VerifyNdSiIntegrity::<ProofCollectionLookalike>::default()).bench();
451    }
452
453    #[macro_rules_attr::apply(test)]
454    fn bench_transaction_kernel_lookalike() {
455        ShadowedAccessor::new(VerifyNdSiIntegrity::<TransactionKernelLookalike>::default()).bench();
456    }
457}