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)]
|
#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)]
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
/// Errors that can be returned when validating `Amount`s
|
/// Errors that can be returned when validating `Amount`s
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Orchard shielded data for `V5` `Transaction`s.
|
//! Orchard shielded data for `V5` `Transaction`s.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
amount::Amount,
|
amount::{Amount, NegativeAllowed},
|
||||||
block::MAX_BLOCK_BYTES,
|
block::MAX_BLOCK_BYTES,
|
||||||
orchard::{tree, Action, Nullifier},
|
orchard::{tree, Action, Nullifier},
|
||||||
primitives::{
|
primitives::{
|
||||||
|
|
@ -48,6 +48,13 @@ impl ShieldedData {
|
||||||
pub fn nullifiers(&self) -> impl Iterator<Item = &Nullifier> {
|
pub fn nullifiers(&self) -> impl Iterator<Item = &Nullifier> {
|
||||||
self.actions().map(|action| &action.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> {
|
impl AtLeastOne<AuthorizedAction> {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
amount::Amount,
|
amount::{Amount, NegativeAllowed},
|
||||||
primitives::{
|
primitives::{
|
||||||
redjubjub::{Binding, Signature},
|
redjubjub::{Binding, Signature},
|
||||||
Groth16Proof,
|
Groth16Proof,
|
||||||
|
|
@ -262,6 +262,13 @@ where
|
||||||
|
|
||||||
key_bytes.into()
|
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>
|
impl<AnchorV> TransferData<AnchorV>
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,16 @@ pub use sighash::HashType;
|
||||||
pub use sighash::SigHash;
|
pub use sighash::SigHash;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
amount, block, orchard,
|
amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative},
|
||||||
|
block, orchard,
|
||||||
parameters::NetworkUpgrade,
|
parameters::NetworkUpgrade,
|
||||||
primitives::{Bctv14Proof, Groth16Proof},
|
primitives::{Bctv14Proof, Groth16Proof},
|
||||||
sapling, sprout, transparent,
|
sapling, sprout, transparent,
|
||||||
|
value_balance::ValueBalance,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// A Zcash transaction.
|
/// A Zcash transaction.
|
||||||
///
|
///
|
||||||
/// A transaction is an encoded data structure that facilitates the transfer of
|
/// 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
|
/// This value is removed from the transparent value pool of this transaction, and added to the
|
||||||
/// sprout value pool.
|
/// sprout value pool.
|
||||||
pub fn sprout_pool_added_values(
|
pub fn sprout_pool_added_values(&self) -> Box<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
|
||||||
&self,
|
|
||||||
) -> Box<dyn Iterator<Item = &amount::Amount<amount::NonNegative>> + '_> {
|
|
||||||
match self {
|
match self {
|
||||||
// JoinSplits with Bctv14 Proofs
|
// JoinSplits with Bctv14 Proofs
|
||||||
Transaction::V2 {
|
Transaction::V2 {
|
||||||
|
|
@ -552,4 +554,124 @@ impl Transaction {
|
||||||
self.orchard_shielded_data()
|
self.orchard_shielded_data()
|
||||||
.map(|orchard_shielded_data| orchard_shielded_data.flags)
|
.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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
amount::{Amount, Error},
|
||||||
primitives::{ed25519, ZkSnarkProof},
|
primitives::{ed25519, ZkSnarkProof},
|
||||||
sprout::{JoinSplit, Nullifier},
|
sprout::{JoinSplit, Nullifier},
|
||||||
};
|
};
|
||||||
|
|
@ -54,4 +55,13 @@ impl<P: ZkSnarkProof> JoinSplitData<P> {
|
||||||
self.joinsplits()
|
self.joinsplits()
|
||||||
.flat_map(|joinsplit| joinsplit.nullifiers.iter())
|
.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;
|
mod prop;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
amount::{Amount, NonNegative},
|
amount::{Amount, NegativeAllowed, NonNegative},
|
||||||
block, transaction,
|
block, transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Arbitrary data inserted by miners into a coinbase transaction.
|
/// Arbitrary data inserted by miners into a coinbase transaction.
|
||||||
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct CoinbaseData(
|
pub struct CoinbaseData(
|
||||||
|
|
@ -132,6 +134,25 @@ impl Input {
|
||||||
unreachable!("unexpected variant: Coinbase Inputs do not have OutPoints");
|
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.
|
/// 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.
|
/// The lock script defines the conditions under which this output can be spent.
|
||||||
pub lock_script: Script,
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn zero() -> Self {
|
/// Creates a [`ValueBalance`] where all the pools are zero.
|
||||||
|
pub fn zero() -> Self {
|
||||||
let zero = Amount::zero();
|
let zero = Amount::zero();
|
||||||
Self {
|
Self {
|
||||||
transparent: zero,
|
transparent: zero,
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,13 @@ pub enum ValidateContextError {
|
||||||
nullifier: orchard::Nullifier,
|
nullifier: orchard::Nullifier,
|
||||||
in_finalized_state: bool,
|
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.
|
/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
|
||||||
|
|
|
||||||
|
|
@ -109,3 +109,39 @@ pub fn transparent_double_spends(
|
||||||
|
|
||||||
Ok(())
|
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