Skip to main content

snarkvm_console_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 bytes;
17mod equal;
18mod find;
19mod parse;
20mod to_bits;
21mod to_fields;
22
23use crate::{
24    Access,
25    Address,
26    Boolean,
27    Entry,
28    Field,
29    Group,
30    Identifier,
31    Literal,
32    Network,
33    Owner,
34    Plaintext,
35    Record,
36    Result,
37    ToField,
38    ToFields,
39    U8,
40    Value,
41};
42
43use snarkvm_console_algorithms::{Poseidon2, Poseidon8};
44use snarkvm_console_collections::merkle_tree::MerkleTree;
45use snarkvm_console_network::*;
46
47use indexmap::IndexMap;
48
49/// The depth of the record data tree.
50pub const RECORD_DATA_TREE_DEPTH: u8 = 5;
51
52/// The record data tree.
53pub type RecordDataTree<E> = MerkleTree<E, Poseidon8<E>, Poseidon2<E>, RECORD_DATA_TREE_DEPTH>;
54/// The console data.
55pub type RecordData<N> = IndexMap<Identifier<N>, Entry<N, Plaintext<N>>>;
56
57/// A dynamic record is a fixed-size representation of a record. Like static
58/// `Record`s, a dynamic record contains an owner, nonce, and a version.
59/// However, instead of storing the full data, it only stores the Merkle root of
60/// the data. This ensures that all dynamic records have a constant size,
61/// regardless of the amount of data they contain.
62///
63/// Suppose we have the following record with two data entries:
64///
65/// ```text
66/// record foo:
67///     owner as address.private;
68///     microcredits as u64.private;
69///     memo as [u8; 32u32].public;
70/// ```
71///
72/// The leaves of its Merkle tree are computed as follows:
73///
74/// ```text
75/// L_0 := HashPSD8(ToField(name_0) || ToFields(entry_0))
76/// L_1 := HashPSD8(ToField(name_1) || ToFields(entry_1))
77/// ```
78///
79/// where `name_i` is the field encoding of the entry identifier (e.g. `"microcredits"` → `Field`),
80/// and `ToFields` packs the entry's mode tag bits (2 bits), plaintext bits, and a terminus `1`
81/// bit into field elements. The terminus bit ensures collision-resistance across entries of
82/// different lengths.
83///
84/// The tree has depth `RECORD_DATA_TREE_DEPTH = 5` and is constructed with
85/// path hasher `HashPSD2` and the padding scheme outlined in
86/// [`snarkVM`'s `MerkleTree`](snarkvm_console_collections::merkle_tree::MerkleTree).
87#[derive(Clone)]
88pub struct DynamicRecord<N: Network> {
89    /// The owner of the record.
90    owner: Address<N>,
91    /// The Merkle root of the record data.
92    root: Field<N>,
93    /// The nonce of the record.
94    nonce: Group<N>,
95    /// The version of the record.
96    version: U8<N>,
97    /// The optional record data.
98    data: Option<RecordData<N>>,
99}
100
101impl<N: Network> DynamicRecord<N> {
102    /// Initializes a dynamic record without checking that the root, tree, and data are consistent.
103    pub const fn new_unchecked(
104        owner: Address<N>,
105        root: Field<N>,
106        nonce: Group<N>,
107        version: U8<N>,
108        data: Option<RecordData<N>>,
109    ) -> Self {
110        Self { owner, root, nonce, version, data }
111    }
112}
113
114impl<N: Network> DynamicRecord<N> {
115    /// Returns the owner of the record.
116    pub const fn owner(&self) -> &Address<N> {
117        &self.owner
118    }
119
120    /// Returns the Merkle root of the record data.
121    pub const fn root(&self) -> &Field<N> {
122        &self.root
123    }
124
125    /// Returns the nonce of the record.
126    pub const fn nonce(&self) -> &Group<N> {
127        &self.nonce
128    }
129
130    /// Returns the version of the record.
131    pub const fn version(&self) -> &U8<N> {
132        &self.version
133    }
134
135    /// Returns the optional record data.
136    pub const fn data(&self) -> &Option<RecordData<N>> {
137        &self.data
138    }
139
140    /// Returns `true` if the dynamic record is a hiding variant.
141    pub fn is_hiding(&self) -> bool {
142        !self.version.is_zero()
143    }
144}
145
146impl<N: Network> DynamicRecord<N> {
147    /// Creates a dynamic record from a static record.
148    pub fn from_record(record: &Record<N, Plaintext<N>>) -> Result<Self> {
149        // Get the owner.
150        let owner = *record.owner().clone();
151        // Get the record data.
152        let data = record.data().clone();
153        // Get the nonce.
154        let nonce = *record.nonce();
155        // Get the version.
156        let version = *record.version();
157
158        // Construct the merkle tree.
159        let tree = Self::merkleize_data(&data)?;
160
161        // Get the root.
162        let root = *tree.root();
163
164        Ok(Self::new_unchecked(owner, root, nonce, version, Some(data)))
165    }
166
167    /// Creates a static record from this dynamic record.
168    pub fn to_record(&self, owner_is_private: bool) -> Result<Record<N, Plaintext<N>>> {
169        // Ensure that the data is present.
170        let Some(data) = &self.data else {
171            bail!("Cannot convert a dynamic record to static record without the underlying data");
172        };
173        // Create the owner.
174        let owner = match owner_is_private {
175            false => Owner::<N, Plaintext<N>>::Public(self.owner),
176            true => Owner::<N, Plaintext<N>>::Private(Plaintext::from(Literal::Address(self.owner))),
177        };
178        // Return the record.
179        Record::<N, Plaintext<N>>::from_plaintext(owner, data.clone(), self.nonce, self.version)
180    }
181
182    /// Computes the Merkle tree containing the given (ordered) entries as
183    /// leaves. More details on the structure of the tree can be found in
184    /// [`DynamicRecord`].
185    pub fn merkleize_data(data: &IndexMap<Identifier<N>, Entry<N, Plaintext<N>>>) -> Result<RecordDataTree<N>> {
186        // Construct the leaves.
187        let leaves = data
188            .iter()
189            .map(|(name, entry)| {
190                // Compute the entry fields.
191                let fields = entry.to_fields()?;
192                // Initialize the leaf with sufficient capacity.
193                let mut leaf = Vec::with_capacity(1 + fields.len());
194                // Add the entry name.
195                leaf.push(name.to_field()?);
196                // Add the entry data.
197                leaf.extend(fields);
198
199                Ok(leaf)
200            })
201            .collect::<Result<Vec<_>>>()?;
202
203        // Initialize the hashers.
204        let (leaf_hasher, path_hasher) = Self::initialize_hashers();
205
206        // Construct the merkle tree.
207        RecordDataTree::new(leaf_hasher, path_hasher, &leaves)
208    }
209
210    /// Returns the leaf and path hashers used to merkleize record entries.
211    pub fn initialize_hashers() -> (&'static Poseidon8<N>, &'static Poseidon2<N>) {
212        (N::dynamic_record_leaf_hasher(), N::dynamic_record_path_hasher())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use snarkvm_console_network::MainnetV0;
220
221    use crate::{Entry, Literal, Owner, Record};
222    use snarkvm_console_types::{Address, Group, U8, U64};
223    use snarkvm_utilities::{TestRng, Uniform};
224
225    use core::str::FromStr;
226
227    type CurrentNetwork = MainnetV0;
228
229    #[test]
230    fn test_data_depth() {
231        assert_eq!(CurrentNetwork::MAX_DATA_ENTRIES.ilog2(), RECORD_DATA_TREE_DEPTH as u32);
232    }
233
234    fn create_test_record(
235        rng: &mut TestRng,
236        data: RecordData<CurrentNetwork>,
237        owner_is_private: bool,
238    ) -> Record<CurrentNetwork, Plaintext<CurrentNetwork>> {
239        let owner = match owner_is_private {
240            true => Owner::Private(Plaintext::from(Literal::Address(Address::rand(rng)))),
241            false => Owner::Public(Address::rand(rng)),
242        };
243        Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_plaintext(owner, data, Group::rand(rng), U8::new(0))
244            .unwrap()
245    }
246
247    fn assert_round_trip(record: &Record<CurrentNetwork, Plaintext<CurrentNetwork>>, owner_is_private: bool) {
248        let dynamic = DynamicRecord::from_record(record).unwrap();
249        let recovered = dynamic.to_record(owner_is_private).unwrap();
250        assert_eq!(record.nonce(), recovered.nonce());
251        assert_eq!(record.data(), recovered.data());
252    }
253
254    #[test]
255    fn test_round_trip_various_records() {
256        let rng = &mut TestRng::default();
257
258        // Empty record.
259        let record = create_test_record(rng, indexmap::IndexMap::new(), false);
260        assert_round_trip(&record, false);
261
262        // Private entries.
263        let data = indexmap::indexmap! {
264            Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
265            Identifier::from_str("b").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
266        };
267        let record = create_test_record(rng, data, true);
268        assert_round_trip(&record, true);
269
270        // Public entries.
271        let data = indexmap::indexmap! {
272            Identifier::from_str("x").unwrap() => Entry::Public(Plaintext::from(Literal::U64(U64::rand(rng)))),
273        };
274        let record = create_test_record(rng, data, false);
275        assert_round_trip(&record, false);
276
277        // Mixed visibility.
278        let data = indexmap::indexmap! {
279            Identifier::from_str("pub").unwrap() => Entry::Public(Plaintext::from(Literal::U64(U64::rand(rng)))),
280            Identifier::from_str("priv").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
281            Identifier::from_str("const").unwrap() => Entry::Constant(Plaintext::from(Literal::U64(U64::rand(rng)))),
282        };
283        let record = create_test_record(rng, data, true);
284        assert_round_trip(&record, true);
285
286        // Struct entry.
287        let inner = Plaintext::Struct(
288            indexmap::indexmap! {
289                Identifier::from_str("x").unwrap() => Plaintext::from(Literal::U64(U64::rand(rng))),
290                Identifier::from_str("y").unwrap() => Plaintext::from(Literal::U64(U64::rand(rng))),
291            },
292            Default::default(),
293        );
294        let data = indexmap::indexmap! {
295            Identifier::from_str("point").unwrap() => Entry::Private(inner),
296        };
297        let record = create_test_record(rng, data, false);
298        assert_round_trip(&record, false);
299    }
300
301    #[test]
302    fn test_root_determinism() {
303        let rng = &mut TestRng::default();
304
305        let data1 = indexmap::indexmap! {
306            Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::new(100)))),
307        };
308        let data2 = indexmap::indexmap! {
309            Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::new(100)))),
310        };
311        let data3 = indexmap::indexmap! {
312            Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::new(200)))),
313        };
314
315        let r1 = DynamicRecord::from_record(&create_test_record(rng, data1, false)).unwrap();
316        let r2 = DynamicRecord::from_record(&create_test_record(rng, data2, false)).unwrap();
317        let r3 = DynamicRecord::from_record(&create_test_record(rng, data3, false)).unwrap();
318
319        assert_eq!(r1.root(), r2.root(), "Same data should produce same root");
320        assert_ne!(r1.root(), r3.root(), "Different data should produce different roots");
321    }
322
323    #[test]
324    fn test_membership_proofs() {
325        let rng = &mut TestRng::default();
326
327        let data = indexmap::indexmap! {
328            Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
329            Identifier::from_str("b").unwrap() => Entry::Public(Plaintext::from(Literal::U64(U64::rand(rng)))),
330            Identifier::from_str("c").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
331        };
332
333        // Build tree and get leaves.
334        let tree = DynamicRecord::<CurrentNetwork>::merkleize_data(&data).unwrap();
335        let leaves: Vec<_> = data
336            .iter()
337            .map(|(name, entry)| {
338                let mut leaf = vec![name.to_field().unwrap()];
339                leaf.extend(entry.to_fields().unwrap());
340                leaf
341            })
342            .collect();
343
344        // Valid proofs.
345        for (i, leaf) in leaves.iter().enumerate() {
346            let path = tree.prove(i, leaf).unwrap();
347            assert!(tree.verify(&path, tree.root(), leaf));
348        }
349
350        // Invalid proofs.
351        let path = tree.prove(0, &leaves[0]).unwrap();
352        assert!(!tree.verify(&path, tree.root(), &leaves[1])); // Wrong leaf.
353        assert!(!tree.verify(&path, &Field::from_u64(12345), &leaves[0])); // Wrong root.
354    }
355
356    #[test]
357    fn test_find_owner() {
358        let rng = &mut TestRng::default();
359        let owner_addr = Address::rand(rng);
360        let record = Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_plaintext(
361            Owner::Public(owner_addr),
362            indexmap::IndexMap::new(),
363            Group::rand(rng),
364            U8::new(0),
365        )
366        .unwrap();
367        let dynamic = DynamicRecord::from_record(&record).unwrap();
368
369        // Finding "owner" must return the owner address.
370        let path = [Access::Member(Identifier::from_str("owner").unwrap())];
371        let value = dynamic.find(&path).unwrap();
372        assert_eq!(value, Value::Plaintext(Plaintext::from(Literal::Address(owner_addr))));
373    }
374
375    #[test]
376    fn test_find_rejects_non_owner_paths() {
377        let rng = &mut TestRng::default();
378        let record = create_test_record(rng, indexmap::IndexMap::new(), false);
379        let dynamic = DynamicRecord::from_record(&record).unwrap();
380
381        // Any path other than "owner" must be rejected.
382        let path = [Access::Member(Identifier::from_str("data").unwrap())];
383        assert!(dynamic.find(&path).is_err());
384
385        // An empty path must be rejected.
386        let empty: &[Access<CurrentNetwork>] = &[];
387        assert!(dynamic.find(empty).is_err());
388
389        // A path of length > 1 must be rejected.
390        let long_path = [
391            Access::Member(Identifier::from_str("owner").unwrap()),
392            Access::Member(Identifier::from_str("nested").unwrap()),
393        ];
394        assert!(dynamic.find(&long_path).is_err());
395    }
396}