diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 28e830f9f..f44a9f17d 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -430,6 +430,26 @@ pub enum Error { }, } +impl Error { + /// Returns the invalid value for this error. + /// + /// This value may be an initial input value, partially calculated value, + /// or an overflowing or underflowing value. + pub fn invalid_value(&self) -> i128 { + use Error::*; + + match self.clone() { + Constraint { value, .. } => value.into(), + Convert { value, .. } => value, + MultiplicationOverflow { + overflowing_result, .. + } => overflowing_result, + DivideByZero { amount } => amount.into(), + SumOverflow { partial_sum, .. } => partial_sum.into(), + } + } +} + /// Marker type for `Amount` that allows negative values. /// /// ``` @@ -521,381 +541,3 @@ impl ZcashDeserialize for Amount { Ok(reader.read_u64::()?.try_into()?) } } - -// TODO: move to tests::vectors after PR #2577 merges -#[cfg(test)] -mod test { - use crate::serialization::ZcashDeserializeInto; - - use super::*; - - use std::{collections::hash_map::RandomState, collections::HashSet, fmt::Debug}; - - use color_eyre::eyre::Result; - - #[test] - fn test_add_bare() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let neg_one: Amount = (-1).try_into()?; - - let zero: Amount = Amount::zero(); - let new_zero = one + neg_one; - - assert_eq!(zero, new_zero?); - - Ok(()) - } - - #[test] - fn test_add_opt_lhs() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let one = Ok(one); - let neg_one: Amount = (-1).try_into()?; - - let zero: Amount = Amount::zero(); - let new_zero = one + neg_one; - - assert_eq!(zero, new_zero?); - - Ok(()) - } - - #[test] - fn test_add_opt_rhs() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let neg_one: Amount = (-1).try_into()?; - let neg_one = Ok(neg_one); - - let zero: Amount = Amount::zero(); - let new_zero = one + neg_one; - - assert_eq!(zero, new_zero?); - - Ok(()) - } - - #[test] - fn test_add_opt_both() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let one = Ok(one); - let neg_one: Amount = (-1).try_into()?; - let neg_one = Ok(neg_one); - - let zero: Amount = Amount::zero(); - let new_zero = one.and_then(|one| one + neg_one); - - assert_eq!(zero, new_zero?); - - Ok(()) - } - - #[test] - fn test_add_assign() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let neg_one: Amount = (-1).try_into()?; - let mut neg_one = Ok(neg_one); - - let zero: Amount = Amount::zero(); - neg_one += one; - let new_zero = neg_one; - - assert_eq!(Ok(zero), new_zero); - - Ok(()) - } - - #[test] - fn test_sub_bare() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let zero: Amount = Amount::zero(); - - let neg_one: Amount = (-1).try_into()?; - let new_neg_one = zero - one; - - assert_eq!(Ok(neg_one), new_neg_one); - - Ok(()) - } - - #[test] - fn test_sub_opt_lhs() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let one = Ok(one); - let zero: Amount = Amount::zero(); - - let neg_one: Amount = (-1).try_into()?; - let new_neg_one = zero - one; - - assert_eq!(Ok(neg_one), new_neg_one); - - Ok(()) - } - - #[test] - fn test_sub_opt_rhs() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let zero: Amount = Amount::zero(); - let zero = Ok(zero); - - let neg_one: Amount = (-1).try_into()?; - let new_neg_one = zero - one; - - assert_eq!(Ok(neg_one), new_neg_one); - - Ok(()) - } - - #[test] - fn test_sub_assign() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let zero: Amount = Amount::zero(); - let mut zero = Ok(zero); - - let neg_one: Amount = (-1).try_into()?; - zero -= one; - let new_neg_one = zero; - - assert_eq!(Ok(neg_one), new_neg_one); - - Ok(()) - } - - #[test] - fn add_with_diff_constraints() -> Result<()> { - zebra_test::init(); - - let one = Amount::::try_from(1)?; - let zero: Amount = Amount::zero(); - - (zero - one.constrain()).expect("should allow negative"); - (zero.constrain() - one).expect_err("shouldn't allow negative"); - - Ok(()) - } - - #[test] - fn deserialize_checks_bounds() -> Result<()> { - zebra_test::init(); - - let big = (MAX_MONEY * 2) - .try_into() - .expect("unexpectedly large constant: multiplied constant should be within range"); - let neg = -10; - - let mut big_bytes = Vec::new(); - (&mut big_bytes) - .write_u64::(big) - .expect("unexpected serialization failure: vec should be infalliable"); - - let mut neg_bytes = Vec::new(); - (&mut neg_bytes) - .write_i64::(neg) - .expect("unexpected serialization failure: vec should be infalliable"); - - Amount::::zcash_deserialize(big_bytes.as_slice()) - .expect_err("deserialization should reject too large values"); - Amount::::zcash_deserialize(big_bytes.as_slice()) - .expect_err("deserialization should reject too large values"); - - Amount::::zcash_deserialize(neg_bytes.as_slice()) - .expect_err("NonNegative deserialization should reject negative values"); - let amount: Amount = neg_bytes - .zcash_deserialize_into() - .expect("NegativeAllowed deserialization should allow negative values"); - - assert_eq!(amount.0, neg); - - Ok(()) - } - - #[test] - fn hash() -> Result<()> { - zebra_test::init(); - - let one = Amount::::try_from(1)?; - let another_one = Amount::::try_from(1)?; - let zero: Amount = Amount::zero(); - - let hash_set: HashSet, RandomState> = [one].iter().cloned().collect(); - assert_eq!(hash_set.len(), 1); - - let hash_set: HashSet, RandomState> = - [one, one].iter().cloned().collect(); - assert_eq!(hash_set.len(), 1, "Amount hashes are consistent"); - - let hash_set: HashSet, RandomState> = - [one, another_one].iter().cloned().collect(); - assert_eq!(hash_set.len(), 1, "Amount hashes are by value"); - - let hash_set: HashSet, RandomState> = - [one, zero].iter().cloned().collect(); - assert_eq!( - hash_set.len(), - 2, - "Amount hashes are different for different values" - ); - - Ok(()) - } - - #[test] - fn ordering_constraints() -> Result<()> { - zebra_test::init(); - - ordering::()?; - ordering::()?; - ordering::()?; - ordering::()?; - - Ok(()) - } - - #[allow(clippy::eq_op)] - fn ordering() -> Result<()> - where - C1: Constraint + Debug, - C2: Constraint + Debug, - { - let zero: Amount = Amount::zero(); - let one = Amount::::try_from(1)?; - let another_one = Amount::::try_from(1)?; - - assert_eq!(one, one); - assert_eq!(one, another_one, "Amount equality is by value"); - - assert_ne!(one, zero); - assert_ne!(zero, one); - - assert!(one > zero); - assert!(zero < one); - assert!(zero <= one); - - let negative_one = Amount::::try_from(-1)?; - let negative_two = Amount::::try_from(-2)?; - - assert_ne!(negative_one, zero); - assert_ne!(negative_one, one); - - assert!(negative_one < zero); - assert!(negative_one <= one); - assert!(zero > negative_one); - assert!(zero >= negative_one); - assert!(negative_two < negative_one); - assert!(negative_one > negative_two); - - Ok(()) - } - - #[test] - fn test_sum() -> Result<()> { - zebra_test::init(); - - let one: Amount = 1.try_into()?; - let neg_one: Amount = (-1).try_into()?; - - let zero: Amount = Amount::zero(); - - // success - let amounts = vec![one, neg_one, zero]; - - let sum_ref: Amount = amounts.iter().sum::>()?; - let sum_value: Amount = amounts.into_iter().sum::>()?; - - assert_eq!(sum_ref, sum_value); - assert_eq!(sum_ref, zero); - - // above max for Amount error - let max: Amount = MAX_MONEY.try_into()?; - let amounts = vec![one, max]; - let integer_sum: i64 = amounts.iter().map(|a| a.0).sum(); - - let sum_ref = amounts.iter().sum::>(); - let sum_value = amounts.into_iter().sum::>(); - - assert_eq!(sum_ref, sum_value); - assert_eq!( - sum_ref, - Err(Error::SumOverflow { - partial_sum: integer_sum, - remaining_items: 0 - }) - ); - - // below min for Amount error - let min: Amount = (-MAX_MONEY).try_into()?; - let amounts = vec![min, neg_one]; - let integer_sum: i64 = amounts.iter().map(|a| a.0).sum(); - - let sum_ref = amounts.iter().sum::>(); - let sum_value = amounts.into_iter().sum::>(); - - assert_eq!(sum_ref, sum_value); - assert_eq!( - sum_ref, - Err(Error::SumOverflow { - partial_sum: integer_sum, - remaining_items: 0 - }) - ); - - // above max of i64 error - let times: usize = (i64::MAX / MAX_MONEY) - .try_into() - .expect("4392 can always be converted to usize"); - let amounts: Vec = std::iter::repeat(MAX_MONEY.try_into()?) - .take(times + 1) - .collect(); - - let sum_ref = amounts.iter().sum::>(); - let sum_value = amounts.into_iter().sum::>(); - - assert_eq!(sum_ref, sum_value); - assert_eq!( - sum_ref, - Err(Error::SumOverflow { - partial_sum: 4200000000000000, - remaining_items: 4391 - }) - ); - - // below min of i64 overflow - let times: usize = (i64::MAX / MAX_MONEY) - .try_into() - .expect("4392 can always be converted to usize"); - let neg_max_money: Amount = (-MAX_MONEY).try_into()?; - let amounts: Vec> = - std::iter::repeat(neg_max_money).take(times + 1).collect(); - - let sum_ref = amounts.iter().sum::>(); - let sum_value = amounts.into_iter().sum::>(); - - assert_eq!(sum_ref, sum_value); - assert_eq!( - sum_ref, - Err(Error::SumOverflow { - partial_sum: -4200000000000000, - remaining_items: 4391 - }) - ); - - Ok(()) - } -} diff --git a/zebra-chain/src/amount/tests.rs b/zebra-chain/src/amount/tests.rs index 0ca4ffe62..71ee495d4 100644 --- a/zebra-chain/src/amount/tests.rs +++ b/zebra-chain/src/amount/tests.rs @@ -1,3 +1,4 @@ //! Tests for amounts mod prop; +mod vectors; diff --git a/zebra-chain/src/amount/tests/vectors.rs b/zebra-chain/src/amount/tests/vectors.rs new file mode 100644 index 000000000..d1f764b4b --- /dev/null +++ b/zebra-chain/src/amount/tests/vectors.rs @@ -0,0 +1,373 @@ +//! Fixed test vectors for amounts. + +use crate::serialization::ZcashDeserializeInto; + +use super::super::*; + +use std::{collections::hash_map::RandomState, collections::HashSet, fmt::Debug}; + +use color_eyre::eyre::Result; + +#[test] +fn test_add_bare() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + + let zero: Amount = Amount::zero(); + let new_zero = one + neg_one; + + assert_eq!(zero, new_zero?); + + Ok(()) +} + +#[test] +fn test_add_opt_lhs() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let one = Ok(one); + let neg_one: Amount = (-1).try_into()?; + + let zero: Amount = Amount::zero(); + let new_zero = one + neg_one; + + assert_eq!(zero, new_zero?); + + Ok(()) +} + +#[test] +fn test_add_opt_rhs() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + let neg_one = Ok(neg_one); + + let zero: Amount = Amount::zero(); + let new_zero = one + neg_one; + + assert_eq!(zero, new_zero?); + + Ok(()) +} + +#[test] +fn test_add_opt_both() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let one = Ok(one); + let neg_one: Amount = (-1).try_into()?; + let neg_one = Ok(neg_one); + + let zero: Amount = Amount::zero(); + let new_zero = one.and_then(|one| one + neg_one); + + assert_eq!(zero, new_zero?); + + Ok(()) +} + +#[test] +fn test_add_assign() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + let mut neg_one = Ok(neg_one); + + let zero: Amount = Amount::zero(); + neg_one += one; + let new_zero = neg_one; + + assert_eq!(Ok(zero), new_zero); + + Ok(()) +} + +#[test] +fn test_sub_bare() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let zero: Amount = Amount::zero(); + + let neg_one: Amount = (-1).try_into()?; + let new_neg_one = zero - one; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) +} + +#[test] +fn test_sub_opt_lhs() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let one = Ok(one); + let zero: Amount = Amount::zero(); + + let neg_one: Amount = (-1).try_into()?; + let new_neg_one = zero - one; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) +} + +#[test] +fn test_sub_opt_rhs() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let zero: Amount = Amount::zero(); + let zero = Ok(zero); + + let neg_one: Amount = (-1).try_into()?; + let new_neg_one = zero - one; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) +} + +#[test] +fn test_sub_assign() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let zero: Amount = Amount::zero(); + let mut zero = Ok(zero); + + let neg_one: Amount = (-1).try_into()?; + zero -= one; + let new_neg_one = zero; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) +} + +#[test] +fn add_with_diff_constraints() -> Result<()> { + zebra_test::init(); + + let one = Amount::::try_from(1)?; + let zero: Amount = Amount::zero(); + + (zero - one.constrain()).expect("should allow negative"); + (zero.constrain() - one).expect_err("shouldn't allow negative"); + + Ok(()) +} + +#[test] +fn deserialize_checks_bounds() -> Result<()> { + zebra_test::init(); + + let big = (MAX_MONEY * 2) + .try_into() + .expect("unexpectedly large constant: multiplied constant should be within range"); + let neg = -10; + + let mut big_bytes = Vec::new(); + (&mut big_bytes) + .write_u64::(big) + .expect("unexpected serialization failure: vec should be infalliable"); + + let mut neg_bytes = Vec::new(); + (&mut neg_bytes) + .write_i64::(neg) + .expect("unexpected serialization failure: vec should be infalliable"); + + Amount::::zcash_deserialize(big_bytes.as_slice()) + .expect_err("deserialization should reject too large values"); + Amount::::zcash_deserialize(big_bytes.as_slice()) + .expect_err("deserialization should reject too large values"); + + Amount::::zcash_deserialize(neg_bytes.as_slice()) + .expect_err("NonNegative deserialization should reject negative values"); + let amount: Amount = neg_bytes + .zcash_deserialize_into() + .expect("NegativeAllowed deserialization should allow negative values"); + + assert_eq!(amount.0, neg); + + Ok(()) +} + +#[test] +fn hash() -> Result<()> { + zebra_test::init(); + + let one = Amount::::try_from(1)?; + let another_one = Amount::::try_from(1)?; + let zero: Amount = Amount::zero(); + + let hash_set: HashSet, RandomState> = [one].iter().cloned().collect(); + assert_eq!(hash_set.len(), 1); + + let hash_set: HashSet, RandomState> = [one, one].iter().cloned().collect(); + assert_eq!(hash_set.len(), 1, "Amount hashes are consistent"); + + let hash_set: HashSet, RandomState> = + [one, another_one].iter().cloned().collect(); + assert_eq!(hash_set.len(), 1, "Amount hashes are by value"); + + let hash_set: HashSet, RandomState> = [one, zero].iter().cloned().collect(); + assert_eq!( + hash_set.len(), + 2, + "Amount hashes are different for different values" + ); + + Ok(()) +} + +#[test] +fn ordering_constraints() -> Result<()> { + zebra_test::init(); + + ordering::()?; + ordering::()?; + ordering::()?; + ordering::()?; + + Ok(()) +} + +#[allow(clippy::eq_op)] +fn ordering() -> Result<()> +where + C1: Constraint + Debug, + C2: Constraint + Debug, +{ + let zero: Amount = Amount::zero(); + let one = Amount::::try_from(1)?; + let another_one = Amount::::try_from(1)?; + + assert_eq!(one, one); + assert_eq!(one, another_one, "Amount equality is by value"); + + assert_ne!(one, zero); + assert_ne!(zero, one); + + assert!(one > zero); + assert!(zero < one); + assert!(zero <= one); + + let negative_one = Amount::::try_from(-1)?; + let negative_two = Amount::::try_from(-2)?; + + assert_ne!(negative_one, zero); + assert_ne!(negative_one, one); + + assert!(negative_one < zero); + assert!(negative_one <= one); + assert!(zero > negative_one); + assert!(zero >= negative_one); + assert!(negative_two < negative_one); + assert!(negative_one > negative_two); + + Ok(()) +} + +#[test] +fn test_sum() -> Result<()> { + zebra_test::init(); + + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + + let zero: Amount = Amount::zero(); + + // success + let amounts = vec![one, neg_one, zero]; + + let sum_ref: Amount = amounts.iter().sum::>()?; + let sum_value: Amount = amounts.into_iter().sum::>()?; + + assert_eq!(sum_ref, sum_value); + assert_eq!(sum_ref, zero); + + // above max for Amount error + let max: Amount = MAX_MONEY.try_into()?; + let amounts = vec![one, max]; + let integer_sum: i64 = amounts.iter().map(|a| a.0).sum(); + + let sum_ref = amounts.iter().sum::>(); + let sum_value = amounts.into_iter().sum::>(); + + assert_eq!(sum_ref, sum_value); + assert_eq!( + sum_ref, + Err(Error::SumOverflow { + partial_sum: integer_sum, + remaining_items: 0 + }) + ); + + // below min for Amount error + let min: Amount = (-MAX_MONEY).try_into()?; + let amounts = vec![min, neg_one]; + let integer_sum: i64 = amounts.iter().map(|a| a.0).sum(); + + let sum_ref = amounts.iter().sum::>(); + let sum_value = amounts.into_iter().sum::>(); + + assert_eq!(sum_ref, sum_value); + assert_eq!( + sum_ref, + Err(Error::SumOverflow { + partial_sum: integer_sum, + remaining_items: 0 + }) + ); + + // above max of i64 error + let times: usize = (i64::MAX / MAX_MONEY) + .try_into() + .expect("4392 can always be converted to usize"); + let amounts: Vec = std::iter::repeat(MAX_MONEY.try_into()?) + .take(times + 1) + .collect(); + + let sum_ref = amounts.iter().sum::>(); + let sum_value = amounts.into_iter().sum::>(); + + assert_eq!(sum_ref, sum_value); + assert_eq!( + sum_ref, + Err(Error::SumOverflow { + partial_sum: 4200000000000000, + remaining_items: 4391 + }) + ); + + // below min of i64 overflow + let times: usize = (i64::MAX / MAX_MONEY) + .try_into() + .expect("4392 can always be converted to usize"); + let neg_max_money: Amount = (-MAX_MONEY).try_into()?; + let amounts: Vec> = + std::iter::repeat(neg_max_money).take(times + 1).collect(); + + let sum_ref = amounts.iter().sum::>(); + let sum_value = amounts.into_iter().sum::>(); + + assert_eq!(sum_ref, sum_value); + assert_eq!( + sum_ref, + Err(Error::SumOverflow { + partial_sum: -4200000000000000, + remaining_items: 4391 + }) + ); + + Ok(()) +} diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 7063f184a..b3a4b753e 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -14,7 +14,7 @@ pub mod arbitrary; #[cfg(any(test, feature = "bench"))] pub mod tests; -use std::{collections::HashMap, convert::TryInto, fmt}; +use std::{collections::HashMap, convert::TryInto, fmt, ops::Neg}; pub use commitment::{ChainHistoryMmrRootHash, Commitment, CommitmentError}; pub use hash::Hash; @@ -170,20 +170,33 @@ impl Block { .expect("number of transactions must fit u64") } - /// Get all the value balances from this block by summing all the value balances - /// in each transaction the block has. + /// Get the overall chain value pool change in this block, + /// the negative sum of the transaction value balances in this block. /// - /// `utxos` must contain the utxos of every input in the block, - /// including UTXOs created by a transaction in this block, - /// then spent by a later transaction that's also in this block. - pub fn value_balance( + /// These are the changes in the transparent, sprout, sapling, and orchard + /// chain value pools, as a result of this block. + /// + /// Positive values are added to the corresponding chain value pool. + /// Negative values are removed from the corresponding pool. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + /// + /// `utxos` must contain the [`Utxo`]s of every input in this block, + /// including UTXOs created by earlier transactions in this block. + /// + /// Note: the chain value pool has the opposite sign to the transaction + /// value pool. + pub fn chain_value_pool_change( &self, utxos: &HashMap, ) -> Result, ValueBalanceError> { - self.transactions + let transaction_value_balance_total = self + .transactions .iter() .flat_map(|t| t.value_balance(utxos)) - .sum() + .sum::, _>>()?; + + Ok(transaction_value_balance_total.neg()) } } diff --git a/zebra-chain/src/block/arbitrary.rs b/zebra-chain/src/block/arbitrary.rs index 9105491fc..da4a1e49b 100644 --- a/zebra-chain/src/block/arbitrary.rs +++ b/zebra-chain/src/block/arbitrary.rs @@ -409,9 +409,7 @@ impl Block { // delete invalid transactions block.transactions = new_transactions; - // TODO: if needed, fixup: - // - transparent values and shielded value balances - // - transaction outputs (currently 0..=16 outputs, consensus rules require 1..) + // TODO: if needed, fixup after modifying the block: // - history and authorizing data commitments // - the transaction merkle root @@ -452,6 +450,7 @@ where { let mut spend_restriction = transaction.coinbase_spend_restriction(height); let mut new_inputs = Vec::new(); + let mut spent_outputs = HashMap::new(); // fixup the transparent spends for mut input in transaction.inputs().to_vec().into_iter() { @@ -466,7 +465,10 @@ where input.set_outpoint(selected_outpoint); new_inputs.push(input); - utxos.remove(&selected_outpoint); + let spent_utxo = utxos + .remove(&selected_outpoint) + .expect("selected outpoint must have a UTXO"); + spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output); } // otherwise, drop the invalid input, because it has no valid UTXOs to spend } else { @@ -478,8 +480,11 @@ where // delete invalid inputs *transaction.inputs_mut() = new_inputs; - // keep transactions with valid input counts - // coinbase transactions will never fail this check + transaction + .fix_remaining_value(&spent_outputs) + .expect("generated chain value fixes always succeed"); + + // TODO: if needed, check output count here as well if transaction.has_transparent_or_shielded_inputs() { // skip genesis created UTXOs if height > Height(0) { diff --git a/zebra-chain/src/orchard/tree.rs b/zebra-chain/src/orchard/tree.rs index c8d48052e..2b9644aaf 100644 --- a/zebra-chain/src/orchard/tree.rs +++ b/zebra-chain/src/orchard/tree.rs @@ -205,7 +205,7 @@ impl<'de> serde::Deserialize<'de> for Node { } #[allow(dead_code, missing_docs)] -#[derive(Error, Debug, PartialEq, Eq)] +#[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum NoteCommitmentTreeError { #[error("The note commitment tree is full")] FullTree, diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs index 4acce7653..ba7b3d591 100644 --- a/zebra-chain/src/sapling/tree.rs +++ b/zebra-chain/src/sapling/tree.rs @@ -169,7 +169,7 @@ impl<'de> serde::Deserialize<'de> for Node { } #[allow(dead_code, missing_docs)] -#[derive(Error, Debug, PartialEq, Eq)] +#[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum NoteCommitmentTreeError { #[error("The note commitment tree is full")] FullTree, diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 5afad812f..c4f483215 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -31,10 +31,10 @@ use crate::{ primitives::{Bctv14Proof, Groth16Proof}, sapling, sprout, transparent::{ - self, + self, outputs_from_utxos, CoinbaseSpendRestriction::{self, *}, }, - value_balance::ValueBalance, + value_balance::{ValueBalance, ValueBalanceError}, }; use std::collections::HashMap; @@ -408,52 +408,6 @@ impl Transaction { } } - /// Returns the `vpub_old` fields from `JoinSplit`s in this transaction, regardless of version. - /// - /// 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> + '_> { - match self { - // JoinSplits with Bctv14 Proofs - Transaction::V2 { - joinsplit_data: Some(joinsplit_data), - .. - } - | Transaction::V3 { - joinsplit_data: Some(joinsplit_data), - .. - } => Box::new( - joinsplit_data - .joinsplits() - .map(|joinsplit| &joinsplit.vpub_old), - ), - // JoinSplits with Groth Proofs - Transaction::V4 { - joinsplit_data: Some(joinsplit_data), - .. - } => Box::new( - joinsplit_data - .joinsplits() - .map(|joinsplit| &joinsplit.vpub_old), - ), - // No JoinSplits - Transaction::V1 { .. } - | Transaction::V2 { - joinsplit_data: None, - .. - } - | Transaction::V3 { - joinsplit_data: None, - .. - } - | Transaction::V4 { - joinsplit_data: None, - .. - } - | Transaction::V5 { .. } => Box::new(std::iter::empty()), - } - } - /// Access the sprout::Nullifiers in this transaction, regardless of version. pub fn sprout_nullifiers(&self) -> Box + '_> { // This function returns a boxed iterator because the different @@ -719,121 +673,443 @@ impl Transaction { // value balances - /// Return the transparent value balance. + /// Return the transparent value balance, + /// using the outputs spent by this transaction. /// - /// 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( + /// See `transparent_value_balance` for details. + fn transparent_value_balance_from_outputs( &self, - utxos: &HashMap, - ) -> Result, AmountError> { - let inputs = self.inputs(); - let outputs = self.outputs(); - let input_value_balance: Amount = inputs + outputs: &HashMap, + ) -> Result, ValueBalanceError> { + let input_value = self + .inputs() .iter() - .map(|i| i.value(utxos)) - .sum::>()?; + .map(|i| i.value_from_outputs(outputs)) + .sum::, AmountError>>() + .map_err(ValueBalanceError::Transparent)? + .constrain() + .expect("conversion from NonNegative to NegativeAllowed is always valid"); - let output_value_balance: Amount = outputs + let output_value = self + .outputs() .iter() .map(|o| o.value()) - .sum::>()?; + .sum::, AmountError>>() + .map_err(ValueBalanceError::Transparent)? + .constrain() + .expect("conversion from NonNegative to NegativeAllowed is always valid"); - Ok(ValueBalance::from_transparent_amount( - (output_value_balance - input_value_balance)?, - )) + (input_value - output_value) + .map(ValueBalance::from_transparent_amount) + .map_err(ValueBalanceError::Transparent) } - /// Return the sprout value balance + /// Return the transparent value balance, + /// the change in the value of the transaction value pool. /// - /// The change in the sprout value pool. - /// The sum of all sprout `vpub_old` fields, minus the sum of all `vpub_new` fields. + /// The sum of the UTXOs spent by transparent inputs in `tx_in` fields, + /// minus the sum of the newly created outputs in `tx_out` fields. + /// + /// Positive values are added to this transaction's value pool, + /// and removed from the transparent chain value pool. + /// Negative values are removed from the transparent chain value pool, + /// and added to this transaction. /// /// 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. + #[allow(dead_code)] + fn transparent_value_balance( + &self, + utxos: &HashMap, + ) -> Result, ValueBalanceError> { + self.transparent_value_balance_from_outputs(&outputs_from_utxos(utxos.clone())) + } + + /// Modify the transparent output values of this transaction, regardless of version. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn output_values_mut(&mut self) -> impl Iterator> { + self.outputs_mut() + .iter_mut() + .map(|output| &mut output.value) + } + + /// Returns the `vpub_old` fields from `JoinSplit`s in this transaction, + /// regardless of version. + /// + /// These values are added to the sprout chain value pool, + /// and removed from the value pool of this transaction. + pub fn output_values_to_sprout(&self) -> Box> + '_> { + match self { + // JoinSplits with Bctv14 Proofs + Transaction::V2 { + joinsplit_data: Some(joinsplit_data), + .. + } + | Transaction::V3 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits() + .map(|joinsplit| &joinsplit.vpub_old), + ), + // JoinSplits with Groth Proofs + Transaction::V4 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits() + .map(|joinsplit| &joinsplit.vpub_old), + ), + // No JoinSplits + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } + | Transaction::V5 { .. } => Box::new(std::iter::empty()), + } + } + + /// Modify the `vpub_old` fields from `JoinSplit`s in this transaction, + /// regardless of version. + /// + /// See `output_values_to_sprout` for details. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn output_values_to_sprout_mut( + &mut self, + ) -> Box> + '_> { + match self { + // JoinSplits with Bctv14 Proofs + Transaction::V2 { + joinsplit_data: Some(joinsplit_data), + .. + } + | Transaction::V3 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits_mut() + .map(|joinsplit| &mut joinsplit.vpub_old), + ), + // JoinSplits with Groth Proofs + Transaction::V4 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits_mut() + .map(|joinsplit| &mut joinsplit.vpub_old), + ), + // No JoinSplits + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } + | Transaction::V5 { .. } => Box::new(std::iter::empty()), + } + } + + /// Returns the `vpub_new` fields from `JoinSplit`s in this transaction, + /// regardless of version. + /// + /// These values are removed from the value pool of this transaction. + /// and added to the sprout chain value pool. + pub fn input_values_from_sprout(&self) -> Box> + '_> { + match self { + // JoinSplits with Bctv14 Proofs + Transaction::V2 { + joinsplit_data: Some(joinsplit_data), + .. + } + | Transaction::V3 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits() + .map(|joinsplit| &joinsplit.vpub_new), + ), + // JoinSplits with Groth Proofs + Transaction::V4 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits() + .map(|joinsplit| &joinsplit.vpub_new), + ), + // No JoinSplits + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } + | Transaction::V5 { .. } => Box::new(std::iter::empty()), + } + } + + /// Modify the `vpub_new` fields from `JoinSplit`s in this transaction, + /// regardless of version. + /// + /// See `input_values_from_sprout` for details. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn input_values_from_sprout_mut( + &mut self, + ) -> Box> + '_> { + match self { + // JoinSplits with Bctv14 Proofs + Transaction::V2 { + joinsplit_data: Some(joinsplit_data), + .. + } + | Transaction::V3 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits_mut() + .map(|joinsplit| &mut joinsplit.vpub_new), + ), + // JoinSplits with Groth Proofs + Transaction::V4 { + joinsplit_data: Some(joinsplit_data), + .. + } => Box::new( + joinsplit_data + .joinsplits_mut() + .map(|joinsplit| &mut joinsplit.vpub_new), + ), + // No JoinSplits + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } + | Transaction::V5 { .. } => Box::new(std::iter::empty()), + } + } + + /// Return the sprout value balance, + /// the change in the transaction value pool due to sprout [`JoinSplit`]s. + /// + /// The sum of all sprout `vpub_new` fields, minus the sum of all `vpub_old` fields. + /// + /// Positive values are added to this transaction's value pool, + /// and removed from the sprout chain value pool. + /// Negative values are removed from this transaction, + /// and added to the sprout pool. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn sprout_value_balance(&self) -> Result, ValueBalanceError> { + let joinsplit_value_balance = match self { + Transaction::V2 { + joinsplit_data: Some(joinsplit_data), + .. + } + | Transaction::V3 { + joinsplit_data: Some(joinsplit_data), + .. + } => joinsplit_data + .value_balance() + .map_err(ValueBalanceError::Sprout)?, + Transaction::V4 { + joinsplit_data: Some(joinsplit_data), + .. + } => joinsplit_data + .value_balance() + .map_err(ValueBalanceError::Sprout)?, + + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } + | Transaction::V5 { .. } => Amount::zero(), + }; + + Ok(ValueBalance::from_sprout_amount(joinsplit_value_balance)) + } + + /// Return the sapling value balance, + /// the change in the transaction value pool due to sapling `Spend`s and `Output`s. + /// + /// Returns the `valueBalanceSapling` field in this transaction. + /// + /// Positive values are added to this transaction's value pool, + /// and removed from the sapling chain value pool. + /// Negative values are removed from this transaction, + /// and added to sapling pool. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn sapling_value_balance(&self) -> ValueBalance { + let sapling_value_balance = match self { + Transaction::V4 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => sapling_shielded_data.value_balance, + Transaction::V5 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => sapling_shielded_data.value_balance, + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { + sapling_shielded_data: None, + .. + } + | Transaction::V5 { + sapling_shielded_data: None, + .. + } => Amount::zero(), + }; + + ValueBalance::from_sapling_amount(sapling_value_balance) + } + + /// Modify the `value_balance` field from the `sapling::ShieldedData` in this transaction, + /// regardless of version. + /// + /// See `sapling_value_balance` for details. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn sapling_value_balance_mut(&mut self) -> Option<&mut Amount> { + match self { + Transaction::V4 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Some(&mut sapling_shielded_data.value_balance), + Transaction::V5 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Some(&mut sapling_shielded_data.value_balance), + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { + sapling_shielded_data: None, + .. + } + | Transaction::V5 { + sapling_shielded_data: None, + .. + } => None, + } + } + + /// Return the orchard value balance, + /// the change in the transaction value pool due to orchard [`Action`]s. + /// + /// Returns the `valueBalanceOrchard` field in this transaction. + /// + /// Positive values are added to this transaction's value pool, + /// and removed from the orchard chain value pool. + /// Negative values are removed from this transaction, + /// and added to orchard pool. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + fn orchard_value_balance(&self) -> ValueBalance { + let orchard_value_balance = self + .orchard_shielded_data() + .map(|shielded_data| shielded_data.value_balance) + .unwrap_or_else(Amount::zero); + + ValueBalance::from_orchard_amount(orchard_value_balance) + } + + /// Modify the `value_balance` field from the `orchard::ShieldedData` in this transaction, + /// regardless of version. + /// + /// See `orchard_value_balance` for details. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount> { + self.orchard_shielded_data_mut() + .map(|shielded_data| &mut shielded_data.value_balance) + } + + /// Get the value balances for this transaction, + /// using the transparent outputs spent in this transaction. + /// + /// See `value_balance` for details. + pub(crate) fn value_balance_from_outputs( + &self, + outputs: &HashMap, + ) -> Result, ValueBalanceError> { + self.transparent_value_balance_from_outputs(outputs)? + + self.sprout_value_balance()? + + self.sapling_value_balance() + + self.orchard_value_balance() + } + + /// Get the value balances for this transaction. + /// These are the changes in the transaction value pool, + /// split up into transparent, sprout, sapling, and orchard values. + /// + /// Calculated as the sum of the inputs and outputs from each pool, + /// or the sum of the value balances from each pool. + /// + /// Positive values are added to this transaction's value pool, + /// and removed from the corresponding chain value pool. + /// Negative values are removed from this transaction, + /// and added to the corresponding pool. + /// + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + /// + /// `utxos` must contain the utxos of every input in the transaction, + /// including UTXOs created by earlier transactions in this block. + /// + /// Note: the chain value pool has the opposite sign to the transaction + /// value pool. 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) + ) -> Result, ValueBalanceError> { + self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone())) } } diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index ce943cab6..235484d58 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -1,7 +1,9 @@ //! Arbitrary data generation for transaction proptests use std::{ + collections::HashMap, convert::{TryFrom, TryInto}, + ops::Neg, sync::Arc, }; @@ -9,7 +11,7 @@ use chrono::{TimeZone, Utc}; use proptest::{arbitrary::any, array, collection::vec, option, prelude::*}; use crate::{ - amount::Amount, + amount::{self, Amount, NegativeAllowed, NonNegative}, at_least_one, block, orchard, parameters::{Network, NetworkUpgrade}, primitives::{ @@ -18,7 +20,10 @@ use crate::{ }, sapling::{self, AnchorVariant, PerSpendAnchor, SharedAnchor}, serialization::{ZcashDeserialize, ZcashDeserializeInto}, - sprout, transparent, LedgerState, + sprout, + transparent::{self, outputs_from_utxos, utxos_from_ordered_utxos}, + value_balance::ValueBalanceError, + LedgerState, }; use itertools::Itertools; @@ -31,6 +36,8 @@ use super::{FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction}; /// for debugging. pub const MAX_ARBITRARY_ITEMS: usize = 4; +// TODO: if needed, fixup transaction outputs +// (currently 0..=9 outputs, consensus rules require 1..) impl Transaction { /// Generate a proptest strategy for V1 Transactions pub fn v1_strategy(ledger_state: LedgerState) -> BoxedStrategy { @@ -162,6 +169,7 @@ impl Transaction { mut ledger_state: LedgerState, len: usize, ) -> BoxedStrategy>> { + // TODO: fixup coinbase miner subsidy let coinbase = Transaction::arbitrary_with(ledger_state).prop_map(Arc::new); ledger_state.has_coinbase = false; let remainder = vec( @@ -176,6 +184,214 @@ impl Transaction { }) .boxed() } + + /// Apply `f` to the transparent output, `v_sprout_new`, and `v_sprout_old` values + /// in this transaction, regardless of version. + pub fn for_each_value_mut(&mut self, mut f: F) + where + F: FnMut(&mut Amount), + { + for output_value in self.output_values_mut() { + f(output_value); + } + + for sprout_added_value in self.output_values_to_sprout_mut() { + f(sprout_added_value); + } + for sprout_removed_value in self.input_values_from_sprout_mut() { + f(sprout_removed_value); + } + } + + /// Apply `f` to the sapling value balance and orchard value balance + /// in this transaction, regardless of version. + pub fn for_each_value_balance_mut(&mut self, mut f: F) + where + F: FnMut(&mut Amount), + { + if let Some(sapling_value_balance) = self.sapling_value_balance_mut() { + f(sapling_value_balance); + } + + if let Some(orchard_value_balance) = self.orchard_value_balance_mut() { + f(orchard_value_balance); + } + } + + /// Fixup non-coinbase transparent values and shielded value balances, + /// so that this transaction passes the "remaining transaction value pool" check. + /// + /// Returns the remaining transaction value. + /// + /// `outputs` must contain all the [`Output`]s spent in this block. + /// + /// Currently, this code almost always leaves some remaining value in the + /// transaction value pool. + /// + /// # Panics + /// + /// If any spent [`Output`] is missing from `outpoints`. + // + // TODO: split this method up, after we've implemented chain value balance adjustments + // + // TODO: take an extra arbitrary bool, which selects between zero and non-zero + // remaining value in the transaction value pool + pub fn fix_remaining_value( + &mut self, + outputs: &HashMap, + ) -> Result, ValueBalanceError> { + // Temporarily make amounts smaller, so the total never overflows MAX_MONEY + // in Zebra's ~100-block chain tests. (With up to 7 values per transaction, + // and 3 transactions per block.) + // TODO: replace this scaling with chain value balance adjustments + fn scale_to_avoid_overflow(amount: &mut Amount) + where + Amount: Copy, + { + *amount = (*amount / 10_000).expect("divisor is not zero"); + } + + self.for_each_value_mut(scale_to_avoid_overflow); + self.for_each_value_balance_mut(scale_to_avoid_overflow); + + if self.is_coinbase() { + // TODO: if needed, fixup coinbase: + // - miner subsidy + // - founders reward or funding streams (hopefully not?) + // - remaining transaction value + return Ok(Amount::zero()); + } + + // calculate the total input value + + let transparent_inputs = self + .inputs() + .iter() + .map(|input| input.value_from_outputs(outputs)) + .sum::, amount::Error>>() + .map_err(ValueBalanceError::Transparent)?; + // TODO: fix callers with invalid values, maybe due to cached outputs? + //.expect("chain is limited to MAX_MONEY"); + + let sprout_inputs = self + .input_values_from_sprout() + .sum::, amount::Error>>() + .expect("chain is limited to MAX_MONEY"); + + // positive value balances add to the transaction value pool + let sapling_input = self + .sapling_value_balance() + .sapling_amount() + .constrain::() + .unwrap_or_else(|_| Amount::zero()); + + let orchard_input = self + .orchard_value_balance() + .orchard_amount() + .constrain::() + .unwrap_or_else(|_| Amount::zero()); + + let mut remaining_input_value = + (transparent_inputs + sprout_inputs + sapling_input + orchard_input) + .expect("chain is limited to MAX_MONEY"); + + // assign remaining input value to outputs, + // zeroing any outputs that would exceed the input value + + for output_value in self.output_values_mut() { + if remaining_input_value >= *output_value { + remaining_input_value = (remaining_input_value - *output_value) + .expect("input >= output so result is always non-negative"); + } else { + *output_value = Amount::zero(); + } + } + + for output_value in self.output_values_to_sprout_mut() { + if remaining_input_value >= *output_value { + remaining_input_value = (remaining_input_value - *output_value) + .expect("input >= output so result is always non-negative"); + } else { + *output_value = Amount::zero(); + } + } + + if let Some(value_balance) = self.sapling_value_balance_mut() { + if let Ok(output_value) = value_balance.neg().constrain::() { + if remaining_input_value >= output_value { + remaining_input_value = (remaining_input_value - output_value) + .expect("input >= output so result is always non-negative"); + } else { + *value_balance = Amount::zero(); + } + } + } + + if let Some(value_balance) = self.orchard_value_balance_mut() { + if let Ok(output_value) = value_balance.neg().constrain::() { + if remaining_input_value >= output_value { + remaining_input_value = (remaining_input_value - output_value) + .expect("input >= output so result is always non-negative"); + } else { + *value_balance = Amount::zero(); + } + } + } + + // check our calculations are correct + let remaining_transaction_value = self + .value_balance_from_outputs(outputs) + .expect("chain is limited to MAX_MONEY") + .remaining_transaction_value() + .unwrap_or_else(|err| { + panic!( + "unexpected remaining transaction value: {:?}, \ + calculated remaining input value: {:?}", + err, remaining_input_value + ) + }); + assert_eq!( + remaining_input_value, + remaining_transaction_value, + "fix_remaining_value and remaining_transaction_value calculated different remaining values" + ); + + Ok(remaining_transaction_value) + } + + /// Fixup non-coinbase transparent values and shielded value balances. + /// See `fix_remaining_value` for details. + /// + /// `utxos` must contain all the [`Utxo`]s spent in this block. + /// + /// # Panics + /// + /// If any spent [`Utxo`] is missing from `utxos`. + #[allow(dead_code)] + pub fn fix_remaining_value_from_utxos( + &mut self, + utxos: &HashMap, + ) -> Result, ValueBalanceError> { + self.fix_remaining_value(&outputs_from_utxos(utxos.clone())) + } + + /// Fixup non-coinbase transparent values and shielded value balances. + /// See `fix_remaining_value` for details. + /// + /// `ordered_utxos` must contain all the [`OrderedUtxo`]s spent in this block. + /// + /// # Panics + /// + /// If any spent [`OrderedUtxo`] is missing from `ordered_utxos`. + #[allow(dead_code)] + pub fn fix_remaining_value_from_ordered_utxos( + &mut self, + ordered_utxos: &HashMap, + ) -> Result, ValueBalanceError> { + self.fix_remaining_value(&outputs_from_utxos(utxos_from_ordered_utxos( + ordered_utxos.clone(), + ))) + } } impl Arbitrary for Memo { diff --git a/zebra-chain/src/transaction/joinsplit.rs b/zebra-chain/src/transaction/joinsplit.rs index 8cca631ab..22c3b683a 100644 --- a/zebra-chain/src/transaction/joinsplit.rs +++ b/zebra-chain/src/transaction/joinsplit.rs @@ -66,12 +66,15 @@ impl JoinSplitData

{ .flat_map(|joinsplit| joinsplit.nullifiers.iter()) } - /// Calculate and return the value balance for the joinsplits. + /// Return the sprout value balance, + /// the change in the transaction value pool due to sprout [`JoinSplit`]s. /// - /// Needed to calculate the sprout value balance. + /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions + /// + /// See [`Transaction::sprout_value_balance`] for details. pub fn value_balance(&self) -> Result { self.joinsplits() - .flat_map(|j| j.vpub_old.constrain() - j.vpub_new.constrain()?) + .flat_map(|j| j.vpub_new.constrain() - j.vpub_old.constrain()?) .sum() } diff --git a/zebra-chain/src/transparent.rs b/zebra-chain/src/transparent.rs index 12528289b..5e93dada8 100644 --- a/zebra-chain/src/transparent.rs +++ b/zebra-chain/src/transparent.rs @@ -15,6 +15,8 @@ pub use utxo::{ OrderedUtxo, Utxo, }; +pub(crate) use utxo::outputs_from_utxos; + #[cfg(any(test, feature = "proptest-impl"))] pub(crate) use utxo::new_transaction_ordered_outputs; @@ -27,11 +29,11 @@ mod arbitrary; mod prop; use crate::{ - amount::{Amount, NegativeAllowed, NonNegative}, + amount::{Amount, NonNegative}, block, transaction, }; -use std::collections::HashMap; +use std::{collections::HashMap, iter}; /// Arbitrary data inserted by miners into a coinbase transaction. #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -142,22 +144,77 @@ impl Input { } } - /// Get the value spent by this input. + /// Get the value spent by this input, by looking up its [`Outpoint`] in `outputs`. + /// See `value` for details. + /// + /// # Panics + /// + /// If the provided `Output`s don't have this input's `Outpoint`. + pub(crate) fn value_from_outputs( + &self, + outputs: &HashMap, + ) -> Amount { + match self { + Input::PrevOut { outpoint, .. } => { + outputs + .get(outpoint) + .unwrap_or_else(|| { + panic!( + "provided Outputs (length {:?}) don't have spent {:?}", + outputs.len(), + outpoint + ) + }) + .value + } + Input::Coinbase { .. } => Amount::zero(), + } + } + + /// Get the value spent by this input, by looking up its [`Outpoint`] in `utxos`. + /// /// 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") + /// If the provided `Utxo`s don't have this input's `Outpoint`. + pub fn value(&self, utxos: &HashMap) -> Amount { + if let Some(outpoint) = self.outpoint() { + // look up the specific Output and convert it to the expected format + let output = utxos + .get(&outpoint) + .expect("provided Utxos don't have spent OutPoint") .output - .value - .constrain() - .expect("conversion from NonNegative to NegativeAllowed is always valid"), - Input::Coinbase { .. } => Amount::zero(), + .clone(); + self.value_from_outputs(&iter::once((outpoint, output)).collect()) + } else { + // coinbase inputs don't need any UTXOs + self.value_from_outputs(&HashMap::new()) + } + } + + /// Get the value spent by this input, by looking up its [`Outpoint`] in `ordered_utxos`. + /// See `value` for details. + /// + /// # Panics + /// + /// If the provided `OrderedUtxo`s don't have this input's `Outpoint`. + pub fn value_from_ordered_utxos( + &self, + ordered_utxos: &HashMap, + ) -> Amount { + if let Some(outpoint) = self.outpoint() { + // look up the specific Output and convert it to the expected format + let output = ordered_utxos + .get(&outpoint) + .expect("provided Utxos don't have spent OutPoint") + .utxo + .output + .clone(); + self.value_from_outputs(&iter::once((outpoint, output)).collect()) + } else { + // coinbase inputs don't need any UTXOs + self.value_from_outputs(&HashMap::new()) } } } @@ -188,9 +245,7 @@ pub struct Output { 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 { + pub fn value(&self) -> Amount { self.value - .constrain() - .expect("conversion from NonNegative to NegativeAllowed is always valid") } } diff --git a/zebra-chain/src/transparent/utxo.rs b/zebra-chain/src/transparent/utxo.rs index bb6f840ba..99c5586b3 100644 --- a/zebra-chain/src/transparent/utxo.rs +++ b/zebra-chain/src/transparent/utxo.rs @@ -95,7 +95,17 @@ pub fn utxos_from_ordered_utxos( ) -> HashMap { ordered_utxos .into_iter() - .map(|(out_point, ordered_utxo)| (out_point, ordered_utxo.utxo)) + .map(|(outpoint, ordered_utxo)| (outpoint, ordered_utxo.utxo)) + .collect() +} + +/// Compute an index of [`Output`]s, given an index of [`Utxo`]s. +pub(crate) fn outputs_from_utxos( + utxos: HashMap, +) -> HashMap { + utxos + .into_iter() + .map(|(outpoint, utxo)| (outpoint, utxo.output)) .collect() } diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index bfaf17cb6..eea54665b 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -1,8 +1,15 @@ //! A type that can hold the four types of Zcash value pools. -use crate::amount::{Amount, Constraint, Error, NegativeAllowed, NonNegative}; +use crate::{ + amount::{self, Amount, Constraint, NegativeAllowed, NonNegative}, + block::Block, + transparent, +}; -use std::convert::TryInto; +use std::{borrow::Borrow, collections::HashMap, convert::TryInto}; + +#[cfg(any(test, feature = "proptest-impl"))] +use crate::transaction::Transaction; #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; @@ -13,7 +20,7 @@ mod tests; use ValueBalanceError::*; /// An amount spread between different Zcash pools. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ValueBalance { transparent: Amount, sprout: Amount, @@ -31,12 +38,80 @@ where /// This rule applies to Block and Mempool transactions. /// /// [Consensus rule]: https://zips.z.cash/protocol/protocol.pdf#transactions - pub fn remaining_transaction_value(&self) -> Result, Error> { - // Calculated in Zebra by negating the sum of the transparent, sprout, - // sapling, and orchard value balances as specified in + /// Design: https://github.com/ZcashFoundation/zebra/blob/main/book/src/dev/rfcs/0012-value-pools.md#definitions + // + // TODO: move this method to Transaction, so it can handle coinbase transactions as well? + pub fn remaining_transaction_value(&self) -> Result, amount::Error> { + // Calculated by summing the transparent, sprout, sapling, and orchard value balances, + // as specified in: // https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions - let value = (self.transparent + self.sprout + self.sapling + self.orchard)?; - (-(value)).constrain::() + (self.transparent + self.sprout + self.sapling + self.orchard)?.constrain::() + } + + /// Update this value balance with the chain value pool change from `block`. + /// + /// `utxos` must contain the [`Utxo`]s of every input in this block, + /// including UTXOs created by earlier transactions in this block. + /// + /// Note: the chain value pool has the opposite sign to the transaction + /// value pool. + /// + /// See [`Block::chain_value_pool_change`] for details. + pub fn update_with_block( + &mut self, + block: impl Borrow, + utxos: &HashMap, + ) -> Result<(), ValueBalanceError> { + let chain_value_pool_change = block.borrow().chain_value_pool_change(utxos)?; + + self.update_with_chain_value_pool_change(chain_value_pool_change) + } + + /// Update this value balance with the chain value pool change from `transaction`. + /// + /// `outputs` must contain the [`Output`]s of every input in this transaction, + /// including UTXOs created by earlier transactions in its block. + /// + /// Note: the chain value pool has the opposite sign to the transaction + /// value pool. + /// + /// See [`Block::chain_value_pool_change`] and [`Transaction::value_balance`] + /// for details. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn update_with_transaction( + &mut self, + transaction: impl Borrow, + utxos: &HashMap, + ) -> Result<(), ValueBalanceError> { + use std::ops::Neg; + + // the chain pool (unspent outputs) has the opposite sign to + // transaction value balances (inputs - outputs) + let chain_value_pool_change = transaction + .borrow() + .value_balance_from_outputs(utxos)? + .neg(); + + self.update_with_chain_value_pool_change(chain_value_pool_change) + } + + /// Update this value balance with a chain value pool change. + /// + /// Note: the chain value pool has the opposite sign to the transaction + /// value pool. + /// + /// See `update_with_block` for details. + fn update_with_chain_value_pool_change( + &mut self, + chain_value_pool_change: ValueBalance, + ) -> Result<(), ValueBalanceError> { + let mut current_value_pool = self + .constrain::() + .expect("conversion from NonNegative to NegativeAllowed is always valid"); + current_value_pool = (current_value_pool + chain_value_pool_change)?; + *self = current_value_pool.constrain()?; + + Ok(()) } /// Creates a [`ValueBalance`] from the given transparent amount. @@ -204,16 +279,16 @@ where /// Errors that can be returned when validating a [`ValueBalance`] pub enum ValueBalanceError { /// transparent amount error {0} - Transparent(Error), + Transparent(amount::Error), /// sprout amount error {0} - Sprout(Error), + Sprout(amount::Error), /// sapling amount error {0} - Sapling(Error), + Sapling(amount::Error), /// orchard amount error {0} - Orchard(Error), + Orchard(amount::Error), } impl std::ops::Add for ValueBalance @@ -230,6 +305,7 @@ where }) } } + impl std::ops::Add> for Result, ValueBalanceError> where C: Constraint, @@ -240,6 +316,29 @@ where } } +impl std::ops::Add, ValueBalanceError>> for ValueBalance +where + C: Constraint, +{ + type Output = Result, ValueBalanceError>; + + fn add(self, rhs: Result, ValueBalanceError>) -> Self::Output { + self + rhs? + } +} + +impl std::ops::AddAssign> for Result, ValueBalanceError> +where + ValueBalance: Copy, + C: Constraint, +{ + fn add_assign(&mut self, rhs: ValueBalance) { + if let Ok(lhs) = *self { + *self = lhs + rhs; + } + } +} + impl std::ops::Sub for ValueBalance where C: Constraint, @@ -264,6 +363,29 @@ where } } +impl std::ops::Sub, ValueBalanceError>> for ValueBalance +where + C: Constraint, +{ + type Output = Result, ValueBalanceError>; + + fn sub(self, rhs: Result, ValueBalanceError>) -> Self::Output { + self - rhs? + } +} + +impl std::ops::SubAssign> for Result, ValueBalanceError> +where + ValueBalance: Copy, + C: Constraint, +{ + fn sub_assign(&mut self, rhs: ValueBalance) { + if let Ok(lhs) = *self { + *self = lhs - rhs; + } + } +} + impl std::iter::Sum> for Result, ValueBalanceError> where C: Constraint + Copy, diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 878ae57ad..7dfb22411 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -125,7 +125,7 @@ pub fn disabled_add_to_sprout_pool( if height >= canopy_activation_height { let zero = Amount::::try_from(0).expect("an amount of 0 is always valid"); - let tx_sprout_pool = tx.sprout_pool_added_values(); + let tx_sprout_pool = tx.output_values_to_sprout(); for vpub_old in tx_sprout_pool { if *vpub_old != zero { return Err(TransactionError::DisabledAddToSproutPool); diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 1b72f05fa..7fadfb951 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -1006,7 +1006,7 @@ fn add_to_sprout_pool_after_nu() { // should fail the check. assert!(block.transactions[4].joinsplit_count() > 0); let vpub_old: Amount = block.transactions[4] - .sprout_pool_added_values() + .output_values_to_sprout() .fold(zero, |acc, &x| (acc + x).unwrap()); assert!(vpub_old > zero); @@ -1019,7 +1019,7 @@ fn add_to_sprout_pool_after_nu() { // should pass the check. assert!(block.transactions[7].joinsplit_count() > 0); let vpub_old: Amount = block.transactions[7] - .sprout_pool_added_values() + .output_values_to_sprout() .fold(zero, |acc, &x| (acc + x).unwrap()); assert_eq!(vpub_old, zero); diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 4127a7779..b58afb299 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -4,7 +4,8 @@ use chrono::{DateTime, Utc}; use thiserror::Error; use zebra_chain::{ - block, orchard, sapling, sprout, transparent, work::difficulty::CompactDifficulty, + amount, block, orchard, sapling, sprout, transparent, value_balance::ValueBalanceError, + work::difficulty::CompactDifficulty, }; use crate::constants::MIN_TRANSPARENT_COINBASE_MATURITY; @@ -35,12 +36,12 @@ impl From for CloneError { pub type BoxError = Box; /// An error describing the reason a block could not be committed to the state. -#[derive(Debug, Error, PartialEq, Eq)] +#[derive(Debug, Error, Clone, PartialEq, Eq)] #[error("block is not contextually valid")] pub struct CommitBlockError(#[from] ValidateContextError); /// An error describing why a block failed contextual validation. -#[derive(Debug, Error, PartialEq, Eq)] +#[derive(Debug, Error, Clone, PartialEq, Eq)] #[non_exhaustive] #[allow(missing_docs)] pub enum ValidateContextError { @@ -140,11 +141,43 @@ pub enum ValidateContextError { in_finalized_state: bool, }, - #[error("remaining value in the transparent transaction value pool MUST be nonnegative: {transaction_hash:?}, in finalized state: {in_finalized_state:?}")] + #[error( + "the remaining value in the transparent transaction value pool MUST be nonnegative: \ + {amount_error:?}, {height:?}, index in block: {tx_index_in_block:?}, \ + {transaction_hash:?}" + )] #[non_exhaustive] - InvalidRemainingTransparentValue { + NegativeRemainingTransactionValue { + amount_error: amount::Error, + height: block::Height, + tx_index_in_block: usize, + transaction_hash: zebra_chain::transaction::Hash, + }, + + #[error( + "error calculating the remaining value in the transaction value pool: \ + {amount_error:?}, {height:?}, index in block: {tx_index_in_block:?}, \ + {transaction_hash:?}" + )] + #[non_exhaustive] + CalculateRemainingTransactionValue { + amount_error: amount::Error, + height: block::Height, + tx_index_in_block: usize, + transaction_hash: zebra_chain::transaction::Hash, + }, + + #[error( + "error calculating value balances for the remaining value in the transaction value pool: \ + {value_balance_error:?}, {height:?}, index in block: {tx_index_in_block:?}, \ + {transaction_hash:?}" + )] + #[non_exhaustive] + CalculateTransactionValueBalances { + value_balance_error: ValueBalanceError, + height: block::Height, + tx_index_in_block: usize, transaction_hash: zebra_chain::transaction::Hash, - in_finalized_state: bool, }, #[error("error in Sapling note commitment tree")] diff --git a/zebra-state/src/service/check/tests/utxo.rs b/zebra-state/src/service/check/tests/utxo.rs index 0da2e53f7..a16281156 100644 --- a/zebra-state/src/service/check/tests/utxo.rs +++ b/zebra-state/src/service/check/tests/utxo.rs @@ -160,7 +160,7 @@ proptest! { .expect("block should deserialize"); // create an output - let output_transaction = transaction_v4_with_transparent_data([], [output.0]); + let output_transaction = transaction_v4_with_transparent_data([], [], [output.0.clone()]); // create a spend let expected_outpoint = transparent::OutPoint { @@ -168,7 +168,11 @@ proptest! { index: 0, }; prevout_input.set_outpoint(expected_outpoint); - let spend_transaction = transaction_v4_with_transparent_data([prevout_input.0], []); + let spend_transaction = transaction_v4_with_transparent_data( + [prevout_input.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); @@ -247,7 +251,7 @@ proptest! { let TestState { mut state, block1, .. - } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output); + } = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output); let previous_mem = state.mem.clone(); let expected_outpoint = transparent::OutPoint { @@ -256,7 +260,11 @@ proptest! { }; prevout_input.set_outpoint(expected_outpoint); - let spend_transaction = transaction_v4_with_transparent_data([prevout_input.0], []); + let spend_transaction = transaction_v4_with_transparent_data( + [prevout_input.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); @@ -329,7 +337,7 @@ proptest! { .zcash_deserialize_into::() .expect("block should deserialize"); - let output_transaction = transaction_v4_with_transparent_data([], [output.0]); + let output_transaction = transaction_v4_with_transparent_data([], [], [output.0.clone()]); let expected_outpoint = transparent::OutPoint { hash: output_transaction.hash(), @@ -338,8 +346,11 @@ proptest! { prevout_input1.set_outpoint(expected_outpoint); prevout_input2.set_outpoint(expected_outpoint); - let spend_transaction = - transaction_v4_with_transparent_data([prevout_input1.0, prevout_input2.0], []); + let spend_transaction = transaction_v4_with_transparent_data( + [prevout_input1.0, prevout_input2.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); @@ -390,7 +401,7 @@ proptest! { let TestState { mut state, block1, .. - } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output); + } = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output); let previous_mem = state.mem.clone(); let expected_outpoint = transparent::OutPoint { @@ -400,8 +411,11 @@ proptest! { prevout_input1.set_outpoint(expected_outpoint); prevout_input2.set_outpoint(expected_outpoint); - let spend_transaction = - transaction_v4_with_transparent_data([prevout_input1.0, prevout_input2.0], []); + let spend_transaction = transaction_v4_with_transparent_data( + [prevout_input1.0, prevout_input2.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); @@ -463,7 +477,7 @@ proptest! { let TestState { mut state, block1, .. - } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output); + } = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output); let previous_mem = state.mem.clone(); let expected_outpoint = transparent::OutPoint { @@ -473,8 +487,16 @@ proptest! { prevout_input1.set_outpoint(expected_outpoint); prevout_input2.set_outpoint(expected_outpoint); - let spend_transaction1 = transaction_v4_with_transparent_data([prevout_input1.0], []); - let spend_transaction2 = transaction_v4_with_transparent_data([prevout_input2.0], []); + let spend_transaction1 = transaction_v4_with_transparent_data( + [prevout_input1.0], + [(expected_outpoint, output.0.clone())], + [] + ); + let spend_transaction2 = transaction_v4_with_transparent_data( + [prevout_input2.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); @@ -548,7 +570,7 @@ proptest! { let TestState { mut state, block1, .. - } = new_state_with_mainnet_transparent_data([], [output.0], use_finalized_state_output); + } = new_state_with_mainnet_transparent_data([], [], [output.0.clone()], use_finalized_state_output); let mut previous_mem = state.mem.clone(); let expected_outpoint = transparent::OutPoint { @@ -558,8 +580,16 @@ proptest! { prevout_input1.set_outpoint(expected_outpoint); prevout_input2.set_outpoint(expected_outpoint); - let spend_transaction1 = transaction_v4_with_transparent_data([prevout_input1.0], []); - let spend_transaction2 = transaction_v4_with_transparent_data([prevout_input2.0], []); + let spend_transaction1 = transaction_v4_with_transparent_data( + [prevout_input1.0], + [(expected_outpoint, output.0.clone())], + [] + ); + let spend_transaction2 = transaction_v4_with_transparent_data( + [prevout_input2.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transactions to a version that the non-finalized state will accept block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); @@ -668,6 +698,7 @@ proptest! { /// is rejected by state contextual validation. #[test] fn reject_missing_transparent_spend( + unused_output in TypeNameToDebug::::arbitrary(), prevout_input in TypeNameToDebug::::arbitrary_with(None), ) { zebra_test::init(); @@ -677,7 +708,12 @@ proptest! { .expect("block should deserialize"); let expected_outpoint = prevout_input.outpoint().unwrap(); - let spend_transaction = transaction_v4_with_transparent_data([prevout_input.0], []); + let spend_transaction = transaction_v4_with_transparent_data( + [prevout_input.0], + // provide an fake spent output for value fixups + [(expected_outpoint, unused_output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); @@ -725,7 +761,7 @@ proptest! { .expect("block should deserialize"); // create an output - let output_transaction = transaction_v4_with_transparent_data([], [output.0]); + let output_transaction = transaction_v4_with_transparent_data([], [], [output.0.clone()]); // create a spend let expected_outpoint = transparent::OutPoint { @@ -733,7 +769,11 @@ proptest! { index: 0, }; prevout_input.set_outpoint(expected_outpoint); - let spend_transaction = transaction_v4_with_transparent_data([prevout_input.0], []); + let spend_transaction = transaction_v4_with_transparent_data( + [prevout_input.0], + [(expected_outpoint, output.0)], + [] + ); // convert the coinbase transaction to a version that the non-finalized state will accept block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); @@ -785,6 +825,7 @@ struct TestState { /// Also returns the finalized genesis block itself. fn new_state_with_mainnet_transparent_data( inputs: impl IntoIterator, + spent_outputs: impl IntoIterator, outputs: impl IntoIterator, use_finalized_state: bool, ) -> TestState { @@ -801,7 +842,7 @@ fn new_state_with_mainnet_transparent_data( .try_into() .expect("unexpectedly large output iterator"); - let transaction = transaction_v4_with_transparent_data(inputs, outputs); + let transaction = transaction_v4_with_transparent_data(inputs, spent_outputs, outputs); let transaction_hash = transaction.hash(); let expected_outpoints = (0..outputs_len).map(|index| transparent::OutPoint { @@ -836,7 +877,15 @@ fn new_state_with_mainnet_transparent_data( let commit_result = state.validate_and_commit(block1.clone()); // the block was committed - assert_eq!(commit_result, Ok(())); + assert_eq!( + commit_result, + Ok(()), + "unexpected invalid block 1, modified with generated transactions: \n\ + converted coinbase: {:?} \n\ + generated non-coinbase: {:?}", + block1.block.transactions[0], + block1.block.transactions[1], + ); assert_eq!(Some((Height(1), block1.hash)), state.best_tip()); // the block data is in the non-finalized state @@ -866,24 +915,30 @@ fn new_state_with_mainnet_transparent_data( } } -/// Return a `Transaction::V4`, using transparent `inputs` and `outputs`, +/// Return a `Transaction::V4`, using transparent `inputs` and their `spent_outputs`, +/// and newly created `outputs`. /// /// Other fields have empty or default values. fn transaction_v4_with_transparent_data( inputs: impl IntoIterator, + spent_outputs: impl IntoIterator, outputs: impl IntoIterator, ) -> Transaction { let inputs: Vec<_> = inputs.into_iter().collect(); let outputs: Vec<_> = outputs.into_iter().collect(); - // do any fixups here, if required - - Transaction::V4 { + let mut transaction = Transaction::V4 { inputs, outputs, lock_time: LockTime::min_lock_time(), expiry_height: Height(0), joinsplit_data: None, sapling_shielded_data: None, - } + }; + + // do required fixups, but ignore any errors, + // because we're not checking all the consensus rules here + let _ = transaction.fix_remaining_value(&spent_outputs.into_iter().collect()); + + transaction } diff --git a/zebra-state/src/service/check/utxo.rs b/zebra-state/src/service/check/utxo.rs index e24f3c073..d402b551c 100644 --- a/zebra-state/src/service/check/utxo.rs +++ b/zebra-state/src/service/check/utxo.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use zebra_chain::{ - block, + amount, block, transparent::{self, CoinbaseSpendRestriction::*}, }; @@ -86,6 +86,8 @@ pub fn transparent_spend( } } + remaining_transaction_value(prepared, &block_spends)?; + Ok(block_spends) } @@ -208,7 +210,14 @@ pub fn transparent_coinbase_spend( /// Reject negative remaining transaction value. /// -/// Consensus rule: The remaining value in the transparent transaction value pool MUST be nonnegative. +/// "As in Bitcoin, the remaining value in the transparent transaction value pool +/// of a non-coinbase transaction is available to miners as a fee. +/// +/// The remaining value in the transparent transaction value pool of a +/// coinbase transaction is destroyed. +/// +/// 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)] @@ -216,8 +225,8 @@ 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. + for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() { + // TODO: check coinbase transaction remaining value (#338, #1162) if transaction.is_coinbase() { continue; } @@ -227,15 +236,33 @@ pub fn remaining_transaction_value( 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(amount_error @ amount::Error::Constraint { .. }) + if amount_error.invalid_value() < 0 => + { + Err(ValidateContextError::NegativeRemainingTransactionValue { + amount_error, + height: prepared.height, + tx_index_in_block, + transaction_hash: prepared.transaction_hashes[tx_index_in_block], + }) + } + Err(amount_error) => { + Err(ValidateContextError::CalculateRemainingTransactionValue { + amount_error, + height: prepared.height, + tx_index_in_block, + transaction_hash: prepared.transaction_hashes[tx_index_in_block], + }) + } }, - Err(_) => Err(ValidateContextError::InvalidRemainingTransparentValue { - transaction_hash: transaction.hash(), - in_finalized_state: false, - }), + Err(value_balance_error) => { + Err(ValidateContextError::CalculateTransactionValueBalances { + value_balance_error, + height: prepared.height, + tx_index_in_block, + transaction_hash: prepared.transaction_hashes[tx_index_in_block], + }) + } }? }