Check remaining transaction value & make value balance signs match the spec (#2566)

* Make Amount arithmetic more generic

To modify generated amounts, we need some extra operations on `Amount`.

We also need to extend existing operations to both `NonNegative` and
`NegativeAllowed` amounts.

* Add a constrain method for ValueBalance

* Derive Eq for ValueBalance

* impl Neg for ValueBalance

* Make some Amount arithmetic expectations explicit

* Explain why we use i128 for multiplication

And expand the overflow error details.

* Expand Amount::sum error details

* Make amount::Error field order consistent

* Rename an amount::Error variant to Constraint, so it's clearer

* Add specific pool variants to ValueBalanceError

* Update coinbase remaining value consensus rule comment

This consensus rule was updated recently to include coinbase transactions,
but Zebra doesn't check block subsidy or miner fees yet.

* Add test methods for modifying transparent values and shielded value balances

* Temporarily set values and value balances to zero in proptests

In both generated chains and proptests that construct their own transactions.

Using zero values reduces value calculation and value check test coverage.
A future change will use non-zero values, and fix them so the check passes.

* Add extra fields to remaining transaction value errors

* Swap the transparent value balance sign to match shielded value balances

This makes the signs of all the chain value pools consistent.

* Use a NonNegative constraint for transparent values

This fix:
* makes the type signature match the consensus rules
* avoids having to write code to handle negative values

* Allocate total generated transaction input value to outputs

If there isn't enough input value for an output, set it to zero.

Temporarily reduce all generated values to avoid overflow.
(We'll remove this workaround when we calculate chain value balances.)

* Consistently use ValueBalanceError for ValueBalances

* Make the value balance signs match the spec

And rename and document methods so their signs are clearer.

* Convert amount::Errors to specific pool ValueBalanceErrors

* Move some error changes to the next PR

* Add extra info to remaining transaction value errors (#2585)

* Distinguish between overflow and negative remaining transaction value errors

And make some error types cloneable.

* Add methods for updating chain value pools (#2586)

* Move amount::test to amount::tests:vectors

* Make ValueBalance traits more consistent with Amount

- implement Add and Sub variants with Result and Assign
- derive Hash

* Clarify some comments and expects

* Create ValueBalance update methods for blocks and transactions

Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
This commit is contained in:
teor 2021-08-10 03:22:26 +10:00 committed by GitHub
parent fc68240fa0
commit f09f2a9022
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1453 additions and 622 deletions

View File

@ -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<NonNegative> {
Ok(reader.read_u64::<LittleEndian>()?.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::<NonNegative>::try_from(1)?;
let zero: Amount<NegativeAllowed> = 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::<LittleEndian>(big)
.expect("unexpected serialization failure: vec should be infalliable");
let mut neg_bytes = Vec::new();
(&mut neg_bytes)
.write_i64::<LittleEndian>(neg)
.expect("unexpected serialization failure: vec should be infalliable");
Amount::<NonNegative>::zcash_deserialize(big_bytes.as_slice())
.expect_err("deserialization should reject too large values");
Amount::<NegativeAllowed>::zcash_deserialize(big_bytes.as_slice())
.expect_err("deserialization should reject too large values");
Amount::<NonNegative>::zcash_deserialize(neg_bytes.as_slice())
.expect_err("NonNegative deserialization should reject negative values");
let amount: Amount<NegativeAllowed> = 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::<NonNegative>::try_from(1)?;
let another_one = Amount::<NonNegative>::try_from(1)?;
let zero: Amount<NonNegative> = Amount::zero();
let hash_set: HashSet<Amount<NonNegative>, RandomState> = [one].iter().cloned().collect();
assert_eq!(hash_set.len(), 1);
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
[one, one].iter().cloned().collect();
assert_eq!(hash_set.len(), 1, "Amount hashes are consistent");
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
[one, another_one].iter().cloned().collect();
assert_eq!(hash_set.len(), 1, "Amount hashes are by value");
let hash_set: HashSet<Amount<NonNegative>, 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::<NonNegative, NonNegative>()?;
ordering::<NonNegative, NegativeAllowed>()?;
ordering::<NegativeAllowed, NonNegative>()?;
ordering::<NegativeAllowed, NegativeAllowed>()?;
Ok(())
}
#[allow(clippy::eq_op)]
fn ordering<C1, C2>() -> Result<()>
where
C1: Constraint + Debug,
C2: Constraint + Debug,
{
let zero: Amount<C1> = Amount::zero();
let one = Amount::<C2>::try_from(1)?;
let another_one = Amount::<C1>::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::<NegativeAllowed>::try_from(-1)?;
let negative_two = Amount::<NegativeAllowed>::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::<Result<Amount, Error>>()?;
let sum_value: Amount = amounts.into_iter().sum::<Result<Amount, Error>>()?;
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::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
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::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
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<Amount> = std::iter::repeat(MAX_MONEY.try_into()?)
.take(times + 1)
.collect();
let sum_ref = amounts.iter().sum::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
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<NegativeAllowed> = (-MAX_MONEY).try_into()?;
let amounts: Vec<Amount<NegativeAllowed>> =
std::iter::repeat(neg_max_money).take(times + 1).collect();
let sum_ref = amounts.iter().sum::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
assert_eq!(sum_ref, sum_value);
assert_eq!(
sum_ref,
Err(Error::SumOverflow {
partial_sum: -4200000000000000,
remaining_items: 4391
})
);
Ok(())
}
}

View File

@ -1,3 +1,4 @@
//! Tests for amounts
mod prop;
mod vectors;

View File

@ -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::<NonNegative>::try_from(1)?;
let zero: Amount<NegativeAllowed> = 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::<LittleEndian>(big)
.expect("unexpected serialization failure: vec should be infalliable");
let mut neg_bytes = Vec::new();
(&mut neg_bytes)
.write_i64::<LittleEndian>(neg)
.expect("unexpected serialization failure: vec should be infalliable");
Amount::<NonNegative>::zcash_deserialize(big_bytes.as_slice())
.expect_err("deserialization should reject too large values");
Amount::<NegativeAllowed>::zcash_deserialize(big_bytes.as_slice())
.expect_err("deserialization should reject too large values");
Amount::<NonNegative>::zcash_deserialize(neg_bytes.as_slice())
.expect_err("NonNegative deserialization should reject negative values");
let amount: Amount<NegativeAllowed> = 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::<NonNegative>::try_from(1)?;
let another_one = Amount::<NonNegative>::try_from(1)?;
let zero: Amount<NonNegative> = Amount::zero();
let hash_set: HashSet<Amount<NonNegative>, RandomState> = [one].iter().cloned().collect();
assert_eq!(hash_set.len(), 1);
let hash_set: HashSet<Amount<NonNegative>, RandomState> = [one, one].iter().cloned().collect();
assert_eq!(hash_set.len(), 1, "Amount hashes are consistent");
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
[one, another_one].iter().cloned().collect();
assert_eq!(hash_set.len(), 1, "Amount hashes are by value");
let hash_set: HashSet<Amount<NonNegative>, 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::<NonNegative, NonNegative>()?;
ordering::<NonNegative, NegativeAllowed>()?;
ordering::<NegativeAllowed, NonNegative>()?;
ordering::<NegativeAllowed, NegativeAllowed>()?;
Ok(())
}
#[allow(clippy::eq_op)]
fn ordering<C1, C2>() -> Result<()>
where
C1: Constraint + Debug,
C2: Constraint + Debug,
{
let zero: Amount<C1> = Amount::zero();
let one = Amount::<C2>::try_from(1)?;
let another_one = Amount::<C1>::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::<NegativeAllowed>::try_from(-1)?;
let negative_two = Amount::<NegativeAllowed>::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::<Result<Amount, Error>>()?;
let sum_value: Amount = amounts.into_iter().sum::<Result<Amount, Error>>()?;
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::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
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::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
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<Amount> = std::iter::repeat(MAX_MONEY.try_into()?)
.take(times + 1)
.collect();
let sum_ref = amounts.iter().sum::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
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<NegativeAllowed> = (-MAX_MONEY).try_into()?;
let amounts: Vec<Amount<NegativeAllowed>> =
std::iter::repeat(neg_max_money).take(times + 1).collect();
let sum_ref = amounts.iter().sum::<Result<Amount, Error>>();
let sum_value = amounts.into_iter().sum::<Result<Amount, Error>>();
assert_eq!(sum_ref, sum_value);
assert_eq!(
sum_ref,
Err(Error::SumOverflow {
partial_sum: -4200000000000000,
remaining_items: 4391
})
);
Ok(())
}

View File

@ -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<transparent::OutPoint, transparent::Utxo>,
) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
self.transactions
let transaction_value_balance_total = self
.transactions
.iter()
.flat_map(|t| t.value_balance(utxos))
.sum()
.sum::<Result<ValueBalance<NegativeAllowed>, _>>()?;
Ok(transaction_value_balance_total.neg())
}
}

