diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 8295f17bd..38c82753b 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -326,6 +326,15 @@ where } } +impl std::ops::Neg for Amount { + type Output = Self; + + fn neg(self) -> Self::Output { + Amount::try_from(-self.0) + .expect("a change in sign to any value inside Amount is always valid") + } +} + #[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)] #[allow(missing_docs)] /// Errors that can be returned when validating `Amount`s diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index f22091028..d9eb49e05 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -1,7 +1,7 @@ //! Orchard shielded data for `V5` `Transaction`s. use crate::{ - amount::Amount, + amount::{Amount, NegativeAllowed}, block::MAX_BLOCK_BYTES, orchard::{tree, Action, Nullifier}, primitives::{ @@ -48,6 +48,13 @@ impl ShieldedData { pub fn nullifiers(&self) -> impl Iterator { self.actions().map(|action| &action.nullifier) } + + /// Provide access to the `value_balance` field of the shielded data. + /// + /// Needed to calculate the sapling value balance. + pub fn value_balance(&self) -> Amount { + self.value_balance + } } impl AtLeastOne { diff --git a/zebra-chain/src/sapling/shielded_data.rs b/zebra-chain/src/sapling/shielded_data.rs index d418f50ee..b32a0cbb0 100644 --- a/zebra-chain/src/sapling/shielded_data.rs +++ b/zebra-chain/src/sapling/shielded_data.rs @@ -7,7 +7,7 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::{ - amount::Amount, + amount::{Amount, NegativeAllowed}, primitives::{ redjubjub::{Binding, Signature}, Groth16Proof, @@ -262,6 +262,13 @@ where key_bytes.into() } + + /// Provide access to the `value_balance` field of the shielded data. + /// + /// Needed to calculate the sapling value balance. + pub fn value_balance(&self) -> Amount { + self.value_balance + } } impl TransferData diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index e0f738b9e..a3d92b79f 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -24,12 +24,16 @@ pub use sighash::HashType; pub use sighash::SigHash; use crate::{ - amount, block, orchard, + amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative}, + block, orchard, parameters::NetworkUpgrade, primitives::{Bctv14Proof, Groth16Proof}, sapling, sprout, transparent, + value_balance::ValueBalance, }; +use std::collections::HashMap; + /// A Zcash transaction. /// /// A transaction is an encoded data structure that facilitates the transfer of @@ -312,9 +316,7 @@ impl Transaction { /// /// This value is removed from the transparent value pool of this transaction, and added to the /// sprout value pool. - pub fn sprout_pool_added_values( - &self, - ) -> Box> + '_> { + pub fn sprout_pool_added_values(&self) -> Box> + '_> { match self { // JoinSplits with Bctv14 Proofs Transaction::V2 { @@ -552,4 +554,124 @@ impl Transaction { self.orchard_shielded_data() .map(|orchard_shielded_data| orchard_shielded_data.flags) } + + // value balances + + /// Return the transparent value balance. + /// + /// The change in the value of the transparent pool. + /// The sum of the outputs spent by transparent inputs in `tx_in` fields, + /// minus the sum of newly created outputs in `tx_out` fields. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn transparent_value_balance( + &self, + utxos: &HashMap, + ) -> Result, AmountError> { + let inputs = self.inputs(); + let outputs = self.outputs(); + let input_value_balance: Amount = inputs + .iter() + .map(|i| i.value(utxos)) + .sum::>()?; + + let output_value_balance: Amount = outputs + .iter() + .map(|o| o.value()) + .sum::>()?; + + Ok(ValueBalance::from_transparent_amount( + (input_value_balance - output_value_balance)?, + )) + } + + /// Return the sprout value balance + /// + /// The change in the sprout value pool. + /// The sum of all sprout `vpub_old` fields, minus the sum of all `vpub_new` fields. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn sprout_value_balance(&self) -> Result, AmountError> { + let joinsplit_amounts = match self { + Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => { + joinsplit_data.as_ref().map(JoinSplitData::value_balance) + } + Transaction::V4 { joinsplit_data, .. } => { + joinsplit_data.as_ref().map(JoinSplitData::value_balance) + } + Transaction::V1 { .. } | Transaction::V5 { .. } => None, + }; + + joinsplit_amounts + .into_iter() + .fold(Ok(Amount::zero()), |accumulator, value| { + accumulator.and_then(|sum| sum + value) + }) + .map(ValueBalance::from_sprout_amount) + } + + /// Return the sapling value balance. + /// + /// The change in the sapling value pool. + /// The negation of the sum of all `valueBalanceSapling` fields. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn sapling_value_balance(&self) -> Result, AmountError> { + let sapling_amounts = match self { + Transaction::V4 { + sapling_shielded_data, + .. + } => sapling_shielded_data + .as_ref() + .map(sapling::ShieldedData::value_balance), + Transaction::V5 { + sapling_shielded_data, + .. + } => sapling_shielded_data + .as_ref() + .map(sapling::ShieldedData::value_balance), + Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => None, + }; + + sapling_amounts + .into_iter() + .fold(Ok(Amount::zero()), |accumulator, value| { + accumulator.and_then(|sum| sum + value) + }) + .map(|amount| ValueBalance::from_sapling_amount(-amount)) + } + + /// Return the orchard value balance. + /// + /// The change in the orchard value pool. + /// The negation of the sum of all `valueBalanceOrchard` fields. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn orchard_value_balance(&self) -> Result, AmountError> { + let orchard = self + .orchard_shielded_data() + .iter() + .map(|o| o.value_balance()) + .sum::>()?; + + Ok(ValueBalance::from_orchard_amount(-orchard)) + } + + /// Get all the value balances for this transaction. + /// + /// `utxos` must contain the utxos of every input in the transaction, + /// including UTXOs created by earlier transactions in this block. + pub fn value_balance( + &self, + utxos: &HashMap, + ) -> Result, AmountError> { + let mut value_balance = ValueBalance::zero(); + + value_balance.set_transparent_value_balance(self.transparent_value_balance(utxos)?); + value_balance.set_sprout_value_balance(self.sprout_value_balance()?); + value_balance.set_sapling_value_balance(self.sapling_value_balance()?); + value_balance.set_orchard_value_balance(self.orchard_value_balance()?); + + Ok(value_balance) + } } diff --git a/zebra-chain/src/transaction/joinsplit.rs b/zebra-chain/src/transaction/joinsplit.rs index 0941cf5ca..1d583b47c 100644 --- a/zebra-chain/src/transaction/joinsplit.rs +++ b/zebra-chain/src/transaction/joinsplit.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::{ + amount::{Amount, Error}, primitives::{ed25519, ZkSnarkProof}, sprout::{JoinSplit, Nullifier}, }; @@ -54,4 +55,13 @@ impl JoinSplitData

