Skip to content

Commit

Permalink
validate sapling v5 tx
Browse files Browse the repository at this point in the history
  • Loading branch information
oxarbitrage committed Apr 26, 2021
1 parent 1be5616 commit 26c474f
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 141 deletions.
3 changes: 1 addition & 2 deletions zebra-chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ displaydoc = "0.2.1"
equihash = "0.1"
futures = "0.3"
hex = "0.4"
itertools = "0.10.0"
jubjub = "0.6.0"
lazy_static = "1.4.0"
primitive-types = "0.9.0"
Expand Down Expand Up @@ -55,8 +56,6 @@ criterion = { version = "0.3", features = ["html_reports"] }
spandoc = "0.2"
tracing = "0.1.25"

itertools = "0.10.0"

proptest = "0.10"
proptest-derive = "0.3"

Expand Down
2 changes: 1 addition & 1 deletion zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod serialize;
mod sighash;

#[cfg(any(test, feature = "proptest-impl"))]
mod arbitrary;
pub mod arbitrary;
#[cfg(test)]
mod tests;

Expand Down
129 changes: 129 additions & 0 deletions zebra-chain/src/transaction/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use crate::{
sapling, sprout, transparent, LedgerState,
};

use itertools::Itertools;

use super::{FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction};
use sapling::{AnchorVariant, PerSpendAnchor, SharedAnchor};

Expand Down Expand Up @@ -330,3 +332,130 @@ impl Arbitrary for Transaction {

type Strategy = BoxedStrategy<Self>;
}

/// Transaction utility tests functions
/// Convert `trans` into a fake v5 transaction,
/// converting sapling shielded data from v4 to v5 if possible.
pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
use Transaction::*;

match trans {
V1 {
inputs,
outputs,
lock_time,
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
},
V2 {
inputs,
outputs,
lock_time,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
},
V3 {
inputs,
outputs,
lock_time,
expiry_height,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: None,
},
V4 {
inputs,
outputs,
lock_time,
expiry_height,
sapling_shielded_data,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: sapling_shielded_data
.clone()
.map(sapling_shielded_v4_to_fake_v5)
.flatten(),
},
v5 @ V5 { .. } => v5.clone(),
}
}

/// Convert a v4 sapling shielded data into a fake v5 sapling shielded data,
/// if possible.
fn sapling_shielded_v4_to_fake_v5(
v4_shielded: sapling::ShieldedData<PerSpendAnchor>,
) -> Option<sapling::ShieldedData<SharedAnchor>> {
use sapling::ShieldedData;
use sapling::TransferData::*;

let unique_anchors: Vec<_> = v4_shielded
.spends()
.map(|spend| spend.per_spend_anchor)
.unique()
.collect();

let fake_spends: Vec<_> = v4_shielded
.spends()
.cloned()
.map(sapling_spend_v4_to_fake_v5)
.collect();

let transfers = match v4_shielded.transfers {
SpendsAndMaybeOutputs { maybe_outputs, .. } => {
let shared_anchor = match unique_anchors.as_slice() {
[unique_anchor] => *unique_anchor,
// Multiple different anchors, can't convert to v5
_ => return None,
};

SpendsAndMaybeOutputs {
shared_anchor,
spends: fake_spends.try_into().unwrap(),
maybe_outputs,
}
}
JustOutputs { outputs } => JustOutputs { outputs },
};

let fake_shielded_v5 = ShieldedData::<SharedAnchor> {
value_balance: v4_shielded.value_balance,
transfers,
binding_sig: v4_shielded.binding_sig,
};

Some(fake_shielded_v5)
}

/// Convert a v4 sapling spend into a fake v5 sapling spend.
fn sapling_spend_v4_to_fake_v5(
v4_spend: sapling::Spend<PerSpendAnchor>,
) -> sapling::Spend<SharedAnchor> {
use sapling::Spend;

Spend::<SharedAnchor> {
cv: v4_spend.cv,
per_spend_anchor: FieldNotPresent,
nullifier: v4_spend.nullifier,
rk: v4_spend.rk,
zkproof: v4_spend.zkproof,
spend_auth_sig: v4_spend.spend_auth_sig,
}
}
132 changes: 1 addition & 131 deletions zebra-chain/src/transaction/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@ use super::super::*;

use crate::{
block::{Block, MAX_BLOCK_BYTES},
sapling::{PerSpendAnchor, SharedAnchor},
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
};

use itertools::Itertools;

use std::convert::TryInto;

#[test]
Expand Down Expand Up @@ -186,7 +183,7 @@ fn fake_v5_round_trip() {
.transactions
.iter()
.map(AsRef::as_ref)
.map(transaction_to_fake_v5)
.map(arbitrary::transaction_to_fake_v5)
.map(Into::into)
.collect();

Expand Down Expand Up @@ -264,130 +261,3 @@ fn fake_v5_round_trip() {
);
}
}

// Utility functions

