use ark_r1cs_std::prelude::*;
use ark_r1cs_std::uint8::UInt8;
use ark_relations::r1cs::SynthesisError;
use penumbra_num::{Amount, AmountVar};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::{
collections::{btree_map, BTreeMap},
fmt::{self, Debug, Formatter},
iter::FusedIterator,
mem,
num::NonZeroU128,
ops::{Add, AddAssign, Deref, Neg, Sub, SubAssign},
};
use crate::{
asset::{AssetIdVar, Id},
value::ValueVar,
Value,
};
pub mod commitment;
pub use commitment::Commitment;
mod imbalance;
mod iter;
use commitment::VALUE_BLINDING_GENERATOR;
use decaf377::{r1cs::ElementVar, Fq, Fr};
use imbalance::Imbalance;
use self::commitment::BalanceCommitmentVar;
#[serde_as]
#[derive(Clone, Eq, Default, Serialize, Deserialize)]
pub struct Balance {
negated: bool,
#[serde_as(as = "Vec<(_, _)>")]
balance: BTreeMap<Id, Imbalance<NonZeroU128>>,
}
impl Debug for Balance {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Balance")
.field("required", &self.required().collect::<Vec<_>>())
.field("provided", &self.provided().collect::<Vec<_>>())
.finish()
}
}
impl Balance {
pub fn zero() -> Self {
Self::default()
}
pub fn is_zero(&self) -> bool {
self.balance.is_empty()
}
pub fn dimension(&self) -> usize {
self.balance.len()
}
pub fn required(
&self,
) -> impl Iterator<Item = Value> + DoubleEndedIterator + FusedIterator + '_ {
self.iter().filter_map(Imbalance::required)
}
pub fn provided(
&self,
) -> impl Iterator<Item = Value> + DoubleEndedIterator + FusedIterator + '_ {
self.iter().filter_map(Imbalance::provided)
}
#[allow(non_snake_case)]
pub fn commit(&self, blinding_factor: Fr) -> Commitment {
let mut commitment = decaf377::Element::default();
for imbalance in self.iter() {
let (sign, value) = imbalance.into_inner();
let G_v = value.asset_id.value_generator();
match sign {
imbalance::Sign::Required => {
commitment -= G_v * Fr::from(value.amount);
}
imbalance::Sign::Provided => {
commitment += G_v * Fr::from(value.amount);
}
}
}
commitment += blinding_factor * VALUE_BLINDING_GENERATOR.deref();
Commitment(commitment)
}
}
impl PartialEq for Balance {
fn eq(&self, other: &Self) -> bool {
if self.dimension() != other.dimension() {
return false;
}
for (i, j) in self.iter().zip(other.iter()) {
if i != j {
return false;
}
}
true
}
}
impl Neg for Balance {
type Output = Self;
fn neg(self) -> Self {
Self {
negated: !self.negated,
balance: self.balance,
}
}
}
impl Add for Balance {
type Output = Self;
fn add(mut self, mut other: Self) -> Self {
if other.dimension() > self.dimension() {
mem::swap(&mut self, &mut other);
}
for imbalance in other.into_iter() {
let (sign, Value { asset_id, amount }) = imbalance.into_inner();
let (asset_id, mut imbalance) = if let Some(amount) = NonZeroU128::new(amount.into()) {
(asset_id, sign.imbalance(amount))
} else {
unreachable!("values stored in balance are always nonzero")
};
match self.balance.entry(asset_id) {
btree_map::Entry::Vacant(entry) => {
if self.negated {
imbalance = -imbalance;
}
entry.insert(imbalance);
}
btree_map::Entry::Occupied(mut entry) => {
let mut existing_imbalance = *entry.get();
if self.negated {
existing_imbalance = -existing_imbalance;
}
if let Some(mut new_imbalance) = existing_imbalance + imbalance {
if self.negated {
new_imbalance = -new_imbalance;
}
entry.insert(new_imbalance);
} else {
entry.remove();
}
}
}
}
self
}
}
impl Add<Value> for Balance {
type Output = Balance;
fn add(self, value: Value) -> Self::Output {
self + Balance::from(value)
}
}
impl AddAssign for Balance {
fn add_assign(&mut self, other: Self) {
*self = mem::take(self) + other;
}
}
impl AddAssign<Value> for Balance {
fn add_assign(&mut self, other: Value) {
*self += Balance::from(other);
}
}
impl Sub for Balance {
type Output = Self;
fn sub(self, other: Self) -> Self {
self + -other
}
}
impl Sub<Value> for Balance {
type Output = Balance;
fn sub(self, value: Value) -> Self::Output {
self - Balance::from(value)
}
}
impl SubAssign for Balance {
fn sub_assign(&mut self, other: Self) {
*self = mem::take(self) - other;
}
}
impl SubAssign<Value> for Balance {
fn sub_assign(&mut self, other: Value) {
*self -= Balance::from(other);
}
}
impl From<Value> for Balance {
fn from(Value { amount, asset_id }: Value) -> Self {
let mut balance = BTreeMap::new();
if let Some(amount) = NonZeroU128::new(amount.into()) {
balance.insert(asset_id, Imbalance::Provided(amount));
}
Balance {
negated: false,
balance,
}
}
}
#[derive(Clone)]
pub struct BalanceVar {
pub inner: Vec<(AssetIdVar, (Boolean<Fq>, AmountVar))>,
}
impl AllocVar<Balance, Fq> for BalanceVar {
fn new_variable<T: std::borrow::Borrow<Balance>>(
cs: impl Into<ark_relations::r1cs::Namespace<Fq>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
mode: ark_r1cs_std::prelude::AllocationMode,
) -> Result<Self, SynthesisError> {
let ns = cs.into();
let cs = ns.cs();
let inner1 = f()?;
let inner = inner1.borrow();
match mode {
AllocationMode::Constant => unimplemented!(),
AllocationMode::Input => unimplemented!(),
AllocationMode::Witness => {
if !inner.negated {
unimplemented!();
}
let mut inner_balance_vars = Vec::new();
for (asset_id, imbalance) in inner.balance.iter() {
let (sign, amount) = imbalance.into_inner();
let asset_id_var = AssetIdVar::new_witness(cs.clone(), || Ok(asset_id))?;
let amount_var = AmountVar::new_witness(cs.clone(), || {
Ok(Amount::from(u128::from(amount)))
})?;
let boolean_var = match sign {
imbalance::Sign::Required => Boolean::constant(false),
imbalance::Sign::Provided => Boolean::constant(true),
};
inner_balance_vars.push((asset_id_var, (boolean_var, amount_var)));
}
Ok(BalanceVar {
inner: inner_balance_vars,
})
}
}
}
}
impl From<ValueVar> for BalanceVar {
fn from(ValueVar { amount, asset_id }: ValueVar) -> Self {
let mut balance_vec = Vec::new();
let sign = Boolean::constant(true);
balance_vec.push((asset_id, (sign, amount)));
BalanceVar { inner: balance_vec }
}
}
impl BalanceVar {
#[allow(non_snake_case)]
#[allow(clippy::assign_op_pattern)]
pub fn commit(
&self,
blinding_factor: Vec<UInt8<Fq>>,
) -> Result<BalanceCommitmentVar, SynthesisError> {
let cs = self
.inner
.get(0)
.expect("at least one contribution to balance")
.0
.asset_id
.cs();
let value_blinding_generator = ElementVar::new_constant(cs, *VALUE_BLINDING_GENERATOR)?;
let mut commitment =
value_blinding_generator.scalar_mul_le(blinding_factor.to_bits_le()?.iter())?;
for (asset_id, (sign, amount)) in self.inner.iter() {
let G_v = asset_id.value_generator()?;
let value_amount = amount.amount.clone();
let vG = G_v.scalar_mul_le(value_amount.to_bits_le()?.iter())?;
let minus_vG = vG.negate()?;
let to_add = ElementVar::conditionally_select(sign, &vG, &minus_vG)?;
commitment = commitment + to_add;
}
Ok(BalanceCommitmentVar { inner: commitment })
}
pub fn from_positive_value_var(value: ValueVar) -> Self {
value.into()
}
pub fn from_negative_value_var(value: ValueVar) -> Self {
let mut balance_vec = Vec::new();
let sign = Boolean::constant(false);
balance_vec.push((value.asset_id, (sign, value.amount)));
BalanceVar { inner: balance_vec }
}
}
impl std::ops::Add for BalanceVar {
type Output = Self;
fn add(self, other: Self) -> Self {
let mut balance_vec = self.inner;
for (asset_id, (sign, amount)) in other.inner {
balance_vec.push((asset_id, (sign, amount)));
}
BalanceVar { inner: balance_vec }
}
}
#[cfg(test)]
mod test {
use crate::{asset::Metadata, STAKING_TOKEN_ASSET_ID};
use ark_ff::Zero;
use decaf377::Fr;
use once_cell::sync::Lazy;
use proptest::prelude::*;
use super::*;
#[test]
fn provide_then_require() {
let mut balance = Balance::zero();
balance += Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
balance -= Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
assert!(balance.is_zero());
}
#[test]
fn require_then_provide() {
let mut balance = Balance::zero();
balance -= Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
balance += Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
assert!(balance.is_zero());
}
#[test]
fn provide_then_require_negative_zero() {
let mut balance = -Balance::zero();
balance += Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
balance -= Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
assert!(balance.is_zero());
}
#[test]
fn require_then_provide_negative_zero() {
let mut balance = -Balance::zero();
balance -= Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
balance += Value {
amount: 1u64.into(),
asset_id: *STAKING_TOKEN_ASSET_ID,
};
assert!(balance.is_zero());
}
#[derive(Debug, Clone)]
enum Expression {
Value(Value),
Neg(Box<Expression>),
Add(Box<Expression>, Box<Expression>),
Sub(Box<Expression>, Box<Expression>),
}
impl Expression {
fn transparent_balance_commitment(&self) -> Commitment {
match self {
Expression::Value(value) => value.commit(Fr::zero()),
Expression::Neg(expr) => -expr.transparent_balance_commitment(),
Expression::Add(lhs, rhs) => {
lhs.transparent_balance_commitment() + rhs.transparent_balance_commitment()
}
Expression::Sub(lhs, rhs) => {
lhs.transparent_balance_commitment() - rhs.transparent_balance_commitment()
}
}
}
fn balance(&self) -> Balance {
match self {
Expression::Value(value) => Balance::from(*value),
Expression::Neg(expr) => -expr.balance(),
Expression::Add(lhs, rhs) => lhs.balance() + rhs.balance(),
Expression::Sub(lhs, rhs) => lhs.balance() - rhs.balance(),
}
}
}
static DENOM_1: Lazy<Metadata> = Lazy::new(|| {
crate::asset::Cache::with_known_assets()
.get_unit("cube")
.unwrap()
.base()
});
static ASSET_ID_1: Lazy<Id> = Lazy::new(|| DENOM_1.id());
static DENOM_2: Lazy<Metadata> = Lazy::new(|| {
crate::asset::Cache::with_known_assets()
.get_unit("nala")
.unwrap()
.base()
});
static ASSET_ID_2: Lazy<Id> = Lazy::new(|| DENOM_2.id());
#[allow(clippy::arc_with_non_send_sync)]
fn gen_expression() -> impl proptest::strategy::Strategy<Value = Expression> {
(
(0u64..u32::MAX as u64), prop_oneof![Just(*ASSET_ID_1), Just(*ASSET_ID_2)],
)
.prop_map(|(amount, asset_id)| {
Expression::Value(Value {
amount: amount.into(),
asset_id,
})
})
.prop_recursive(8, 256, 2, |inner| {
prop_oneof![
inner
.clone()
.prop_map(|beneath| Expression::Neg(Box::new(beneath))),
(inner.clone(), inner.clone()).prop_map(|(left, right)| {
Expression::Add(Box::new(left), Box::new(right))
}),
(inner.clone(), inner).prop_map(|(left, right)| {
Expression::Sub(Box::new(left), Box::new(right))
}),
]
})
}
proptest! {
#[test]
fn all_expressions_correct_commitment(
expr in gen_expression()
) {
let balance = expr.balance();
let commitment = expr.transparent_balance_commitment();
let mut balance_commitment = Commitment::default();
for required in balance.required() {
balance_commitment = balance_commitment - required.commit(Fr::zero());
}
for provided in balance.provided() {
balance_commitment = balance_commitment + provided.commit(Fr::zero());
}
assert_eq!(commitment, balance_commitment);
}
}
}