Skip to main content

snarkvm_circuit_program/data/dynamic/record/
mod.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16mod equal;
17mod find;
18mod to_bits;
19mod to_fields;
20
21use crate::{Access, Aleo, Entry, Equal, Identifier, Literal, Plaintext, Record, ToBits, ToFields, Value};
22
23use console::RECORD_DATA_TREE_DEPTH;
24use snarkvm_circuit_algorithms::{Poseidon2, Poseidon8};
25use snarkvm_circuit_collections::merkle_tree::MerkleTree;
26use snarkvm_circuit_types::{Address, Boolean, Field, Group, U8, environment::prelude::*};
27
28type CircuitLH<A> = Poseidon8<A>;
29type CircuitPH<A> = Poseidon2<A>;
30
31/// The record data tree.
32pub type RecordDataTree<A> = MerkleTree<A, CircuitLH<A>, CircuitPH<A>, RECORD_DATA_TREE_DEPTH>;
33
34/// A dynamic record is a fixed-size representation of a record. Like static
35/// `Record`s, a dynamic record contains an owner, nonce, and a version.
36/// However, instead of storing the full data, it only stores the Merkle root of
37/// the data. This ensures that all dynamic records have a constant size,
38/// regardless of the amount of data they contain.
39///
40/// Suppose we have the following record with two data entries:
41///
42/// ```text
43/// record foo:
44///     owner as address.private;
45///     microcredits as u64.private;
46///     memo as [u8; 32u32].public;
47/// ```
48///
49/// The leaves of its Merkle tree are computed as follows:
50///
51/// ```text
52/// L_0 := HashPSD8(ToField(name_0) || ToFields(entry_0))
53/// L_1 := HashPSD8(ToField(name_1) || ToFields(entry_1))
54/// ```
55///
56/// where `name_i` is the field encoding of the entry identifier (e.g. `"microcredits"` → `Field`),
57/// and `ToFields` encodes the entry's mode and plaintext variant.
58///
59/// The tree has depth `RECORD_DATA_TREE_DEPTH = 5` and is constructed with
60/// path hasher `HashPSD2` and the padding scheme outlined in
61/// [`snarkVM`'s `MerkleTree`](snarkvm_circuit_collections::merkle_tree::MerkleTree).
62#[derive(Clone)]
63pub struct DynamicRecord<A: Aleo> {
64    /// The owner of the record.
65    owner: Address<A>,
66    /// The Merkle root of the record data.
67    root: Field<A>,
68    /// The nonce of the record.
69    nonce: Group<A>,
70    /// The version of the record.
71    version: U8<A>,
72    /// The optional console record data.
73    /// Note: This is NOT part of the circuit representation.
74    data: Option<console::RecordData<A::Network>>,
75}
76
77impl<A: Aleo> Inject for DynamicRecord<A> {
78    type Primitive = console::DynamicRecord<A::Network>;
79
80    /// Initializes a plaintext record from a primitive.
81    fn new(_: Mode, record: Self::Primitive) -> Self {
82        Self {
83            owner: Inject::new(Mode::Private, *record.owner()),
84            root: Inject::new(Mode::Private, *record.root()),
85            nonce: Inject::new(Mode::Private, *record.nonce()),
86            version: Inject::new(Mode::Private, *record.version()),
87            data: record.data().clone(),
88        }
89    }
90}
91
92impl<A: Aleo> DynamicRecord<A> {
93    /// Returns the owner of the record.
94    pub const fn owner(&self) -> &Address<A> {
95        &self.owner
96    }
97
98    /// Returns the Merkle root of the record data.
99    pub const fn root(&self) -> &Field<A> {
100        &self.root
101    }
102
103    /// Returns the nonce of the record.
104    pub const fn nonce(&self) -> &Group<A> {
105        &self.nonce
106    }
107
108    /// Returns the version of the record.
109    pub const fn version(&self) -> &U8<A> {
110        &self.version
111    }
112
113    /// Returns the console record data.
114    pub const fn data(&self) -> Option<&console::RecordData<A::Network>> {
115        self.data.as_ref()
116    }
117}
118
119impl<A: Aleo> Eject for DynamicRecord<A> {
120    type Primitive = console::DynamicRecord<A::Network>;
121
122    /// Ejects the mode of the dynamic record.
123    fn eject_mode(&self) -> Mode {
124        let owner = self.owner.eject_mode();
125        let root = self.root.eject_mode();
126        let nonce = self.nonce.eject_mode();
127        let version = self.version.eject_mode();
128
129        Mode::combine(owner, [root, nonce, version])
130    }
131
132    /// Ejects the dynamic record.
133    fn eject_value(&self) -> Self::Primitive {
134        Self::Primitive::new_unchecked(
135            self.owner.eject_value(),
136            self.root.eject_value(),
137            self.nonce.eject_value(),
138            self.version.eject_value(),
139            self.data.clone(),
140        )
141    }
142}
143
144impl<A: Aleo> DynamicRecord<A> {
145    /// Creates a dynamic record from a static one.
146    pub fn from_record(record: &Record<A, Plaintext<A>>) -> Result<Self> {
147        // This mimics the console::DynamicRecord::from_record function.
148
149        // Note that, in most lines below, cloning (e.g. of record.owner())
150        // does not introduce a new variable into the witness but rather creates
151        // a new reference to the preexisting witness variable.
152
153        // Get the owner.
154        let owner = (**record.owner()).clone();
155        // Get the record's data (not part of the circuit representation)
156        let data = record.data();
157        // Get the nonce.
158        let nonce = record.nonce().clone();
159        // Get the version.
160        let version = record.version().clone();
161
162        let tree = Self::merkleize_data(data)?;
163        let root = tree.root().clone();
164
165        let console_data =
166            data.iter().map(|(identifier, entry)| (identifier, entry).eject_value()).collect::<IndexMap<_, _>>();
167
168        Ok(Self { owner, root, nonce, version, data: Some(console_data) })
169    }
170
171    /// Serializes the given (ordered) entries to field elements, prepends an identifier tag
172    /// per entry, and computes the Merkle tree over the resulting leaves. More details on
173    /// the structure of the tree can be found in [`DynamicRecord`].
174    pub fn merkleize_data(data: &IndexMap<Identifier<A>, Entry<A, Plaintext<A>>>) -> Result<RecordDataTree<A>> {
175        // Initialize the circuit hashers.
176        let (console_leaf_hasher, console_path_hasher) = console::DynamicRecord::initialize_hashers();
177        let circuit_leaf_hasher = CircuitLH::<A>::constant(console_leaf_hasher.clone());
178        let circuit_path_hasher = CircuitPH::<A>::constant(console_path_hasher.clone());
179
180        // Serialize the in-circuit entries to leaf field elements.
181        let leaves = data
182            .iter()
183            .map(|(identifier, entry)| {
184                let fields = entry.to_fields();
185                let mut leaf = Vec::with_capacity(1 + fields.len());
186                leaf.push(identifier.to_field());
187                leaf.extend(fields);
188                leaf
189            })
190            .collect::<Vec<Vec<Field<A>>>>();
191
192        // Construct the merkle tree
193        RecordDataTree::<A>::new(circuit_leaf_hasher, circuit_path_hasher, &leaves)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::Circuit;
201    use snarkvm_circuit_types::environment::{Inject, assert_scope};
202    use snarkvm_utilities::{TestRng, Uniform};
203
204    use core::str::FromStr;
205
206    type CurrentNetwork = <Circuit as Environment>::Network;
207    type ConsoleRecord = console::Record<CurrentNetwork, console::Plaintext<CurrentNetwork>>;
208
209    /// Verifies circuit/console equivalence for a record parsed from a string.
210    /// This helper enables easier testing of various record structures.
211    fn check_circuit_console_equivalence(
212        record_str: &str,
213        num_constants: u64,
214        num_public: u64,
215        num_private: u64,
216        num_constraints: u64,
217    ) {
218        // Parse the record from the string.
219        let console_record = ConsoleRecord::from_str(record_str).unwrap();
220
221        // Convert to console DynamicRecord.
222        let console_dynamic = console::DynamicRecord::from_record(&console_record).unwrap();
223
224        // Inject the console record into the circuit.
225        let circuit_record = Record::<Circuit, Plaintext<Circuit>>::new(Mode::Private, console_record);
226
227        Circuit::scope("check_circuit_console_equivalence", || {
228            // Convert to circuit DynamicRecord.
229            let circuit_dynamic = DynamicRecord::<Circuit>::from_record(&circuit_record).unwrap();
230
231            // Verify the circuit root matches the console root.
232            let circuit_root = circuit_dynamic.root().eject_value();
233            let console_root = *console_dynamic.root();
234            assert_eq!(
235                circuit_root, console_root,
236                "Circuit and console DynamicRecord should produce the same Merkle root"
237            );
238
239            // Verify other fields match.
240            assert_eq!(circuit_dynamic.owner().eject_value(), *console_dynamic.owner());
241            assert_eq!(circuit_dynamic.nonce().eject_value(), *console_dynamic.nonce());
242            assert_eq!(circuit_dynamic.version().eject_value(), *console_dynamic.version());
243
244            // Verify circuit constraint counts.
245            assert_scope!(num_constants, num_public, num_private, num_constraints);
246        });
247
248        Circuit::reset();
249    }
250
251    /// Creates a console record with the given data for testing.
252    fn create_console_record(
253        rng: &mut TestRng,
254        data: console::RecordData<CurrentNetwork>,
255        owner_is_private: bool,
256    ) -> ConsoleRecord {
257        let owner = match owner_is_private {
258            true => console::Owner::Private(console::Plaintext::from(console::Literal::Address(
259                console::Address::rand(rng),
260            ))),
261            false => console::Owner::Public(console::Address::rand(rng)),
262        };
263        ConsoleRecord::from_plaintext(owner, data, console::Group::rand(rng), console::U8::new(0)).unwrap()
264    }
265
266    #[test]
267    fn test_circuit_console_equivalence_empty_record() {
268        // Empty record with public owner.
269        let record_str = r#"{
270          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
271          _nonce: 0group.public,
272          _version: 0u8.public
273        }"#;
274        check_circuit_console_equivalence(record_str, 1075, 0, 0, 0);
275    }
276
277    #[test]
278    fn test_circuit_console_equivalence_single_private_field() {
279        // Record with a single private u64 field.
280        let record_str = r#"{
281          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
282          amount: 100u64.private,
283          _nonce: 0group.public,
284          _version: 0u8.public
285        }"#;
286        check_circuit_console_equivalence(record_str, 1100, 0, 3175, 3175);
287    }
288
289    #[test]
290    fn test_circuit_console_equivalence_single_public_field() {
291        // Record with a single public u64 field.
292        let record_str = r#"{
293          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
294          amount: 100u64.public,
295          _nonce: 0group.public,
296          _version: 0u8.public
297        }"#;
298        check_circuit_console_equivalence(record_str, 1100, 0, 3175, 3175);
299    }
300
301    #[test]
302    fn test_circuit_console_equivalence_single_constant_field() {
303        // Record with a single constant u64 field.
304        let record_str = r#"{
305          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
306          amount: 100u64.constant,
307          _nonce: 0group.public,
308          _version: 0u8.public
309        }"#;
310        check_circuit_console_equivalence(record_str, 1100, 0, 3175, 3175);
311    }
312
313    #[test]
314    fn test_circuit_console_equivalence_mixed_entry_types() {
315        let rng = &mut TestRng::default();
316
317        // Create test data with mixed entry types (private, public, constant).
318        let mut data = IndexMap::new();
319        data.insert(
320            console::Identifier::from_str("a").unwrap(),
321            console::Entry::Private(console::Plaintext::from(console::Literal::U64(console::U64::rand(rng)))),
322        );
323        data.insert(
324            console::Identifier::from_str("b").unwrap(),
325            console::Entry::Public(console::Plaintext::from(console::Literal::U64(console::U64::rand(rng)))),
326        );
327        data.insert(
328            console::Identifier::from_str("c").unwrap(),
329            console::Entry::Constant(console::Plaintext::from(console::Literal::U64(console::U64::rand(rng)))),
330        );
331
332        // Create the record and convert to string for consistent testing.
333        let console_record = create_console_record(rng, data, true);
334        let record_str = console_record.to_string();
335
336        check_circuit_console_equivalence(&record_str, 1151, 0, 4665, 4665);
337    }
338
339    #[test]
340    fn test_circuit_console_equivalence_nested_struct() {
341        let rng = &mut TestRng::default();
342
343        // Create a nested struct entry.
344        let mut inner_map = IndexMap::new();
345        inner_map.insert(
346            console::Identifier::from_str("x").unwrap(),
347            console::Plaintext::from(console::Literal::U64(console::U64::rand(rng))),
348        );
349        inner_map.insert(
350            console::Identifier::from_str("y").unwrap(),
351            console::Plaintext::from(console::Literal::U64(console::U64::rand(rng))),
352        );
353        let inner = console::Plaintext::Struct(inner_map, Default::default());
354
355        let mut data = IndexMap::new();
356        data.insert(console::Identifier::from_str("point").unwrap(), console::Entry::Private(inner));
357
358        // Create the record and convert to string for consistent testing.
359        let console_record = create_console_record(rng, data, false);
360        let record_str = console_record.to_string();
361
362        check_circuit_console_equivalence(&record_str, 1180, 0, 3180, 3180);
363    }
364
365    #[test]
366    fn test_circuit_console_equivalence_private_owner() {
367        // Record with private owner.
368        let record_str = r#"{
369          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.private,
370          _nonce: 0group.public,
371          _version: 0u8.public
372        }"#;
373        check_circuit_console_equivalence(record_str, 1075, 0, 0, 0);
374    }
375
376    #[test]
377    fn test_circuit_console_equivalence_multiple_fields() {
378        // Record with multiple fields of the same visibility.
379        let record_str = r#"{
380          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
381          x: 1u64.private,
382          y: 2u64.private,
383          z: 3u64.private,
384          _nonce: 0group.public,
385          _version: 0u8.public
386        }"#;
387        check_circuit_console_equivalence(record_str, 1151, 0, 4665, 4665);
388    }
389
390    #[test]
391    fn test_circuit_console_equivalence_boolean_field() {
392        // Record with a boolean field.
393        let record_str = r#"{
394          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
395          flag: true.private,
396          _nonce: 0group.public,
397          _version: 0u8.public
398        }"#;
399        check_circuit_console_equivalence(record_str, 1100, 0, 3175, 3175);
400    }
401
402    #[test]
403    fn test_circuit_console_equivalence_address_field() {
404        // Record with an address field.
405        let record_str = r#"{
406          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
407          recipient: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.private,
408          _nonce: 0group.public,
409          _version: 0u8.public
410        }"#;
411        check_circuit_console_equivalence(record_str, 1100, 0, 3685, 3687);
412    }
413
414    #[test]
415    fn test_find_owner() {
416        let record_str = r#"{
417          owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.public,
418          _nonce: 0group.public,
419          _version: 0u8.public
420        }"#;
421        let console_record = ConsoleRecord::from_str(record_str).unwrap();
422        let circuit_record = Record::<Circuit, Plaintext<Circuit>>::new(Mode::Private, console_record);
423        let circuit_dynamic = DynamicRecord::<Circuit>::from_record(&circuit_record).unwrap();
424
425        // Finding "owner" must succeed.
426        let path = [Access::Member(Identifier::from_str("owner").unwrap())];
427        assert!(circuit_dynamic.find(&path).is_ok());
428
429        // Any path other than "owner" must fail.
430        let path_bad = [Access::Member(Identifier::from_str("data").unwrap())];
431        assert!(circuit_dynamic.find(&path_bad).is_err());
432    }
433}