1use 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
63pub const MAX_NOTE_VALUE: u64 = u64::MAX;
65
66pub const VALUE_SUM_RANGE: RangeInclusive<i128> =
72 -(MAX_NOTE_VALUE as i128)..=MAX_NOTE_VALUE as i128;
73
74#[derive(Debug)]
77#[non_exhaustive]
78pub enum BalanceError {
79 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub struct NoteValue(u64);
101
102impl NoteValue {
103 pub fn zero() -> Self {
105 Default::default()
107 }
108
109 pub fn inner(&self) -> u64 {
111 self.0
112 }
113
114 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#[derive(Debug)]
158pub enum Sign {
159 Positive,
161 Negative,
163}
164
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
167pub struct ValueSum(i128);
168
169impl ValueSum {
170 pub(crate) fn zero() -> Self {
171 Default::default()
173 }
174
175 pub(crate) fn from_raw(value: i64) -> Self {
182 ValueSum(value as i128)
183 }
184
185 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 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#[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 pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
269 pallas::Scalar::from_repr(bytes).map(ValueCommitTrapdoor)
270 }
271
272 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 pub(crate) fn random(rng: impl RngCore) -> Self {
302 ValueCommitTrapdoor(pallas::Scalar::random(rng))
303 }
304
305 pub(crate) fn zero() -> Self {
307 ValueCommitTrapdoor(pallas::Scalar::zero())
308 }
309
310 pub(crate) fn into_bsk(self) -> redpallas::SigningKey<Binding> {
311 self.0.to_repr().try_into().unwrap()
313 }
314}
315
316#[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 #[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 self.0.to_bytes().try_into().unwrap()
377 }
378
379 pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<ValueCommitment> {
381 pallas::Point::from_bytes(bytes).map(ValueCommitment)
382 }
383
384 pub fn to_bytes(&self) -> [u8; 32] {
386 self.0.to_bytes()
387 }
388
389 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 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#[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 pub fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar {
421 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 pub fn arb_value_sum()(value in VALUE_SUM_RANGE) -> ValueSum {
431 ValueSum(value)
432 }
433 }
434
435 prop_compose! {
436 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 pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor {
445 ValueCommitTrapdoor(rcv)
446 }
447 }
448
449 prop_compose! {
450 pub fn arb_note_value()(value in 0u64..MAX_NOTE_VALUE) -> NoteValue {
452 NoteValue(value)
453 }
454 }
455
456 prop_compose! {
457 pub fn arb_note_value_bounded(max: u64)(value in 0u64..max) -> NoteValue {
460 NoteValue(value)
461 }
462 }
463
464 prop_compose! {
465 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}