/// Convert `trans` into a fake v5 transaction,
/// converting sapling shielded data from v4 to v5 if possible.
fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
use Transaction::*;

match trans {
V1 {
inputs,
outputs,
lock_time,
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
},
V2 {
inputs,
outputs,
lock_time,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
},
V3 {
inputs,
outputs,
lock_time,
expiry_height,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: None,
},
V4 {
inputs,
outputs,
lock_time,
expiry_height,
sapling_shielded_data,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: sapling_shielded_data
.clone()
.map(sapling_shielded_v4_to_fake_v5)
.flatten(),
},
v5 @ V5 { .. } => v5.clone(),
}
}

/// Convert a v4 sapling shielded data into a fake v5 sapling shielded data,
/// if possible.
fn sapling_shielded_v4_to_fake_v5(
v4_shielded: sapling::ShieldedData<PerSpendAnchor>,
) -> Option<sapling::ShieldedData<SharedAnchor>> {
use sapling::ShieldedData;
use sapling::TransferData::*;

let unique_anchors: Vec<_> = v4_shielded
.spends()
.map(|spend| spend.per_spend_anchor)
.unique()
.collect();

let fake_spends: Vec<_> = v4_shielded
.spends()
.cloned()
.map(sapling_spend_v4_to_fake_v5)
.collect();

let transfers = match v4_shielded.transfers {
SpendsAndMaybeOutputs { maybe_outputs, .. } => {
let shared_anchor = match unique_anchors.as_slice() {
[unique_anchor] => *unique_anchor,
// Multiple different anchors, can't convert to v5
_ => return None,
};

SpendsAndMaybeOutputs {
shared_anchor,
spends: fake_spends.try_into().unwrap(),
maybe_outputs,
}
}
JustOutputs { outputs } => JustOutputs { outputs },
};

let fake_shielded_v5 = ShieldedData::<SharedAnchor> {
value_balance: v4_shielded.value_balance,
transfers,
binding_sig: v4_shielded.binding_sig,
};

Some(fake_shielded_v5)
}

/// Convert a v4 sapling spend into a fake v5 sapling spend.
fn sapling_spend_v4_to_fake_v5(
v4_spend: sapling::Spend<PerSpendAnchor>,
) -> sapling::Spend<SharedAnchor> {
use sapling::Spend;

Spend::<SharedAnchor> {
cv: v4_spend.cv,
per_spend_anchor: FieldNotPresent,
nullifier: v4_spend.nullifier,
rk: v4_spend.rk,
zkproof: v4_spend.zkproof,
spend_auth_sig: v4_spend.spend_auth_sig,
}
}
1 change: 1 addition & 0 deletions zebra-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ tokio = { version = "0.3.6", features = ["full"] }
tracing-error = "0.1.2"
tracing-subscriber = "0.2.17"

zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] }
zebra-test = { path = "../zebra-test/" }
2 changes: 2 additions & 0 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use zebra_state as zs;
use crate::{error::TransactionError, primitives, script, BoxError};

mod check;
#[cfg(test)]
mod tests;

/// Asynchronous transaction verification.
#[derive(Debug, Clone)]
Expand Down
39 changes: 32 additions & 7 deletions zebra-consensus/src/transaction/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,30 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError>
Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
unreachable!("tx version is checked first")
}
Transaction::V5 { .. } => {
unimplemented!("v5 transaction format as specified in ZIP-225")
Transaction::V5 {
inputs,
outputs,
sapling_shielded_data,
..
} => {
let tx_in_count = inputs.len();
let tx_out_count = outputs.len();
let n_shielded_spend = sapling_shielded_data
.as_ref()
.map(|d| d.spends().count())
.unwrap_or(0);
let n_shielded_output = sapling_shielded_data
.as_ref()
.map(|d| d.outputs().count())
.unwrap_or(0);

if tx_in_count + n_shielded_spend == 0 {
Err(TransactionError::NoInputs)
} else if tx_out_count + n_shielded_output == 0 {
Err(TransactionError::NoOutputs)
} else {
Ok(())
}
}
}
}
Expand Down Expand Up @@ -100,15 +122,18 @@ pub fn coinbase_tx_no_joinsplit_or_spend(tx: &Transaction) -> Result<(), Transac
Err(TransactionError::CoinbaseHasSpend)
}

Transaction::V4 { .. } => Ok(()),
Transaction::V5 {
sapling_shielded_data: Some(sapling_shielded_data),
..
} if sapling_shielded_data.spends().count() > 0 => {
Err(TransactionError::CoinbaseHasSpend)
}

Transaction::V4 { .. } | Transaction::V5 { .. } => Ok(()),

Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
unreachable!("tx version is checked first")
}

Transaction::V5 { .. } => {
unimplemented!("v5 coinbase validation as specified in ZIP-225 and the draft spec")
}
}
} else {
Ok(())
Expand Down
Loading

0 comments on commit 26c474f

Please sign in to comment.