Merge pull request #1058 from zcash/1044-sapling-tree-leaf-refactor

Refactor `sapling::tree::Node`
This commit is contained in:
str4d 2023-12-05 12:29:12 +00:00 committed by GitHub
commit fedc9af1f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 50 deletions

View File

@ -52,6 +52,7 @@ and this library adheres to Rust's notion of
- `note_encryption::SaplingDomain::new` - `note_encryption::SaplingDomain::new`
- `note_encryption::Zip212Enforcement` - `note_encryption::Zip212Enforcement`
- `prover::{SpendProver, OutputProver}` - `prover::{SpendProver, OutputProver}`
- `tree::Node::{from_bytes, to_bytes}`
- `value`: - `value`:
- `ValueCommitTrapdoor::from_bytes` - `ValueCommitTrapdoor::from_bytes`
- `impl Sub<TrapdoorSum> for TrapdoorSum` - `impl Sub<TrapdoorSum> for TrapdoorSum`

View File

@ -23,6 +23,23 @@ pub trait HashSer {
fn write<W: Write>(&self, writer: W) -> io::Result<()>; fn write<W: Write>(&self, writer: W) -> io::Result<()>;
} }
impl HashSer for sapling::Node {
fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let mut repr = [0u8; 32];
reader.read_exact(&mut repr)?;
Option::from(Self::from_bytes(repr)).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"Non-canonical encoding of Jubjub base field value.",
)
})
}
fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.to_bytes())
}
}
impl HashSer for MerkleHashOrchard { impl HashSer for MerkleHashOrchard {
fn read<R: Read>(mut reader: R) -> io::Result<Self> fn read<R: Read>(mut reader: R) -> io::Result<Self>
where where
@ -815,7 +832,7 @@ mod tests {
for i in 0..16 { for i in 0..16 {
let cmu = hex::decode(commitments[i]).unwrap(); let cmu = hex::decode(commitments[i]).unwrap();
let cmu = Node::new(cmu[..].try_into().unwrap()); let cmu = Node::from_bytes(cmu[..].try_into().unwrap()).unwrap();
// Witness here // Witness here
witnesses.push((IncrementalWitness::from_tree(tree.clone()), last_cmu)); witnesses.push((IncrementalWitness::from_tree(tree.clone()), last_cmu));

View File

@ -631,7 +631,7 @@ pub mod testing {
use crate::{ use crate::{
sapling::{ sapling::{
note::ExtractedNoteCommitment, note::testing::arb_cmu,
value::{ value::{
testing::{arb_note_value_bounded, arb_trapdoor}, testing::{arb_note_value_bounded, arb_trapdoor},
ValueCommitment, MAX_NOTE_VALUE, ValueCommitment, MAX_NOTE_VALUE,
@ -691,9 +691,7 @@ pub mod testing {
pub fn arb_output_description(n_outputs: usize)( pub fn arb_output_description(n_outputs: usize)(
value in arb_note_value_bounded(MAX_NOTE_VALUE.checked_div(n_outputs as u64).unwrap_or(0)), value in arb_note_value_bounded(MAX_NOTE_VALUE.checked_div(n_outputs as u64).unwrap_or(0)),
rcv in arb_trapdoor(), rcv in arb_trapdoor(),
cmu in vec(any::<u8>(), 64) cmu in arb_cmu(),
.prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap())
.prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)),
enc_ciphertext in vec(any::<u8>(), ENC_CIPHERTEXT_SIZE) enc_ciphertext in vec(any::<u8>(), ENC_CIPHERTEXT_SIZE)
.prop_map(|v| <[u8; ENC_CIPHERTEXT_SIZE]>::try_from(v.as_slice()).unwrap()), .prop_map(|v| <[u8; ENC_CIPHERTEXT_SIZE]>::try_from(v.as_slice()).unwrap()),
epk in arb_extended_point(), epk in arb_extended_point(),
@ -703,7 +701,6 @@ pub mod testing {
.prop_map(|v| <[u8; GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()), .prop_map(|v| <[u8; GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()),
) -> OutputDescription<GrothProofBytes> { ) -> OutputDescription<GrothProofBytes> {
let cv = ValueCommitment::derive(value, rcv); let cv = ValueCommitment::derive(value, rcv);
let cmu = ExtractedNoteCommitment::from_bytes(&cmu.to_bytes()).unwrap();
OutputDescription { OutputDescription {
cv, cv,
cmu, cmu,

View File

@ -152,11 +152,11 @@ impl Note {
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub(super) mod testing { pub(super) mod testing {
use proptest::prelude::*; use proptest::{collection::vec, prelude::*};
use super::{ use super::{
super::{testing::arb_payment_address, value::NoteValue}, super::{testing::arb_payment_address, value::NoteValue},
Note, Rseed, ExtractedNoteCommitment, Note, Rseed,
}; };
prop_compose! { prop_compose! {
@ -171,4 +171,14 @@ pub(super) mod testing {
} }
} }
} }
prop_compose! {
pub(crate) fn arb_cmu()(
cmu in vec(any::<u8>(), 64)
.prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap())
.prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)),
) -> ExtractedNoteCommitment {
ExtractedNoteCommitment(cmu)
}
}
} }

View File

@ -77,6 +77,10 @@ impl ExtractedNoteCommitment {
pub fn to_bytes(self) -> [u8; 32] { pub fn to_bytes(self) -> [u8; 32] {
self.0.to_repr() self.0.to_repr()
} }
pub(crate) fn inner(&self) -> jubjub::Base {
self.0
}
} }
impl From<NoteCommitment> for ExtractedNoteCommitment { impl From<NoteCommitment> for ExtractedNoteCommitment {

View File

@ -2,15 +2,14 @@ use bitvec::{order::Lsb0, view::AsBits};
use group::{ff::PrimeField, Curve}; use group::{ff::PrimeField, Curve};
use incrementalmerkletree::{Hashable, Level}; use incrementalmerkletree::{Hashable, Level};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use subtle::CtOption;
use std::fmt; use std::fmt;
use std::io::{self, Read, Write};
use super::{ use super::{
note::ExtractedNoteCommitment, note::ExtractedNoteCommitment,
pedersen_hash::{pedersen_hash, Personalization}, pedersen_hash::{pedersen_hash, Personalization},
}; };
use crate::merkle_tree::HashSer;
pub const NOTE_COMMITMENT_TREE_DEPTH: u8 = 32; pub const NOTE_COMMITMENT_TREE_DEPTH: u8 = 32;
pub type CommitmentTree = pub type CommitmentTree =
@ -33,6 +32,10 @@ lazy_static! {
/// Compute a parent node in the Sapling commitment tree given its two children. /// Compute a parent node in the Sapling commitment tree given its two children.
pub fn merkle_hash(depth: usize, lhs: &[u8; 32], rhs: &[u8; 32]) -> [u8; 32] { pub fn merkle_hash(depth: usize, lhs: &[u8; 32], rhs: &[u8; 32]) -> [u8; 32] {
merkle_hash_field(depth, lhs, rhs).to_repr()
}
fn merkle_hash_field(depth: usize, lhs: &[u8; 32], rhs: &[u8; 32]) -> jubjub::Base {
let lhs = { let lhs = {
let mut tmp = [false; 256]; let mut tmp = [false; 256];
for (a, b) in tmp.iter_mut().zip(lhs.as_bits::<Lsb0>()) { for (a, b) in tmp.iter_mut().zip(lhs.as_bits::<Lsb0>()) {
@ -62,55 +65,55 @@ pub fn merkle_hash(depth: usize, lhs: &[u8; 32], rhs: &[u8; 32]) -> [u8; 32] {
)) ))
.to_affine() .to_affine()
.get_u() .get_u()
.to_repr()
} }
/// A node within the Sapling commitment tree. /// A node within the Sapling commitment tree.
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct Node { pub struct Node(jubjub::Base);
pub(super) repr: [u8; 32],
}
impl fmt::Debug for Node { impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Node") f.debug_struct("Node")
.field("repr", &hex::encode(self.repr)) .field("repr", &hex::encode(self.0.to_bytes()))
.finish() .finish()
} }
} }
impl Node { impl Node {
#[cfg(test)]
pub(crate) fn new(repr: [u8; 32]) -> Self {
Node { repr }
}
/// Creates a tree leaf from the given Sapling note commitment. /// Creates a tree leaf from the given Sapling note commitment.
pub fn from_cmu(value: &ExtractedNoteCommitment) -> Self { pub fn from_cmu(value: &ExtractedNoteCommitment) -> Self {
Node { Node(value.inner())
repr: value.to_bytes(),
}
} }
/// Constructs a new note commitment tree node from a [`bls12_381::Scalar`] /// Constructs a new note commitment tree node from a [`bls12_381::Scalar`]
pub fn from_scalar(cmu: bls12_381::Scalar) -> Self { pub fn from_scalar(cmu: bls12_381::Scalar) -> Self {
Self { Self(cmu)
repr: cmu.to_repr(), }
}
/// Parses a tree leaf from the bytes of a Sapling note commitment.
///
/// Returns `None` if the provided bytes represent a non-canonical encoding.
pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
jubjub::Base::from_repr(bytes).map(Self)
}
/// Returns the canonical byte representation of this node.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_repr()
} }
} }
impl Hashable for Node { impl Hashable for Node {
fn empty_leaf() -> Self { fn empty_leaf() -> Self {
Node { Node(*UNCOMMITTED_SAPLING)
repr: UNCOMMITTED_SAPLING.to_repr(),
}
} }
fn combine(level: Level, lhs: &Self, rhs: &Self) -> Self { fn combine(level: Level, lhs: &Self, rhs: &Self) -> Self {
Node { Node(merkle_hash_field(
repr: merkle_hash(level.into(), &lhs.repr, &rhs.repr), level.into(),
} &lhs.0.to_bytes(),
&rhs.0.to_bytes(),
))
} }
fn empty_root(level: Level) -> Self { fn empty_root(level: Level) -> Self {
@ -118,22 +121,9 @@ impl Hashable for Node {
} }
} }
impl HashSer for Node {
fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let mut repr = [0u8; 32];
reader.read_exact(&mut repr)?;
Ok(Node { repr })
}
fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(self.repr.as_ref())
}
}
impl From<Node> for bls12_381::Scalar { impl From<Node> for bls12_381::Scalar {
fn from(node: Node) -> Self { fn from(node: Node) -> Self {
// Tree nodes should be in the prime field. node.0
bls12_381::Scalar::from_repr(node.repr).unwrap()
} }
} }
@ -142,12 +132,11 @@ pub(super) mod testing {
use proptest::prelude::*; use proptest::prelude::*;
use super::Node; use super::Node;
use crate::sapling::note::testing::arb_cmu;
prop_compose! { prop_compose! {
pub fn arb_node()(value in prop::array::uniform32(prop::num::u8::ANY)) -> Node { pub fn arb_node()(cmu in arb_cmu()) -> Node {
Node { Node::from_cmu(&cmu)
repr: value
}
} }
} }
} }