Calculate the remaining value in the transparent transaction value pool (#2486)

* add value_balance methods to transparent and shielded

* add value_balance() to transaction

* check the remaining value consensus rule

* change error name

* fix doc and nitpick

* refactor value_balance() method for joinsplit

* changes to value_balance() of Inputs

* implement joinsplits() method(not working)

* remove created methods

* remove special case

* change return error in utilities

* move utils functions to transaction methods

* fix the docs

* simplify some code

* add constrains explicitly

* remove turbofish

* refactor some transaction methods

* fix value balance signs, add docs

* simplify some code

* avoid panic in consensus check

* add missing doc

* move remaining value balance check to the state

* make changes from the last review

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2021-07-29 00:49:36 -03:00 committed by GitHub
parent e2a3a38047
commit ee3c992ca6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 238 additions and 8 deletions

View File

@ -326,6 +326,15 @@ where
}
}
impl std::ops::Neg for Amount<NegativeAllowed> {
type Output = Self;
fn neg(self) -> Self::Output {
Amount::try_from(-self.0)
.expect("a change in sign to any value inside Amount<NegativeAllowed> is always valid")
}
}
#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)]
#[allow(missing_docs)]
/// Errors that can be returned when validating `Amount`s

View File

@ -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<Item = &Nullifier> {
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<NegativeAllowed> {
self.value_balance
}
}
impl AtLeastOne<AuthorizedAction> {

View File

@ -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<NegativeAllowed> {
self.value_balance
}
}
impl<AnchorV> TransferData<AnchorV>

View File

@ -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<dyn Iterator<Item = &amount::Amount<amount::NonNegative>> + '_> {
pub fn sprout_pool_added_values(&self) -> Box<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
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<transparent::OutPoint, transparent::Utxo>,
) -> Result<ValueBalance<NegativeAllowed>, AmountError> {
let inputs = self.inputs();
let outputs = self.outputs();
let input_value_balance: Amount = inputs
.iter()
.map(|i| i.value(utxos))
.sum::<Result<Amount, AmountError>>()?;
let output_value_balance: Amount<NegativeAllowed> = outputs
.iter()
.map(|o| o.value())
.sum::<Result<Amount, AmountError>>()?;
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<ValueBalance<NegativeAllowed>, 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<ValueBalance<NegativeAllowed>, 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<ValueBalance<NegativeAllowed>, AmountError> {
let orchard = self
.orchard_shielded_data()
.iter()
.map(|o| o.value_balance())
.sum::<Result<Amount, AmountError>>()?;
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<transparent::OutPoint, transparent::Utxo>,
) -> Result<ValueBalance<NegativeAllowed>, 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)
}
}

View File

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::{
amount::{Amount, Error},
primitives::{ed25519, ZkSnarkProof},
sprout::{JoinSplit, Nullifier},
};
@ -54,4 +55,13 @@ impl<P: ZkSnarkProof> JoinSplitData<P> {
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<Amount, Error> {
self.joinsplits()
.flat_map(|j| j.vpub_old.constrain() - j.vpub_new.constrain()?)
.sum()
}
}

View File

@ -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<OutPoint, utxo::Utxo>) -> Amount<NegativeAllowed> {
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<NegativeAllowed> {
self.value
.constrain()
.expect("conversion from NonNegative to NegativeAllowed is always valid")
}
}

View File

@ -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,

View File

@ -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.

View File

@ -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<transparent::OutPoint, transparent::Utxo>,
) -> 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(())
}