mod bytes;
mod equal;
mod find;
mod parse;
mod to_bits;
mod to_fields;
use crate::{
Access,
Address,
Boolean,
Entry,
Field,
Group,
Identifier,
Literal,
Network,
Owner,
Plaintext,
Record,
Result,
ToField,
ToFields,
U8,
Value,
};
use snarkvm_console_algorithms::{Poseidon2, Poseidon8};
use snarkvm_console_collections::merkle_tree::MerkleTree;
use snarkvm_console_network::*;
use indexmap::IndexMap;
pub const RECORD_DATA_TREE_DEPTH: u8 = 5;
pub type RecordDataTree<E> = MerkleTree<E, Poseidon8<E>, Poseidon2<E>, RECORD_DATA_TREE_DEPTH>;
pub type RecordData<N> = IndexMap<Identifier<N>, Entry<N, Plaintext<N>>>;
#[derive(Clone)]
pub struct DynamicRecord<N: Network> {
owner: Address<N>,
root: Field<N>,
nonce: Group<N>,
version: U8<N>,
data: Option<RecordData<N>>,
}
impl<N: Network> DynamicRecord<N> {
pub const fn new_unchecked(
owner: Address<N>,
root: Field<N>,
nonce: Group<N>,
version: U8<N>,
data: Option<RecordData<N>>,
) -> Self {
Self { owner, root, nonce, version, data }
}
}
impl<N: Network> DynamicRecord<N> {
pub const fn owner(&self) -> &Address<N> {
&self.owner
}
pub const fn root(&self) -> &Field<N> {
&self.root
}
pub const fn nonce(&self) -> &Group<N> {
&self.nonce
}
pub const fn version(&self) -> &U8<N> {
&self.version
}
pub const fn data(&self) -> &Option<RecordData<N>> {
&self.data
}
pub fn is_hiding(&self) -> bool {
!self.version.is_zero()
}
}
impl<N: Network> DynamicRecord<N> {
pub fn from_record(record: &Record<N, Plaintext<N>>) -> Result<Self> {
let owner = *record.owner().clone();
let data = record.data().clone();
let nonce = *record.nonce();
let version = *record.version();
let tree = Self::merkleize_data(&data)?;
let root = *tree.root();
Ok(Self::new_unchecked(owner, root, nonce, version, Some(data)))
}
pub fn to_record(&self, owner_is_private: bool) -> Result<Record<N, Plaintext<N>>> {
let Some(data) = &self.data else {
bail!("Cannot convert a dynamic record to static record without the underlying data");
};
let owner = match owner_is_private {
false => Owner::<N, Plaintext<N>>::Public(self.owner),
true => Owner::<N, Plaintext<N>>::Private(Plaintext::from(Literal::Address(self.owner))),
};
Record::<N, Plaintext<N>>::from_plaintext(owner, data.clone(), self.nonce, self.version)
}
pub fn merkleize_data(data: &IndexMap<Identifier<N>, Entry<N, Plaintext<N>>>) -> Result<RecordDataTree<N>> {
let leaves = data
.iter()
.map(|(name, entry)| {
let fields = entry.to_fields()?;
let mut leaf = Vec::with_capacity(1 + fields.len());
leaf.push(name.to_field()?);
leaf.extend(fields);
Ok(leaf)
})
.collect::<Result<Vec<_>>>()?;
let (leaf_hasher, path_hasher) = Self::initialize_hashers();
RecordDataTree::new(leaf_hasher, path_hasher, &leaves)
}
pub fn initialize_hashers() -> (&'static Poseidon8<N>, &'static Poseidon2<N>) {
(N::dynamic_record_leaf_hasher(), N::dynamic_record_path_hasher())
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm_console_network::MainnetV0;
use crate::{Entry, Literal, Owner, Record};
use snarkvm_console_types::{Address, Group, U8, U64};
use snarkvm_utilities::{TestRng, Uniform};
use core::str::FromStr;
type CurrentNetwork = MainnetV0;
#[test]
fn test_data_depth() {
assert_eq!(CurrentNetwork::MAX_DATA_ENTRIES.ilog2(), RECORD_DATA_TREE_DEPTH as u32);
}
fn create_test_record(
rng: &mut TestRng,
data: RecordData<CurrentNetwork>,
owner_is_private: bool,
) -> Record<CurrentNetwork, Plaintext<CurrentNetwork>> {
let owner = match owner_is_private {
true => Owner::Private(Plaintext::from(Literal::Address(Address::rand(rng)))),
false => Owner::Public(Address::rand(rng)),
};
Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_plaintext(owner, data, Group::rand(rng), U8::new(0))
.unwrap()
}
fn assert_round_trip(record: &Record<CurrentNetwork, Plaintext<CurrentNetwork>>, owner_is_private: bool) {
let dynamic = DynamicRecord::from_record(record).unwrap();
let recovered = dynamic.to_record(owner_is_private).unwrap();
assert_eq!(record.nonce(), recovered.nonce());
assert_eq!(record.data(), recovered.data());
}
#[test]
fn test_round_trip_various_records() {
let rng = &mut TestRng::default();
let record = create_test_record(rng, indexmap::IndexMap::new(), false);
assert_round_trip(&record, false);
let data = indexmap::indexmap! {
Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
Identifier::from_str("b").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
};
let record = create_test_record(rng, data, true);
assert_round_trip(&record, true);
let data = indexmap::indexmap! {
Identifier::from_str("x").unwrap() => Entry::Public(Plaintext::from(Literal::U64(U64::rand(rng)))),
};
let record = create_test_record(rng, data, false);
assert_round_trip(&record, false);
let data = indexmap::indexmap! {
Identifier::from_str("pub").unwrap() => Entry::Public(Plaintext::from(Literal::U64(U64::rand(rng)))),
Identifier::from_str("priv").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
Identifier::from_str("const").unwrap() => Entry::Constant(Plaintext::from(Literal::U64(U64::rand(rng)))),
};
let record = create_test_record(rng, data, true);
assert_round_trip(&record, true);
let inner = Plaintext::Struct(
indexmap::indexmap! {
Identifier::from_str("x").unwrap() => Plaintext::from(Literal::U64(U64::rand(rng))),
Identifier::from_str("y").unwrap() => Plaintext::from(Literal::U64(U64::rand(rng))),
},
Default::default(),
);
let data = indexmap::indexmap! {
Identifier::from_str("point").unwrap() => Entry::Private(inner),
};
let record = create_test_record(rng, data, false);
assert_round_trip(&record, false);
}
#[test]
fn test_root_determinism() {
let rng = &mut TestRng::default();
let data1 = indexmap::indexmap! {
Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::new(100)))),
};
let data2 = indexmap::indexmap! {
Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::new(100)))),
};
let data3 = indexmap::indexmap! {
Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::new(200)))),
};
let r1 = DynamicRecord::from_record(&create_test_record(rng, data1, false)).unwrap();
let r2 = DynamicRecord::from_record(&create_test_record(rng, data2, false)).unwrap();
let r3 = DynamicRecord::from_record(&create_test_record(rng, data3, false)).unwrap();
assert_eq!(r1.root(), r2.root(), "Same data should produce same root");
assert_ne!(r1.root(), r3.root(), "Different data should produce different roots");
}
#[test]
fn test_membership_proofs() {
let rng = &mut TestRng::default();
let data = indexmap::indexmap! {
Identifier::from_str("a").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
Identifier::from_str("b").unwrap() => Entry::Public(Plaintext::from(Literal::U64(U64::rand(rng)))),
Identifier::from_str("c").unwrap() => Entry::Private(Plaintext::from(Literal::U64(U64::rand(rng)))),
};
let tree = DynamicRecord::<CurrentNetwork>::merkleize_data(&data).unwrap();
let leaves: Vec<_> = data
.iter()
.map(|(name, entry)| {
let mut leaf = vec![name.to_field().unwrap()];
leaf.extend(entry.to_fields().unwrap());
leaf
})
.collect();
for (i, leaf) in leaves.iter().enumerate() {
let path = tree.prove(i, leaf).unwrap();
assert!(tree.verify(&path, tree.root(), leaf));
}
let path = tree.prove(0, &leaves[0]).unwrap();
assert!(!tree.verify(&path, tree.root(), &leaves[1])); assert!(!tree.verify(&path, &Field::from_u64(12345), &leaves[0])); }
#[test]
fn test_find_owner() {
let rng = &mut TestRng::default();
let owner_addr = Address::rand(rng);
let record = Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_plaintext(
Owner::Public(owner_addr),
indexmap::IndexMap::new(),
Group::rand(rng),
U8::new(0),
)
.unwrap();
let dynamic = DynamicRecord::from_record(&record).unwrap();
let path = [Access::Member(Identifier::from_str("owner").unwrap())];
let value = dynamic.find(&path).unwrap();
assert_eq!(value, Value::Plaintext(Plaintext::from(Literal::Address(owner_addr))));
}
#[test]
fn test_find_rejects_non_owner_paths() {
let rng = &mut TestRng::default();
let record = create_test_record(rng, indexmap::IndexMap::new(), false);
let dynamic = DynamicRecord::from_record(&record).unwrap();
let path = [Access::Member(Identifier::from_str("data").unwrap())];
assert!(dynamic.find(&path).is_err());
let empty: &[Access<CurrentNetwork>] = &[];
assert!(dynamic.find(empty).is_err());
let long_path = [
Access::Member(Identifier::from_str("owner").unwrap()),
Access::Member(Identifier::from_str("nested").unwrap()),
];
assert!(dynamic.find(&long_path).is_err());
}
}