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:
parent
e2a3a38047
commit
ee3c992ca6
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue