Skip to main content

orchard/
value.rs

1//! Monetary values within the Orchard shielded pool.
2//!
3//! Values are represented in three places within the Orchard protocol:
4//! - [`NoteValue`], the value of an individual note. It is an unsigned 64-bit integer
5//!   (with maximum value [`MAX_NOTE_VALUE`]), and is serialized in a note plaintext.
6//! - [`ValueSum`], the sum of note values within an Orchard [`Action`] or [`Bundle`].
7//!   It is a signed 64-bit integer (with range [`VALUE_SUM_RANGE`]).
8//! - `valueBalanceOrchard`, which is a signed 63-bit integer. This is represented
9//!   by a user-defined type parameter on [`Bundle`], returned by
10//!   [`Bundle::value_balance`] and [`Builder::value_balance`].
11//!
12//! If your specific instantiation of the Orchard protocol requires a smaller bound on
13//! valid note values (for example, Zcash's `MAX_MONEY` fits into a 51-bit integer), you
14//! should enforce this in two ways:
15//!
16//! - Define your `valueBalanceOrchard` type to enforce your valid value range. This can
17//!   be checked in its `TryFrom<i64>` implementation.
18//! - Define your own "amount" type for note values, and convert it to `NoteValue` prior
19//!   to calling [`Builder::add_output`].
20//!
21//! Inside the circuit, note values are constrained to be unsigned 64-bit integers.
22//!
23//! # Caution!
24//!
25//! An `i64` is _not_ a signed 64-bit integer! The [Rust documentation] calls `i64` the
26//! 64-bit signed integer type, which is true in the sense that its encoding in memory
27//! takes up 64 bits. Numerically, however, `i64` is a signed 63-bit integer.
28//!
29//! Fortunately, users of this crate should never need to construct [`ValueSum`] directly;
30//! you should only need to interact with [`NoteValue`] (which can be safely constructed
31//! from a `u64`) and `valueBalanceOrchard` (which can be represented as an `i64`).
32//!
33//! [`Action`]: crate::action::Action
34//! [`Bundle`]: crate::bundle::Bundle
35//! [`Bundle::value_balance`]: crate::bundle::Bundle::value_balance
36//! [`Builder::value_balance`]: crate::builder::Builder::value_balance
37//! [`Builder::add_output`]: crate::builder::Builder::add_output
38//! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html
39
40use core::fmt::{self, Debug};
41use core::iter::Sum;
42use core::ops::{Add, RangeInclusive, Sub};
43
44use bitvec::{array::BitArray, order::Lsb0};
45use ff::{Field, PrimeField};
46use group::{Curve, Group, GroupEncoding};
47#[cfg(feature = "circuit")]
48use halo2_proofs::plonk::Assigned;
49use pasta_curves::{
50    arithmetic::{CurveAffine, CurveExt},
51    pallas,
52};
53use rand::RngCore;
54use subtle::CtOption;
55
56use crate::{
57    constants::fixed_bases::{
58        VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_R_BYTES, VALUE_COMMITMENT_V_BYTES,
59    },
60    primitives::redpallas::{self, Binding},
61};
62
63/// Maximum note value.
64pub const MAX_NOTE_VALUE: u64 = u64::MAX;
65
66/// The valid range of the scalar multiplication used in ValueCommit^Orchard.
67///
68/// Defined in a note in [Zcash Protocol Spec § 4.17.4: Action Statement (Orchard)][actionstatement].
69///
70/// [actionstatement]: https://zips.z.cash/protocol/nu5.pdf#actionstatement
71pub const VALUE_SUM_RANGE: RangeInclusive<i128> =
72    -(MAX_NOTE_VALUE as i128)..=MAX_NOTE_VALUE as i128;
73
74/// A type for balance violations in amount addition and subtraction
75/// (overflow and underflow of allowed ranges).
76#[derive(Debug)]
77#[non_exhaustive]
78pub enum BalanceError {
79    /// Two values were added or subtracted, and the result overflowed the valid range for
80    /// the value.
81    ///
82    /// Normally this range is [`VALUE_SUM_RANGE`], but when interacting with value
83    /// balances it may be `i64`.
84    Overflow,
85}
86
87impl fmt::Display for BalanceError {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Self::Overflow => write!(f, "Orchard value operation overflowed"),
91        }
92    }
93}
94
95#[cfg(feature = "std")]
96impl std::error::Error for BalanceError {}
97
98/// The non-negative value of an individual Orchard note.
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub struct NoteValue(u64);
101
102impl NoteValue {
103    /// Returns a zero note value.
104    pub fn zero() -> Self {
105        // Default for u64 is zero.
106        Default::default()
107    }
108
109    /// Returns the raw underlying value.
110    pub fn inner(&self) -> u64 {
111        self.0
112    }
113
114    /// Creates a note value from its raw numeric value.
115    ///
116    /// This only enforces that the value is an unsigned 64-bit integer. Callers should
117    /// enforce any additional constraints on the value's valid range themselves.
118    pub fn from_raw(value: u64) -> Self {
119        NoteValue(value)
120    }
121
122    pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self {
123        NoteValue(u64::from_le_bytes(bytes))
124    }
125
126    pub(crate) fn to_bytes(self) -> [u8; 8] {
127        self.0.to_le_bytes()
128    }
129
130    pub(crate) fn to_le_bits(self) -> BitArray<[u8; 8], Lsb0> {
131        BitArray::<_, Lsb0>::new(self.0.to_le_bytes())
132    }
133}
134
135#[cfg(feature = "circuit")]
136impl From<&NoteValue> for Assigned<pallas::Base> {
137    fn from(v: &NoteValue) -> Self {
138        pallas::Base::from(v.inner()).into()
139    }
140}
141
142impl Sub for NoteValue {
143    type Output = ValueSum;
144
145    #[allow(clippy::suspicious_arithmetic_impl)]
146    fn sub(self, rhs: Self) -> Self::Output {
147        let a = self.0 as i128;
148        let b = rhs.0 as i128;
149        a.checked_sub(b)
150            .filter(|v| VALUE_SUM_RANGE.contains(v))
151            .map(ValueSum)
152            .expect("u64 - u64 result is always in VALUE_SUM_RANGE")
153    }
154}
155
156/// The sign of a [`ValueSum`].
157#[derive(Debug)]
158pub enum Sign {
159    /// A non-negative [`ValueSum`].
160    Positive,
161    /// A negative [`ValueSum`].
162    Negative,
163}
164
165/// A sum of Orchard note values.
166#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
167pub struct ValueSum(i128);
168
169impl ValueSum {
170    pub(crate) fn zero() -> Self {
171        // Default for i128 is zero.
172        Default::default()
173    }
174
175    /// Creates a value sum from a raw i64 (which is always in range for this type).
176    ///
177    /// This only enforces that the value is a signed 63-bit integer. We use it internally
178    /// in `Bundle::binding_validating_key`, where we are converting from the user-defined
179    /// `valueBalance` type that enforces any additional constraints on the value's valid
180    /// range.
181    pub(crate) fn from_raw(value: i64) -> Self {
182        ValueSum(value as i128)
183    }
184
185    /// Constructs a value sum from its magnitude and sign.
186    pub(crate) fn from_magnitude_sign(magnitude: u64, sign: Sign) -> Self {
187        Self(match sign {
188            Sign::Positive => magnitude as i128,
189            Sign::Negative => -(magnitude as i128),
190        })
191    }
192
193    /// Splits this value sum into its magnitude and sign.
194    ///
195    /// This is a low-level API, requiring a detailed understanding of the
196    /// [use of value balancing][orchardbalance] in the Zcash protocol to use correctly
197    /// and securely. It is intended to be used in combination with the [`crate::pczt`]
198    /// module.
199    ///
200    /// [orchardbalance]: https://zips.z.cash/protocol/protocol.pdf#orchardbalance
201    pub fn magnitude_sign(&self) -> (u64, Sign) {
202        let (magnitude, sign) = if self.0.is_negative() {
203            (-self.0, Sign::Negative)
204        } else {
205            (self.0, Sign::Positive)
206        };
207        (
208            u64::try_from(magnitude)
209                .expect("ValueSum magnitude is in range for u64 by construction"),
210            sign,
211        )
212    }
213}
214
215impl Add for ValueSum {
216    type Output = Option<ValueSum>;
217
218    #[allow(clippy::suspicious_arithmetic_impl)]
219    fn add(self, rhs: Self) -> Self::Output {
220        self.0
221            .checked_add(rhs.0)
222            .filter(|v| VALUE_SUM_RANGE.contains(v))
223            .map(ValueSum)
224    }
225}
226
227impl<'a> Sum<&'a ValueSum> for Result<ValueSum, BalanceError> {
228    fn sum<I: Iterator<Item = &'a ValueSum>>(mut iter: I) -> Self {
229        iter.try_fold(ValueSum(0), |acc, v| acc + *v)
230            .ok_or(BalanceError::Overflow)
231    }
232}
233
234impl Sum<ValueSum> for Result<ValueSum, BalanceError> {
235    fn sum<I: Iterator<Item = ValueSum>>(mut iter: I) -> Self {
236        iter.try_fold(ValueSum(0), |acc, v| acc + v)
237            .ok_or(BalanceError::Overflow)
238    }
239}
240
241impl TryFrom<ValueSum> for i64 {
242    type Error = BalanceError;
243
244    fn try_from(v: ValueSum) -> Result<i64, Self::Error> {
245        i64::try_from(v.0).map_err(|_| BalanceError::Overflow)
246    }
247}
248
249/// The blinding factor for a [`ValueCommitment`].
250#[derive(Clone, Debug)]
251pub struct ValueCommitTrapdoor(pallas::Scalar);
252
253impl ValueCommitTrapdoor {
254    pub(crate) fn inner(&self) -> pallas::Scalar {
255        self.0
256    }
257
258    /// Constructs `ValueCommitTrapdoor` from the byte representation of a scalar.
259    /// Returns a `None` [`CtOption`] if `bytes` is not a canonical representation
260    /// of a Pallas scalar.
261    ///
262    /// This is a low-level API, requiring a detailed understanding of the
263    /// [use of value commitment trapdoors][orchardbalance] in the Zcash protocol
264    /// to use correctly and securely. It is intended to be used in combination
265    /// with [`ValueCommitment::derive`].
266    ///
267    /// [orchardbalance]: https://zips.z.cash/protocol/protocol.pdf#orchardbalance
268    pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
269        pallas::Scalar::from_repr(bytes).map(ValueCommitTrapdoor)
270    }
271
272    /// Returns the byte encoding of a `ValueCommitTrapdoor`.
273    ///
274    /// This is a low-level API, requiring a detailed understanding of the
275    /// [use of value commitment trapdoors][orchardbalance] in the Zcash protocol
276    /// to use correctly and securely. It is intended to be used in combination
277    /// with the [`crate::pczt`] module.
278    ///
279    /// [orchardbalance]: https://zips.z.cash/protocol/protocol.pdf#orchardbalance
280    pub fn to_bytes(&self) -> [u8; 32] {
281        self.0.to_repr()
282    }
283}
284
285impl Add<&ValueCommitTrapdoor> for ValueCommitTrapdoor {
286    type Output = ValueCommitTrapdoor;
287
288    fn add(self, rhs: &Self) -> Self::Output {
289        ValueCommitTrapdoor(self.0 + rhs.0)
290    }
291}
292
293impl<'a> Sum<&'a ValueCommitTrapdoor> for ValueCommitTrapdoor {
294    fn sum<I: Iterator<Item = &'a ValueCommitTrapdoor>>(iter: I) -> Self {
295        iter.fold(ValueCommitTrapdoor::zero(), |acc, cv| acc + cv)
296    }
297}
298
299impl ValueCommitTrapdoor {
300    /// Generates a new value commitment trapdoor.
301    pub(crate) fn random(rng: impl RngCore) -> Self {
302        ValueCommitTrapdoor(pallas::Scalar::random(rng))
303    }
304
305    /// Returns the zero trapdoor, which provides no blinding.
306    pub(crate) fn zero() -> Self {
307        ValueCommitTrapdoor(pallas::Scalar::zero())
308    }
309
310    pub(crate) fn into_bsk(self) -> redpallas::SigningKey<Binding> {
311        // TODO: impl From<pallas::Scalar> for redpallas::SigningKey.
312        self.0.to_repr().try_into().unwrap()
313    }
314}
315
316/// A commitment to a [`ValueSum`].
317#[derive(Clone, Debug)]
318pub struct ValueCommitment(pallas::Point);
319
320impl Add<&ValueCommitment> for ValueCommitment {
321    type Output = ValueCommitment;
322
323    fn add(self, rhs: &Self) -> Self::Output {
324        ValueCommitment(self.0 + rhs.0)
325    }
326}
327
328impl Sub for ValueCommitment {
329    type Output = ValueCommitment;
330
331    fn sub(self, rhs: Self) -> Self::Output {
332        ValueCommitment(self.0 - rhs.0)
333    }
334}
335
336impl Sum for ValueCommitment {
337    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
338        iter.fold(ValueCommitment(pallas::Point::identity()), |acc, cv| {
339            acc + &cv
340        })
341    }
342}
343
344impl<'a> Sum<&'a ValueCommitment> for ValueCommitment {
345    fn sum<I: Iterator<Item = &'a ValueCommitment>>(iter: I) -> Self {
346        iter.fold(ValueCommitment(pallas::Point::identity()), |acc, cv| {
347            acc + cv
348        })
349    }
350}
351
352impl ValueCommitment {
353    /// Derives a `ValueCommitment` by $\mathsf{ValueCommit^{Orchard}}$.
354    ///
355    /// Defined in [Zcash Protocol Spec § 5.4.8.3: Homomorphic Pedersen commitments (Sapling and Orchard)][concretehomomorphiccommit].
356    ///
357    /// [concretehomomorphiccommit]: https://zips.z.cash/protocol/nu5.pdf#concretehomomorphiccommit
358    #[allow(non_snake_case)]
359    pub fn derive(value: ValueSum, rcv: ValueCommitTrapdoor) -> Self {
360        let hasher = pallas::Point::hash_to_curve(VALUE_COMMITMENT_PERSONALIZATION);
361        let V = hasher(&VALUE_COMMITMENT_V_BYTES);
362        let R = hasher(&VALUE_COMMITMENT_R_BYTES);
363        let abs_value = u64::try_from(value.0.abs()).expect("value must be in valid range");
364
365        let value = if value.0.is_negative() {
366            -pallas::Scalar::from(abs_value)
367        } else {
368            pallas::Scalar::from(abs_value)
369        };
370
371        ValueCommitment(V * value + R * rcv.0)
372    }
373
374    pub(crate) fn into_bvk(self) -> redpallas::VerificationKey<Binding> {
375        // TODO: impl From<pallas::Point> for redpallas::VerificationKey.
376        self.0.to_bytes().try_into().unwrap()
377    }
378
379    /// Deserialize a value commitment from its byte representation
380    pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<ValueCommitment> {
381        pallas::Point::from_bytes(bytes).map(ValueCommitment)
382    }
383
384    /// Serialize this value commitment to its canonical byte representation.
385    pub fn to_bytes(&self) -> [u8; 32] {
386        self.0.to_bytes()
387    }
388
389    /// x-coordinate of this value commitment.
390    pub(crate) fn x(&self) -> pallas::Base {
391        if self.0 == pallas::Point::identity() {
392            pallas::Base::zero()
393        } else {
394            *self.0.to_affine().coordinates().unwrap().x()
395        }
396    }
397
398    /// y-coordinate of this value commitment.
399    pub(crate) fn y(&self) -> pallas::Base {
400        if self.0 == pallas::Point::identity() {
401            pallas::Base::zero()
402        } else {
403            *self.0.to_affine().coordinates().unwrap().y()
404        }
405    }
406}
407
408/// Generators for property testing.
409#[cfg(any(test, feature = "test-dependencies"))]
410#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
411pub mod testing {
412    use group::ff::FromUniformBytes;
413    use pasta_curves::pallas;
414    use proptest::prelude::*;
415
416    use super::{NoteValue, ValueCommitTrapdoor, ValueSum, MAX_NOTE_VALUE, VALUE_SUM_RANGE};
417
418    prop_compose! {
419        /// Generate an arbitrary Pallas scalar.
420        pub fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar {
421            // Instead of rejecting out-of-range bytes, let's reduce them.
422            let mut buf = [0; 64];
423            buf[..32].copy_from_slice(&bytes);
424            pallas::Scalar::from_uniform_bytes(&buf)
425        }
426    }
427
428    prop_compose! {
429        /// Generate an arbitrary [`ValueSum`] in the range of valid Zcash values.
430        pub fn arb_value_sum()(value in VALUE_SUM_RANGE) -> ValueSum {
431            ValueSum(value)
432        }
433    }
434
435    prop_compose! {
436        /// Generate an arbitrary [`ValueSum`] in the range of valid Zcash values.
437        pub fn arb_value_sum_bounded(bound: NoteValue)(value in -(bound.0 as i128)..=(bound.0 as i128)) -> ValueSum {
438            ValueSum(value)
439        }
440    }
441
442    prop_compose! {
443        /// Generate an arbitrary ValueCommitTrapdoor
444        pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor {
445            ValueCommitTrapdoor(rcv)
446        }
447    }
448
449    prop_compose! {
450        /// Generate an arbitrary value in the range of valid nonnegative Zcash amounts.
451        pub fn arb_note_value()(value in 0u64..MAX_NOTE_VALUE) -> NoteValue {
452            NoteValue(value)
453        }
454    }
455
456    prop_compose! {
457        /// Generate an arbitrary value in the range of valid positive Zcash amounts
458        /// less than a specified value.
459        pub fn arb_note_value_bounded(max: u64)(value in 0u64..max) -> NoteValue {
460            NoteValue(value)
461        }
462    }
463
464    prop_compose! {
465        /// Generate an arbitrary value in the range of valid positive Zcash amounts
466        /// less than a specified value.
467        pub fn arb_positive_note_value(max: u64)(value in 1u64..max) -> NoteValue {
468            NoteValue(value)
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use proptest::prelude::*;
476
477    use super::{
478        testing::{arb_note_value_bounded, arb_trapdoor, arb_value_sum_bounded},
479        BalanceError, ValueCommitTrapdoor, ValueCommitment, ValueSum, MAX_NOTE_VALUE,
480    };
481    use crate::primitives::redpallas;
482
483    proptest! {
484        #[test]
485        fn bsk_consistent_with_bvk(
486            values in (1usize..10).prop_flat_map(|n_values|
487                arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound|
488                    prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor()), n_values)
489                )
490            )
491        ) {
492            let value_balance = values
493                .iter()
494                .map(|(value, _)| value)
495                .sum::<Result<ValueSum, BalanceError>>()
496                .expect("we generate values that won't overflow");
497
498            let bsk = values
499                .iter()
500                .map(|(_, rcv)| rcv)
501                .sum::<ValueCommitTrapdoor>()
502                .into_bsk();
503
504            let bvk = (values
505                .into_iter()
506                .map(|(value, rcv)| ValueCommitment::derive(value, rcv))
507                .sum::<ValueCommitment>()
508                - ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero()))
509            .into_bvk();
510
511            assert_eq!(redpallas::VerificationKey::from(&bsk), bvk);
512        }
513    }
514}