View File

@ -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) {

View File

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

View File

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

View File

@ -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<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
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<dyn Iterator<Item = &sprout::Nullifier> + '_> {
// 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<transparent::OutPoint, transparent::Utxo>,
) -> Result<ValueBalance<NegativeAllowed>, AmountError> {
let inputs = self.inputs();
let outputs = self.outputs();
let input_value_balance: Amount = inputs
outputs: &HashMap<transparent::OutPoint, transparent::Output>,
) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
let input_value = self
.inputs()
.iter()
.map(|i| i.value(utxos))
.sum::<Result<Amount, AmountError>>()?;
.map(|i| i.value_from_outputs(outputs))
.sum::<Result<Amount<NonNegative>, AmountError>>()
.map_err(ValueBalanceError::Transparent)?
.constrain()
.expect("conversion from NonNegative to NegativeAllowed is always valid");
let output_value_balance: Amount<NegativeAllowed> = outputs
let output_value = self
.outputs()
.iter()
.map(|o| o.value())
.sum::<Result<Amount, AmountError>>()?;
.sum::<Result<Amount<NonNegative>, 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<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.
#[allow(dead_code)]
fn transparent_value_balance(
&self,
utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
) -> Result<ValueBalance<NegativeAllowed>, 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<Item = &mut Amount<NonNegative>> {
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<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
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<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
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<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
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<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
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<ValueBalance<NegativeAllowed>, 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<NegativeAllowed> {
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<NegativeAllowed>> {
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<NegativeAllowed> {
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<NegativeAllowed>> {
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<transparent::OutPoint, transparent::Output>,
) -> Result<ValueBalance<NegativeAllowed>, 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<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)
) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone()))
}
}

