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)]
76pub struct OverflowError;
77
78impl fmt::Display for OverflowError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "Orchard value operation overflowed")
81 }
82}
83
84#[cfg(feature = "std")]
85impl std::error::Error for OverflowError {}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub struct NoteValue(u64);
90
91impl NoteValue {
92 pub(crate) fn zero() -> Self {
93 Default::default()
95 }
96
97 pub fn inner(&self) -> u64 {
99 self.0
100 }
101
102 pub fn from_raw(value: u64) -> Self {
107 NoteValue(value)
108 }
109
110 pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self {
111 NoteValue(u64::from_le_bytes(bytes))
112 }
113
114 pub(crate) fn to_bytes(self) -> [u8; 8] {
115 self.0.to_le_bytes()
116 }
117
118 pub(crate) fn to_le_bits(self) -> BitArray<[u8; 8], Lsb0> {
119 BitArray::<_, Lsb0>::new(self.0.to_le_bytes())
120 }
121}
122
123#[cfg(feature = "circuit")]
124impl From<&NoteValue> for Assigned<pallas::Base> {
125 fn from(v: &NoteValue) -> Self {
126 pallas::Base::from(v.inner()).into()
127 }
128}
129
130impl Sub for NoteValue {
131 type Output = ValueSum;
132
133 #[allow(clippy::suspicious_arithmetic_impl)]
134 fn sub(self, rhs: Self) -> Self::Output {
135 let a = self.0 as i128;
136 let b = rhs.0 as i128;
137 a.checked_sub(b)
138 .filter(|v| VALUE_SUM_RANGE.contains(v))
139 .map(ValueSum)
140 .expect("u64 - u64 result is always in VALUE_SUM_RANGE")
141 }
142}
143
144#[derive(Debug)]
146pub enum Sign {
147 Positive,
149 Negative,
151}
152
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155pub struct ValueSum(i128);
156
157impl ValueSum {
158 pub(crate) fn zero() -> Self {
159 Default::default()
161 }
162
163 pub(crate) fn from_raw(value: i64) -> Self {
170 ValueSum(value as i128)
171 }
172
173 pub(crate) fn from_magnitude_sign(magnitude: u64, sign: Sign) -> Self {
175 Self(match sign {
176 Sign::Positive => magnitude as i128,
177 Sign::Negative => -(magnitude as i128),
178 })
179 }
180
181 pub fn magnitude_sign(&self) -> (u64, Sign) {
190 let (magnitude, sign) = if self.0.is_negative() {
191 (-self.0, Sign::Negative)
192 } else {
193 (self.0, Sign::Positive)
194 };
195 (
196 u64::try_from(magnitude)
197 .expect("ValueSum magnitude is in range for u64 by construction"),
198 sign,
199 )
200 }
201}
202
203impl Add for ValueSum {
204 type Output = Option<ValueSum>;
205
206 #[allow(clippy::suspicious_arithmetic_impl)]
207 fn add(self, rhs: Self) -> Self::Output {
208 self.0
209 .checked_add(rhs.0)
210 .filter(|v| VALUE_SUM_RANGE.contains(v))
211 .map(ValueSum)
212 }
213}
214
215impl<'a> Sum<&'a ValueSum> for Result<ValueSum, OverflowError> {
216 fn sum<I: Iterator<Item = &'a ValueSum>>(mut iter: I) -> Self {
217 iter.try_fold(ValueSum(0), |acc, v| acc + *v)
218 .ok_or(OverflowError)
219 }
220}
221
222impl Sum<ValueSum> for Result<ValueSum, OverflowError> {
223 fn sum<I: Iterator<Item = ValueSum>>(mut iter: I) -> Self {
224 iter.try_fold(ValueSum(0), |acc, v| acc + v)
225 .ok_or(OverflowError)
226 }
227}
228
229impl TryFrom<ValueSum> for i64 {
230 type Error = OverflowError;
231
232 fn try_from(v: ValueSum) -> Result<i64, Self::Error> {
233 i64::try_from(v.0).map_err(|_| OverflowError)
234 }
235}
236
237#[derive(Clone, Debug)]
239pub struct ValueCommitTrapdoor(pallas::Scalar);
240
241impl ValueCommitTrapdoor {
242 pub(crate) fn inner(&self) -> pallas::Scalar {
243 self.0
244 }
245
246 pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
257 pallas::Scalar::from_repr(bytes).map(ValueCommitTrapdoor)
258 }
259
260 pub fn to_bytes(&self) -> [u8; 32] {
269 self.0.to_repr()
270 }
271}
272
273impl Add<&ValueCommitTrapdoor> for ValueCommitTrapdoor {
274 type Output = ValueCommitTrapdoor;
275
276 fn add(self, rhs: &Self) -> Self::Output {
277 ValueCommitTrapdoor(self.0 + rhs.0)
278 }
279}
280
281impl<'a> Sum<&'a ValueCommitTrapdoor> for ValueCommitTrapdoor {
282 fn sum<I: Iterator<Item = &'a ValueCommitTrapdoor>>(iter: I) -> Self {
283 iter.fold(ValueCommitTrapdoor::zero(), |acc, cv| acc + cv)
284 }
285}
286
287impl ValueCommitTrapdoor {
288 pub(crate) fn random(rng: impl RngCore) -> Self {
290 ValueCommitTrapdoor(pallas::Scalar::random(rng))
291 }
292
293 pub(crate) fn zero() -> Self {
295 ValueCommitTrapdoor(pallas::Scalar::zero())
296 }
297
298 pub(crate) fn into_bsk(self) -> redpallas::SigningKey<Binding> {
299 self.0.to_repr().try_into().unwrap()
301 }
302}
303
304#[derive(Clone, Debug)]
306pub struct ValueCommitment(pallas::Point);
307
308impl Add<&ValueCommitment> for ValueCommitment {
309 type Output = ValueCommitment;
310
311 fn add(self, rhs: &Self) -> Self::Output {
312 ValueCommitment(self.0 + rhs.0)
313 }
314}
315
316impl Sub for ValueCommitment {
317 type Output = ValueCommitment;
318
319 fn sub(self, rhs: Self) -> Self::Output {
320 ValueCommitment(self.0 - rhs.0)
321 }
322}
323
324impl Sum for ValueCommitment {
325 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
326 iter.fold(ValueCommitment(pallas::Point::identity()), |acc, cv| {
327 acc + &cv
328 })
329 }
330}
331
332impl<'a> Sum<&'a ValueCommitment> for ValueCommitment {
333 fn sum<I: Iterator<Item = &'a ValueCommitment>>(iter: I) -> Self {
334 iter.fold(ValueCommitment(pallas::Point::identity()), |acc, cv| {
335 acc + cv
336 })
337 }
338}
339
340impl ValueCommitment {
341 #[allow(non_snake_case)]
347 pub fn derive(value: ValueSum, rcv: ValueCommitTrapdoor) -> Self {
348 let hasher = pallas::Point::hash_to_curve(VALUE_COMMITMENT_PERSONALIZATION);
349 let V = hasher(&VALUE_COMMITMENT_V_BYTES);
350 let R = hasher(&VALUE_COMMITMENT_R_BYTES);
351 let abs_value = u64::try_from(value.0.abs()).expect("value must be in valid range");
352
353 let value = if value.0.is_negative() {
354 -pallas::Scalar::from(abs_value)
355 } else {
356 pallas::Scalar::from(abs_value)
357 };
358
359 ValueCommitment(V * value + R * rcv.0)
360 }
361
362 pub(crate) fn into_bvk(self) -> redpallas::VerificationKey<Binding> {
363 self.0.to_bytes().try_into().unwrap()
365 }
366
367 pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<ValueCommitment> {
369 pallas::Point::from_bytes(bytes).map(ValueCommitment)
370 }
371
372 pub fn to_bytes(&self) -> [u8; 32] {
374 self.0.to_bytes()
375 }
376
377 pub(crate) fn x(&self) -> pallas::Base {
379 if self.0 == pallas::Point::identity() {
380 pallas::Base::zero()
381 } else {
382 *self.0.to_affine().coordinates().unwrap().x()
383 }
384 }
385
386 pub(crate) fn y(&self) -> pallas::Base {
388 if self.0 == pallas::Point::identity() {
389 pallas::Base::zero()
390 } else {
391 *self.0.to_affine().coordinates().unwrap().y()
392 }
393 }
394}
395
396#[cfg(any(test, feature = "test-dependencies"))]
398#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
399pub mod testing {
400 use group::ff::FromUniformBytes;
401 use pasta_curves::pallas;
402 use proptest::prelude::*;
403
404 use super::{NoteValue, ValueCommitTrapdoor, ValueSum, MAX_NOTE_VALUE, VALUE_SUM_RANGE};
405
406 prop_compose! {
407 pub fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar {
409 let mut buf = [0; 64];
411 buf[..32].copy_from_slice(&bytes);
412 pallas::Scalar::from_uniform_bytes(&buf)
413 }
414 }
415
416 prop_compose! {
417 pub fn arb_value_sum()(value in VALUE_SUM_RANGE) -> ValueSum {
419 ValueSum(value)
420 }
421 }
422
423 prop_compose! {
424 pub fn arb_value_sum_bounded(bound: NoteValue)(value in -(bound.0 as i128)..=(bound.0 as i128)) -> ValueSum {
426 ValueSum(value)
427 }
428 }
429
430 prop_compose! {
431 pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor {
433 ValueCommitTrapdoor(rcv)
434 }
435 }
436
437 prop_compose! {
438 pub fn arb_note_value()(value in 0u64..MAX_NOTE_VALUE) -> NoteValue {
440 NoteValue(value)
441 }
442 }
443
444 prop_compose! {
445 pub fn arb_note_value_bounded(max: u64)(value in 0u64..max) -> NoteValue {
448 NoteValue(value)
449 }
450 }
451
452 prop_compose! {
453 pub fn arb_positive_note_value(max: u64)(value in 1u64..max) -> NoteValue {
456 NoteValue(value)
457 }
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use proptest::prelude::*;
464
465 use super::{
466 testing::{arb_note_value_bounded, arb_trapdoor, arb_value_sum_bounded},
467 OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum, MAX_NOTE_VALUE,
468 };
469 use crate::primitives::redpallas;
470
471 proptest! {
472 #[test]
473 fn bsk_consistent_with_bvk(
474 values in (1usize..10).prop_flat_map(|n_values|
475 arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound|
476 prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor()), n_values)
477 )
478 )
479 ) {
480 let value_balance = values
481 .iter()
482 .map(|(value, _)| value)
483 .sum::<Result<ValueSum, OverflowError>>()
484 .expect("we generate values that won't overflow");
485
486 let bsk = values
487 .iter()
488 .map(|(_, rcv)| rcv)
489 .sum::<ValueCommitTrapdoor>()
490 .into_bsk();
491
492 let bvk = (values
493 .into_iter()
494 .map(|(value, rcv)| ValueCommitment::derive(value, rcv))
495 .sum::<ValueCommitment>()
496 - ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero()))
497 .into_bvk();
498
499 assert_eq!(redpallas::VerificationKey::from(&bsk), bvk);
500 }
501 }
502}