{ self.joinsplits() .flat_map(|joinsplit| joinsplit.nullifiers.iter()) } + + /// Calculate and return the value balance for the joinsplits. + /// + /// Needed to calculate the sprout value balance. + pub fn value_balance(&self) -> Result { + self.joinsplits() + .flat_map(|j| j.vpub_old.constrain() - j.vpub_new.constrain()?) + .sum() + } } diff --git a/zebra-chain/src/transparent.rs b/zebra-chain/src/transparent.rs index 6200b0306..7e3e6f69e 100644 --- a/zebra-chain/src/transparent.rs +++ b/zebra-chain/src/transparent.rs @@ -20,10 +20,12 @@ mod arbitrary; mod prop; use crate::{ - amount::{Amount, NonNegative}, + amount::{Amount, NegativeAllowed, NonNegative}, block, transaction, }; +use std::collections::HashMap; + /// Arbitrary data inserted by miners into a coinbase transaction. #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct CoinbaseData( @@ -132,6 +134,25 @@ impl Input { unreachable!("unexpected variant: Coinbase Inputs do not have OutPoints"); } } + + /// Get the value spent by this input. + /// This amount is added to the transaction value pool by this input. + /// + /// # Panics + /// + /// If the provided Utxos don't have the transaction outpoint. + pub fn value(&self, utxos: &HashMap) -> Amount { + match self { + Input::PrevOut { outpoint, .. } => utxos + .get(outpoint) + .expect("Provided Utxos don't have transaction Outpoint") + .output + .value + .constrain() + .expect("conversion from NonNegative to NegativeAllowed is always valid"), + Input::Coinbase { .. } => Amount::zero(), + } + } } /// A transparent output from a transaction. @@ -156,3 +177,13 @@ pub struct Output { /// The lock script defines the conditions under which this output can be spent. pub lock_script: Script, } + +impl Output { + /// Get the value contained in this output. + /// This amount is subtracted from the transaction value pool by this output. + pub fn value(&self) -> Amount { + self.value + .constrain() + .expect("conversion from NonNegative to NegativeAllowed is always valid") + } +} diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index 368fa0078..526d7ea40 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -117,7 +117,8 @@ where self } - fn zero() -> Self { + /// Creates a [`ValueBalance`] where all the pools are zero. + pub fn zero() -> Self { let zero = Amount::zero(); Self { transparent: zero, diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 4965dbad5..a495debd5 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -115,6 +115,13 @@ pub enum ValidateContextError { nullifier: orchard::Nullifier, in_finalized_state: bool, }, + + #[error("remaining value in the transparent transaction value pool MUST be nonnegative: {transaction_hash:?}, in finalized state: {in_finalized_state:?}")] + #[non_exhaustive] + InvalidRemainingTransparentValue { + transaction_hash: zebra_chain::transaction::Hash, + in_finalized_state: bool, + }, } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/service/check/utxo.rs b/zebra-state/src/service/check/utxo.rs index faf809205..ac77bf47c 100644 --- a/zebra-state/src/service/check/utxo.rs +++ b/zebra-state/src/service/check/utxo.rs @@ -109,3 +109,39 @@ pub fn transparent_double_spends( Ok(()) } + +/// Reject negative remaining transaction value. +/// +/// Consensus rule: The remaining value in the transparent transaction value pool MUST be nonnegative. +/// +/// https://zips.z.cash/protocol/protocol.pdf#transactions +#[allow(dead_code)] +pub fn remaining_transaction_value( + prepared: &PreparedBlock, + utxos: &HashMap, +) -> Result<(), ValidateContextError> { + for transaction in prepared.block.transactions.iter() { + // This rule does not apply to coinbase transactions. + if transaction.is_coinbase() { + continue; + } + + // Check the remaining transparent value pool for this transaction + let value_balance = transaction.value_balance(utxos); + match value_balance { + Ok(vb) => match vb.remaining_transaction_value() { + Ok(_) => Ok(()), + Err(_) => Err(ValidateContextError::InvalidRemainingTransparentValue { + transaction_hash: transaction.hash(), + in_finalized_state: false, + }), + }, + Err(_) => Err(ValidateContextError::InvalidRemainingTransparentValue { + transaction_hash: transaction.hash(), + in_finalized_state: false, + }), + }? + } + + Ok(()) +}