View File

@ -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<Self> {
@ -162,6 +169,7 @@ impl Transaction {
mut ledger_state: LedgerState,
len: usize,
) -> BoxedStrategy<Vec<Arc<Self>>> {
// 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<F>(&mut self, mut f: F)
where
F: FnMut(&mut Amount<NonNegative>),
{
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<F>(&mut self, mut f: F)
where
F: FnMut(&mut Amount<NegativeAllowed>),
{
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<transparent::OutPoint, transparent::Output>,
) -> Result<Amount<NonNegative>, 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<C: amount::Constraint>(amount: &mut Amount<C>)
where
Amount<C>: 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::<Result<Amount<NonNegative>, 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::<Result<Amount<NonNegative>, 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::<NonNegative>()
.unwrap_or_else(|_| Amount::zero());
let orchard_input = self
.orchard_value_balance()
.orchard_amount()
.constrain::<NonNegative>()
.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::<NonNegative>() {
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::<NonNegative>() {
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<transparent::OutPoint, transparent::Utxo>,
) -> Result<Amount<NonNegative>, 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<transparent::OutPoint, transparent::OrderedUtxo>,
) -> Result<Amount<NonNegative>, ValueBalanceError> {
self.fix_remaining_value(&outputs_from_utxos(utxos_from_ordered_utxos(
ordered_utxos.clone(),
)))
}
}
impl Arbitrary for Memo {

View File

@ -66,12 +66,15 @@ impl<P: ZkSnarkProof> JoinSplitData<P> {
.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<Amount, Error> {
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()
}

View File

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

View File

@ -95,7 +95,17 @@ pub fn utxos_from_ordered_utxos(
) -> HashMap<transparent::OutPoint, Utxo> {
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<transparent::OutPoint, Utxo>,
) -> HashMap<transparent::OutPoint, transparent::Output> {
utxos
.into_iter()
.map(|(outpoint, utxo)| (outpoint, utxo.output))
.collect()
}

View File

@ -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<C> {
transparent: Amount<C>,
sprout: Amount<C>,
@ -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<Amount<NonNegative>, 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<NonNegative>, 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::<NonNegative>()
(self.transparent + self.sprout + self.sapling + self.orchard)?.constrain::<NonNegative>()
}
/// 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<Block>,
utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
) -> 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<Transaction>,
utxos: &HashMap<transparent::OutPoint, transparent::Output>,
) -> 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<NegativeAllowed>,
) -> Result<(), ValueBalanceError> {
let mut current_value_pool = self
.constrain::<NegativeAllowed>()
.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<C> std::ops::Add for ValueBalance<C>
@ -230,6 +305,7 @@ where
})
}
}
impl<C> std::ops::Add<ValueBalance<C>> for Result<ValueBalance<C>, ValueBalanceError>
where
C: Constraint,
@ -240,6 +316,29 @@ where
}
}
impl<C> std::ops::Add<Result<ValueBalance<C>, ValueBalanceError>> for ValueBalance<C>
where
C: Constraint,
{
type Output = Result<ValueBalance<C>, ValueBalanceError>;
fn add(self, rhs: Result<ValueBalance<C>, ValueBalanceError>) -> Self::Output {
self + rhs?
}
}
impl<C> std::ops::AddAssign<ValueBalance<C>> for Result<ValueBalance<C>, ValueBalanceError>
where
ValueBalance<C>: Copy,
C: Constraint,
{
fn add_assign(&mut self, rhs: ValueBalance<C>) {
if let Ok(lhs) = *self {
*self = lhs + rhs;
}
}
}
impl<C> std::ops::Sub for ValueBalance<C>
where
C: Constraint,
@ -264,6 +363,29 @@ where
}
}
impl<C> std::ops::Sub<Result<ValueBalance<C>, ValueBalanceError>> for ValueBalance<C>
where
C: Constraint,
{
type Output = Result<ValueBalance<C>, ValueBalanceError>;
fn sub(self, rhs: Result<ValueBalance<C>, ValueBalanceError>) -> Self::Output {
self - rhs?
}
}
impl<C> std::ops::SubAssign<ValueBalance<C>> for Result<ValueBalance<C>, ValueBalanceError>
where
ValueBalance<C>: Copy,
C: Constraint,
{
fn sub_assign(&mut self, rhs: ValueBalance<C>) {
if let Ok(lhs) = *self {
*self = lhs - rhs;
}
}
}
impl<C> std::iter::Sum<ValueBalance<C>> for Result<ValueBalance<C>, ValueBalanceError>
where
C: Constraint + Copy,

View File

@ -125,7 +125,7 @@ pub fn disabled_add_to_sprout_pool(
if height >= canopy_activation_height {
let zero = Amount::<NonNegative>::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);

View File

@ -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<NonNegative> = 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<NonNegative> = block.transactions[7]
.sprout_pool_added_values()
.output_values_to_sprout()
.fold(zero, |acc, &x| (acc + x).unwrap());
assert_eq!(vpub_old, zero);

View File

@ -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<BoxError> for CloneError {
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
/// 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")]

View File

@ -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::<Block>()
.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::<transparent::Output>::arbitrary(),
prevout_input in TypeNameToDebug::<transparent::Input>::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<Item = transparent::Input>,
spent_outputs: impl IntoIterator<Item = (transparent::OutPoint, transparent::Output)>,
outputs: impl IntoIterator<Item = transparent::Output>,
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<Item = transparent::Input>,
spent_outputs: impl IntoIterator<Item = (transparent::OutPoint, transparent::Output)>,
outputs: impl IntoIterator<Item = transparent::Output>,
) -> 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
}

View File

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