sapling-crypto/src/value.rs

268 lines
10 KiB
Rust

//! Monetary values within the Sapling shielded pool.
//!
//! Values are represented in three places within the Sapling protocol:
//! - [`NoteValue`], the value of an individual note. It is an unsigned 64-bit integer
//! (with maximum value [`MAX_NOTE_VALUE`]), and is serialized in a note plaintext.
//! - [`ValueSum`], the sum of note values within a Sapling [`Bundle`]. It is represented
//! as an `i128` and places an upper bound on the maximum number of notes within a
//! single [`Bundle`].
//! - `valueBalanceSapling`, which is a signed 63-bit integer. This is represented
//! by a user-defined type parameter on [`Bundle`], returned by
//! [`Bundle::value_balance`] and [`Builder::value_balance`].
//!
//! If your specific instantiation of the Sapling protocol requires a smaller bound on
//! valid note values (for example, Zcash's `MAX_MONEY` fits into a 51-bit integer), you
//! should enforce this in two ways:
//!
//! - Define your `valueBalanceSapling` type to enforce your valid value range. This can
//! be checked in its `TryFrom<i64>` implementation.
//! - Define your own "amount" type for note values, and convert it to `NoteValue` prior
//! to calling [`Builder::add_output`].
//!
//! Inside the circuit, note values are constrained to be unsigned 64-bit integers.
//!
//! # Caution!
//!
//! An `i64` is _not_ a signed 64-bit integer! The [Rust documentation] calls `i64` the
//! 64-bit signed integer type, which is true in the sense that its encoding in memory
//! takes up 64 bits. Numerically, however, `i64` is a signed 63-bit integer.
//!
//! Fortunately, users of this crate should never need to construct [`ValueSum`] directly;
//! you should only need to interact with [`NoteValue`] (which can be safely constructed
//! from a `u64`) and `valueBalanceSapling` (which can be represented as an `i64`).
//!
//! [`Bundle`]: crate::Bundle
//! [`Bundle::value_balance`]: crate::Bundle::value_balance
//! [`Builder::value_balance`]: crate::builder::Builder::value_balance
//! [`Builder::add_output`]: crate::builder::Builder::add_output
//! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html
use bitvec::{array::BitArray, order::Lsb0};
use ff::{Field, PrimeField};
use group::GroupEncoding;
use rand::RngCore;
use subtle::CtOption;
use super::constants::{VALUE_COMMITMENT_RANDOMNESS_GENERATOR, VALUE_COMMITMENT_VALUE_GENERATOR};
mod sums;
pub use sums::{CommitmentSum, OverflowError, TrapdoorSum, ValueSum};
/// Maximum note value.
pub const MAX_NOTE_VALUE: u64 = u64::MAX;
/// The non-negative value of an individual Sapling note.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NoteValue(u64);
impl NoteValue {
/// The zero note value.
pub const ZERO: NoteValue = NoteValue(0);
/// Returns the raw underlying value.
pub fn inner(&self) -> u64 {
self.0
}
/// Creates a note value from its raw numeric value.
///
/// This only enforces that the value is an unsigned 64-bit integer. Callers should
/// enforce any additional constraints on the value's valid range themselves.
pub fn from_raw(value: u64) -> Self {
NoteValue(value)
}
pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self {
NoteValue(u64::from_le_bytes(bytes))
}
pub(crate) fn to_le_bits(self) -> BitArray<[u8; 8], Lsb0> {
BitArray::<_, Lsb0>::new(self.0.to_le_bytes())
}
}
/// The blinding factor for a [`ValueCommitment`].
#[derive(Clone, Debug)]
pub struct ValueCommitTrapdoor(jubjub::Scalar);
impl ValueCommitTrapdoor {
/// Generates a new value commitment trapdoor.
///
/// This is public for access by `zcash_proofs`.
pub fn random(rng: impl RngCore) -> Self {
ValueCommitTrapdoor(jubjub::Scalar::random(rng))
}
/// Constructs `ValueCommitTrapdoor` from the byte representation of a scalar.
///
/// Returns a `None` [`CtOption`] if `bytes` is not a canonical representation of a
/// Jubjub scalar.
///
/// This is a low-level API, requiring a detailed understanding of the
/// [use of value commitment trapdoors][saplingbalance] in the Zcash protocol
/// to use correctly and securely. It is intended to be used in combination
/// with [`ValueCommitment::derive`].
///
/// [saplingbalance]: https://zips.z.cash/protocol/protocol.pdf#saplingbalance
pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
jubjub::Scalar::from_repr(bytes).map(ValueCommitTrapdoor)
}
/// Returns the inner Jubjub scalar representing this trapdoor.
///
/// This is public for access by `zcash_proofs`.
pub fn inner(&self) -> jubjub::Scalar {
self.0
}
}
/// A commitment to a [`ValueSum`].
///
/// # Consensus rules
///
/// The Zcash Protocol Spec requires Sapling Spend Descriptions and Output Descriptions to
/// not contain a small order `ValueCommitment`. However, the `ValueCommitment` type as
/// specified (and implemented here) may contain a small order point. In practice, it will
/// not occur:
/// - [`ValueCommitment::derive`] will only produce a small order point if both the given
/// [`NoteValue`] and [`ValueCommitTrapdoor`] are zero. However, the only constructor
/// available for `ValueCommitTrapdoor` is [`ValueCommitTrapdoor::random`], which will
/// produce zero with negligible probability (assuming a non-broken PRNG).
/// - [`ValueCommitment::from_bytes_not_small_order`] enforces this by definition, and is
/// the only constructor that can be used with data received over the network.
#[derive(Clone, Debug)]
pub struct ValueCommitment(jubjub::ExtendedPoint);
impl ValueCommitment {
/// Derives a `ValueCommitment` by $\mathsf{ValueCommit^{Sapling}}$.
///
/// Defined in [Zcash Protocol Spec § 5.4.8.3: Homomorphic Pedersen commitments (Sapling and Orchard)][concretehomomorphiccommit].
///
/// [concretehomomorphiccommit]: https://zips.z.cash/protocol/protocol.pdf#concretehomomorphiccommit
pub fn derive(value: NoteValue, rcv: ValueCommitTrapdoor) -> Self {
let cv = (VALUE_COMMITMENT_VALUE_GENERATOR * jubjub::Scalar::from(value.0))
+ (VALUE_COMMITMENT_RANDOMNESS_GENERATOR * rcv.0);
ValueCommitment(cv.into())
}
/// Returns the inner Jubjub point representing this value commitment.
///
/// This is public for access by `zcash_proofs`.
pub fn as_inner(&self) -> &jubjub::ExtendedPoint {
&self.0
}
/// Deserializes a value commitment from its byte representation.
///
/// Returns `None` if `bytes` is an invalid representation of a Jubjub point, or the
/// resulting point is of small order.
///
/// This method can be used to enforce the "not small order" consensus rules defined
/// in [Zcash Protocol Spec § 4.4: Spend Descriptions][spenddesc] and
/// [§ 4.5: Output Descriptions][outputdesc].
///
/// [spenddesc]: https://zips.z.cash/protocol/protocol.pdf#spenddesc
/// [outputdesc]: https://zips.z.cash/protocol/protocol.pdf#outputdesc
pub fn from_bytes_not_small_order(bytes: &[u8; 32]) -> CtOption<ValueCommitment> {
jubjub::ExtendedPoint::from_bytes(bytes)
.and_then(|cv| CtOption::new(ValueCommitment(cv), !cv.is_small_order()))
}
/// Serializes this value commitment to its canonical byte representation.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use proptest::prelude::*;
use super::{NoteValue, ValueCommitTrapdoor, MAX_NOTE_VALUE};
prop_compose! {
/// Generate an arbitrary value in the range of valid nonnegative amounts.
pub fn arb_note_value()(value in 0u64..MAX_NOTE_VALUE) -> NoteValue {
NoteValue(value)
}
}
prop_compose! {
/// Generate an arbitrary value in the range of valid positive amounts less than a
/// specified value.
pub fn arb_note_value_bounded(max: u64)(value in 0u64..max) -> NoteValue {
NoteValue(value)
}
}
prop_compose! {
/// Generate an arbitrary value in the range of valid positive amounts less than a
/// specified value.
pub fn arb_positive_note_value(max: u64)(value in 1u64..max) -> NoteValue {
NoteValue(value)
}
}
prop_compose! {
/// Generate an arbitrary Jubjub scalar.
fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> jubjub::Scalar {
// Instead of rejecting out-of-range bytes, let's reduce them.
let mut buf = [0; 64];
buf[..32].copy_from_slice(&bytes);
jubjub::Scalar::from_bytes_wide(&buf)
}
}
prop_compose! {
/// Generate an arbitrary ValueCommitTrapdoor
pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor {
ValueCommitTrapdoor(rcv)
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::{
testing::{arb_note_value_bounded, arb_trapdoor},
CommitmentSum, OverflowError, TrapdoorSum, ValueCommitment, ValueSum,
};
proptest! {
#[test]
fn bsk_consistent_with_bvk(
values in (1usize..10).prop_flat_map(|n_values| prop::collection::vec(
(arb_note_value_bounded((i64::MAX as u64) / (n_values as u64)), arb_trapdoor()),
n_values,
))
) {
let value_balance: i64 = values
.iter()
.map(|(value, _)| value)
.sum::<Result<ValueSum, OverflowError>>()
.expect("we generate values that won't overflow")
.try_into()
.unwrap();
let bsk = values
.iter()
.map(|(_, rcv)| rcv)
.sum::<TrapdoorSum>()
.into_bsk();
let bvk = values
.into_iter()
.map(|(value, rcv)| ValueCommitment::derive(value, rcv))
.sum::<CommitmentSum>()
.into_bvk(value_balance);
assert_eq!(redjubjub::VerificationKey::from(&bsk), bvk);
}
}
}