Move the check in `transaction::check::sapling_balances_match` to `V4` deserialization (#2234)
* Implement `PartialEq<i64>` for `Amount` Allows to compare an `Amount` instance directly to an integer. * Add `SerializationError::BadTransactionBalance` Error variant representing deserialization of a transaction that doesn't conform to the Sapling consensus rule where the balance MUST be zero if there aren't any shielded spends and outputs. * Validate consensus rule when deserializing Return an error if the deserialized V4 transaction has a non-zero value balance but doesn't have any Sapling shielded spends nor outputs. * Add consensus rule link to field documentation Describe how the consensus rule is validated structurally by `ShieldedData`. * Clarify that `value_balance` is zero Make the description more concise and objective. Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * Update field documentation Include information about how the consensus rule is guaranteed during serialization. Co-authored-by: teor <teor@riseup.net> * Remove `check::sapling_balances_match` function The check is redundant because the respective consensus rule is validated structurally by `ShieldedData`. * Test deserialization of invalid V4 transaction A transaction with no Sapling shielded spends and no outputs but with a non-zero balance value should fail to deserialize. * Change least-significant byte of the value balance State how the byte index is calculated, and change the least significant-byte to be non-zero. Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
2f0f379a9e
commit
b44d81669f
|
@ -227,6 +227,18 @@ impl<C1, C2> PartialEq<Amount<C2>> for Amount<C1> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<C> PartialEq<i64> for Amount<C> {
|
||||||
|
fn eq(&self, other: &i64) -> bool {
|
||||||
|
self.0.eq(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> PartialEq<Amount<C>> for i64 {
|
||||||
|
fn eq(&self, other: &Amount<C>) -> bool {
|
||||||
|
self.eq(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Eq for Amount<NegativeAllowed> {}
|
impl Eq for Amount<NegativeAllowed> {}
|
||||||
impl Eq for Amount<NonNegative> {}
|
impl Eq for Amount<NonNegative> {}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,21 @@ where
|
||||||
AnchorV: AnchorVariant + Clone,
|
AnchorV: AnchorVariant + Clone,
|
||||||
{
|
{
|
||||||
/// The net value of Sapling spend transfers minus output transfers.
|
/// The net value of Sapling spend transfers minus output transfers.
|
||||||
|
///
|
||||||
|
/// [`ShieldedData`] validates this [value balance consensus
|
||||||
|
/// rule](https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus):
|
||||||
|
///
|
||||||
|
/// "If effectiveVersion = 4 and there are no Spend descriptions or Output
|
||||||
|
/// descriptions, then valueBalanceSapling MUST be 0."
|
||||||
|
///
|
||||||
|
/// During deserialization, this rule is checked when there are no spends and
|
||||||
|
/// no outputs.
|
||||||
|
///
|
||||||
|
/// During serialization, this rule is structurally validated by [`ShieldedData`].
|
||||||
|
/// `value_balance` is a field in [`ShieldedData`], which must have at least
|
||||||
|
/// one spend or output in its `transfers` field. If [`ShieldedData`] is `None`
|
||||||
|
/// then there can not possibly be any spends or outputs, and the
|
||||||
|
/// `value_balance` is always serialized as zero.
|
||||||
pub value_balance: Amount,
|
pub value_balance: Amount,
|
||||||
|
|
||||||
/// A bundle of spends and outputs, containing at least one spend or
|
/// A bundle of spends and outputs, containing at least one spend or
|
||||||
|
|
|
@ -23,4 +23,10 @@ pub enum SerializationError {
|
||||||
#[from]
|
#[from]
|
||||||
source: crate::amount::Error,
|
source: crate::amount::Error,
|
||||||
},
|
},
|
||||||
|
/// Invalid transaction with a non-zero balance and no Sapling shielded spends or outputs.
|
||||||
|
///
|
||||||
|
/// Transaction does not conform to the Sapling [consensus
|
||||||
|
/// rule](https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus).
|
||||||
|
#[error("transaction balance is non-zero but doesn't have Sapling shielded spends or outputs")]
|
||||||
|
BadTransactionBalance,
|
||||||
}
|
}
|
||||||
|
|
|
@ -608,6 +608,12 @@ impl ZcashDeserialize for Transaction {
|
||||||
outputs: shielded_outputs.try_into().expect("checked for outputs"),
|
outputs: shielded_outputs.try_into().expect("checked for outputs"),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// There are no shielded outputs and no shielded spends, so the value balance
|
||||||
|
// MUST be zero:
|
||||||
|
// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
|
||||||
|
if value_balance != 0 {
|
||||||
|
return Err(SerializationError::BadTransactionBalance);
|
||||||
|
}
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use super::super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
block::{Block, Height, MAX_BLOCK_BYTES},
|
block::{Block, Height, MAX_BLOCK_BYTES},
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
|
serialization::{SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
@ -37,6 +37,37 @@ fn librustzcash_tx_hash() {
|
||||||
assert_eq!(hash, expected);
|
assert_eq!(hash, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doesnt_deserialize_transaction_with_invalid_value_balance() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
let dummy_transaction = Transaction::V4 {
|
||||||
|
inputs: vec![],
|
||||||
|
outputs: vec![],
|
||||||
|
lock_time: LockTime::Height(Height(1)),
|
||||||
|
expiry_height: Height(10),
|
||||||
|
joinsplit_data: None,
|
||||||
|
sapling_shielded_data: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut input_bytes = Vec::new();
|
||||||
|
dummy_transaction
|
||||||
|
.zcash_serialize(&mut input_bytes)
|
||||||
|
.expect("dummy transaction should serialize");
|
||||||
|
// Set value balance to non-zero
|
||||||
|
// There are 4 * 4 byte fields and 2 * 1 byte compact sizes = 18 bytes before the 8 byte amount
|
||||||
|
// (Zcash is little-endian unless otherwise specified:
|
||||||
|
// https://zips.z.cash/protocol/nu5.pdf#endian)
|
||||||
|
input_bytes[18] = 1;
|
||||||
|
|
||||||
|
let result = Transaction::zcash_deserialize(&input_bytes[..]);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(SerializationError::BadTransactionBalance)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zip143_deserialize_and_round_trip() {
|
fn zip143_deserialize_and_round_trip() {
|
||||||
zebra_test::init();
|
zebra_test::init();
|
||||||
|
|
|
@ -215,8 +215,6 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sapling_shielded_data) = sapling_shielded_data {
|
if let Some(sapling_shielded_data) = sapling_shielded_data {
|
||||||
check::sapling_balances_match(&sapling_shielded_data)?;
|
|
||||||
|
|
||||||
for spend in sapling_shielded_data.spends_per_anchor() {
|
for spend in sapling_shielded_data.spends_per_anchor() {
|
||||||
// Consensus rule: cv and rk MUST NOT be of small
|
// Consensus rule: cv and rk MUST NOT be of small
|
||||||
// order, i.e. [h_J]cv MUST NOT be 𝒪_J and [h_J]rk
|
// order, i.e. [h_J]cv MUST NOT be 𝒪_J and [h_J]rk
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
orchard::Flags,
|
orchard::Flags,
|
||||||
sapling::{AnchorVariant, Output, PerSpendAnchor, ShieldedData, Spend},
|
sapling::{Output, PerSpendAnchor, Spend},
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,31 +40,6 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that if there are no Spends or Outputs, the Sapling valueBalance is also 0.
|
|
||||||
///
|
|
||||||
/// If effectiveVersion = 4 and there are no Spend descriptions or Output descriptions,
|
|
||||||
/// then valueBalanceSapling MUST be 0.
|
|
||||||
///
|
|
||||||
/// This check is redundant for `Transaction::V5`, because the transaction format
|
|
||||||
/// omits `valueBalanceSapling` when there are no spends and no outputs. But it's
|
|
||||||
/// simpler to just do the redundant check anyway.
|
|
||||||
///
|
|
||||||
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
|
|
||||||
pub fn sapling_balances_match<AnchorV>(
|
|
||||||
sapling_shielded_data: &ShieldedData<AnchorV>,
|
|
||||||
) -> Result<(), TransactionError>
|
|
||||||
where
|
|
||||||
AnchorV: AnchorVariant + Clone,
|
|
||||||
{
|
|
||||||
if (sapling_shielded_data.spends().count() + sapling_shielded_data.outputs().count() != 0)
|
|
||||||
|| i64::from(sapling_shielded_data.value_balance) == 0
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(TransactionError::BadBalance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends.
|
/// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends.
|
||||||
///
|
///
|
||||||
/// A coinbase transaction MUST NOT have any transparent inputs, JoinSplit descriptions,
|
/// A coinbase transaction MUST NOT have any transparent inputs, JoinSplit descriptions,
|
||||||
|
|
|
@ -35,8 +35,6 @@ fn v5_fake_transactions() -> Result<(), Report> {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Some(s) = sapling_shielded_data {
|
if let Some(s) = sapling_shielded_data {
|
||||||
check::sapling_balances_match(&s)?;
|
|
||||||
|
|
||||||
for spend in s.spends_per_anchor() {
|
for spend in s.spends_per_anchor() {
|
||||||
check::spend_cv_rk_not_small_order(&spend)?
|
check::spend_cv_rk_not_small_order(&spend)?
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue