diff --git a/chain/src/sapling.rs b/chain/src/sapling.rs index 7495d54b..c5beedc5 100644 --- a/chain/src/sapling.rs +++ b/chain/src/sapling.rs @@ -3,7 +3,7 @@ use hex::ToHex; #[derive(Clone)] pub struct Sapling { - pub amount: u64, + pub amount: i64, pub spends: Vec, pub outputs: Vec, pub binding_sig: [u8; 64], diff --git a/network/src/consensus.rs b/network/src/consensus.rs index 3e3345f3..326e02de 100644 --- a/network/src/consensus.rs +++ b/network/src/consensus.rs @@ -158,7 +158,7 @@ impl ConsensusParams { 20_000 } - pub fn max_transaction_value(&self) -> u64 { + pub fn max_transaction_value(&self) -> i64 { 21_000_000 * 100_000_000 // No amount larger than this (in satoshi) is valid } @@ -173,4 +173,8 @@ impl ConsensusParams { 100_000 } } + + pub fn transaction_expiry_height_threshold(&self) -> u32 { + 500_000_000 + } } diff --git a/test-data/src/chain_builder.rs b/test-data/src/chain_builder.rs index c70f645e..fd734a11 100644 --- a/test-data/src/chain_builder.rs +++ b/test-data/src/chain_builder.rs @@ -1,7 +1,8 @@ use primitives::hash::H256; use ser::Serializable; use primitives::bytes::Bytes; -use chain::{Transaction, IndexedTransaction, TransactionInput, TransactionOutput, OutPoint}; +use chain::{Transaction, IndexedTransaction, TransactionInput, TransactionOutput, OutPoint, + JoinSplit, Sapling}; #[derive(Debug, Default, Clone)] pub struct ChainBuilder { @@ -76,6 +77,16 @@ impl TransactionBuilder { builder.add_input(&Transaction::default(), output_index) } + pub fn with_sapling(sapling: Sapling) -> TransactionBuilder { + let builder = TransactionBuilder::default(); + builder.set_sapling(sapling) + } + + pub fn with_join_split(join_split: JoinSplit) -> TransactionBuilder { + let builder = TransactionBuilder::default(); + builder.set_join_split(join_split) + } + pub fn reset(self) -> TransactionBuilder { TransactionBuilder::default() } @@ -143,6 +154,21 @@ impl TransactionBuilder { self } + pub fn set_sapling(mut self, sapling: Sapling) -> TransactionBuilder { + self.transaction.sapling = Some(sapling); + self + } + + pub fn set_join_split(mut self, join_split: JoinSplit) -> TransactionBuilder { + self.transaction.join_split = Some(join_split); + self + } + + pub fn set_expiry_height(mut self, expiry_height: u32) -> TransactionBuilder { + self.transaction.expiry_height = expiry_height; + self + } + pub fn lock(mut self) -> Self { self.transaction.inputs[0].sequence = 0; self.transaction.lock_time = 500000; diff --git a/verification/src/error.rs b/verification/src/error.rs index ee165e2e..7fcfb07a 100644 --- a/verification/src/error.rs +++ b/verification/src/error.rs @@ -114,5 +114,11 @@ pub enum TransactionError { InvalidVersionGroup, /// Transaction has too large output value. ValueOverflow, + /// Transaction expiry height is too high. + ExpiryHeightTooHigh, + /// Sapling with empty spends && outputs has non-empty balance. + EmptySaplingHasBalance, + /// Both value_pub_old && value_pub_new in join split description are non-zero. + JoinSplitBothPubsNonZero, } diff --git a/verification/src/verify_transaction.rs b/verification/src/verify_transaction.rs index 97e17b44..324439b8 100644 --- a/verification/src/verify_transaction.rs +++ b/verification/src/verify_transaction.rs @@ -10,12 +10,15 @@ use constants::{MIN_COINBASE_SIZE, MAX_COINBASE_SIZE}; pub struct TransactionVerifier<'a> { pub version: TransactionVersion<'a>, + pub expiry: TransactionExpiry<'a>, pub empty: TransactionEmpty<'a>, pub null_non_coinbase: TransactionNullNonCoinbase<'a>, pub oversized_coinbase: TransactionOversizedCoinbase<'a>, pub join_split_in_coinbase: TransactionJoinSplitInCoinbase<'a>, pub size: TransactionAbsoluteSize<'a>, - pub value_overflow: TransactionValueOverflow<'a>, + pub sapling: TransactionSapling<'a>, + pub join_split: TransactionJoinSplit<'a>, + pub value_overflow: TransactionOutputValueOverflow<'a>, } impl<'a> TransactionVerifier<'a> { @@ -23,22 +26,28 @@ impl<'a> TransactionVerifier<'a> { trace!(target: "verification", "Tx pre-verification {}", transaction.hash.to_reversed_str()); TransactionVerifier { version: TransactionVersion::new(transaction), + expiry: TransactionExpiry::new(transaction, consensus), empty: TransactionEmpty::new(transaction), null_non_coinbase: TransactionNullNonCoinbase::new(transaction), oversized_coinbase: TransactionOversizedCoinbase::new(transaction, MIN_COINBASE_SIZE..MAX_COINBASE_SIZE), join_split_in_coinbase: TransactionJoinSplitInCoinbase::new(transaction), size: TransactionAbsoluteSize::new(transaction, consensus), - value_overflow: TransactionValueOverflow::new(transaction, consensus), + sapling: TransactionSapling::new(transaction), + join_split: TransactionJoinSplit::new(transaction), + value_overflow: TransactionOutputValueOverflow::new(transaction, consensus), } } pub fn check(&self) -> Result<(), TransactionError> { self.version.check()?; + self.expiry.check()?; self.empty.check()?; self.null_non_coinbase.check()?; self.oversized_coinbase.check()?; self.join_split_in_coinbase.check()?; self.size.check()?; + self.sapling.check()?; + self.join_split.check()?; self.value_overflow.check()?; Ok(()) } @@ -50,7 +59,7 @@ pub struct MemoryPoolTransactionVerifier<'a> { pub is_coinbase: TransactionMemoryPoolCoinbase<'a>, pub size: TransactionAbsoluteSize<'a>, pub sigops: TransactionSigops<'a>, - pub value_overflow: TransactionValueOverflow<'a>, + pub value_overflow: TransactionOutputValueOverflow<'a>, } impl<'a> MemoryPoolTransactionVerifier<'a> { @@ -62,7 +71,7 @@ impl<'a> MemoryPoolTransactionVerifier<'a> { is_coinbase: TransactionMemoryPoolCoinbase::new(transaction), size: TransactionAbsoluteSize::new(transaction, consensus), sigops: TransactionSigops::new(transaction, consensus.max_block_sigops()), - value_overflow: TransactionValueOverflow::new(transaction, consensus), + value_overflow: TransactionOutputValueOverflow::new(transaction, consensus), } } @@ -92,18 +101,20 @@ impl<'a> TransactionEmpty<'a> { } fn check(&self) -> Result<(), TransactionError> { - // If version == 1 or nJoinSplit == 0, then tx_in_count MUST NOT be 0. - if self.transaction.raw.version == 1 || self.transaction.raw.join_split.is_none() { - if self.transaction.raw.inputs.is_empty() { + // Transactions containing empty `vin` must have either non-empty `vjoinsplit` or non-empty `vShieldedSpend`. + if self.transaction.raw.inputs.is_empty() { + let is_empty_join_split = self.transaction.raw.join_split.is_none(); + let is_empty_shielded_spends = self.transaction.raw.sapling.as_ref().map(|s| s.spends.is_empty()).unwrap_or(true); + if is_empty_join_split && is_empty_shielded_spends { return Err(TransactionError::Empty); } } - // Transactions containing empty `vin` must have either non-empty `vjoinsplit`. - // Transactions containing empty `vout` must have either non-empty `vjoinsplit`. - // TODO [Sapling]: ... or non-empty `vShieldedOutput` - if self.transaction.raw.is_empty() { - if self.transaction.raw.join_split.is_none() { + // Transactions containing empty `vout` must have either non-empty `vjoinsplit` or non-empty `vShieldedOutput`. + if self.transaction.raw.outputs.is_empty() { + let is_empty_join_split = self.transaction.raw.join_split.is_none(); + let is_empty_shielded_outputs = self.transaction.raw.sapling.as_ref().map(|s| s.outputs.is_empty()).unwrap_or(true); + if is_empty_join_split && is_empty_shielded_outputs { return Err(TransactionError::Empty); } } @@ -286,33 +297,141 @@ impl<'a> TransactionJoinSplitInCoinbase<'a> { } } -/// Check for overflow of output values. -pub struct TransactionValueOverflow<'a> { +/// Check that transaction sapling is well-formed. +pub struct TransactionSapling<'a> { transaction: &'a IndexedTransaction, - max_value: u64, } -impl<'a> TransactionValueOverflow<'a> { +impl<'a> TransactionSapling<'a> { + fn new(transaction: &'a IndexedTransaction) -> Self { + TransactionSapling { + transaction, + } + } + + fn check(&self) -> Result<(), TransactionError> { + if let Some(ref sapling) = self.transaction.raw.sapling { + // sapling balance should be zero if spends and outputs are empty + if sapling.amount != 0 && sapling.spends.is_empty() && sapling.outputs.is_empty() { + return Err(TransactionError::EmptySaplingHasBalance); + } + } + + Ok(()) + } +} + + +/// Check that transaction join split is well-formed. +pub struct TransactionJoinSplit<'a> { + transaction: &'a IndexedTransaction, +} + +impl<'a> TransactionJoinSplit<'a> { + fn new(transaction: &'a IndexedTransaction) -> Self { + TransactionJoinSplit { + transaction, + } + } + + fn check(&self) -> Result<(), TransactionError> { + if let Some(ref join_split) = self.transaction.raw.join_split { + for desc in &join_split.descriptions { + if desc.value_pub_old != 0 && desc.value_pub_new != 0 { + return Err(TransactionError::JoinSplitBothPubsNonZero) + } + } + } + + Ok(()) + } +} + +/// Check for overflow of output values. +pub struct TransactionOutputValueOverflow<'a> { + transaction: &'a IndexedTransaction, + max_value: i64, +} + +impl<'a> TransactionOutputValueOverflow<'a> { fn new(transaction: &'a IndexedTransaction, consensus: &'a ConsensusParams) -> Self { - TransactionValueOverflow { + TransactionOutputValueOverflow { transaction, max_value: consensus.max_transaction_value(), } } fn check(&self) -> Result<(), TransactionError> { - let mut total_output = 0u64; + let mut total_output = 0i64; + + // each output should be less than max_value + // the sum of all outputs should be less than max value for output in &self.transaction.raw.outputs { - if output.value > self.max_value { + if output.value > self.max_value as u64 { return Err(TransactionError::ValueOverflow) } - total_output = match total_output.checked_add(output.value) { + total_output = match total_output.checked_add(output.value as i64) { Some(total_output) if total_output <= self.max_value => total_output, _ => return Err(TransactionError::ValueOverflow), }; } + if let Some(ref sapling) = self.transaction.raw.sapling { + // check that sapling amount is within limits + if sapling.amount < -self.max_value || sapling.amount > self.max_value { + return Err(TransactionError::ValueOverflow); + } + + // negative sapling amount takes value from transparent pool + if sapling.amount < 0 { + total_output = match total_output.checked_add(-sapling.amount) { + Some(total_output) if total_output <= self.max_value => total_output, + _ => return Err(TransactionError::ValueOverflow), + }; + } + } + + if let Some(ref join_split) = self.transaction.raw.join_split { + for desc in &join_split.descriptions { + if desc.value_pub_old > self.max_value as u64 { + return Err(TransactionError::ValueOverflow); + } + + if desc.value_pub_new > self.max_value as u64 { + return Err(TransactionError::ValueOverflow); + } + + total_output = match total_output.checked_add(desc.value_pub_old as i64) { + Some(total_output) if total_output <= self.max_value => total_output, + _ => return Err(TransactionError::ValueOverflow), + }; + } + } + + Ok(()) + } +} + +/// Check that transaction expiry height is too high. +pub struct TransactionExpiry<'a> { + transaction: &'a IndexedTransaction, + height_threshold: u32, +} + +impl<'a> TransactionExpiry<'a> { + fn new(transaction: &'a IndexedTransaction, consensus: &'a ConsensusParams) -> Self { + TransactionExpiry { + transaction, + height_threshold: consensus.transaction_expiry_height_threshold(), + } + } + + fn check(&self) -> Result<(), TransactionError> { + if self.transaction.raw.overwintered && self.transaction.raw.expiry_height >= self.height_threshold { + return Err(TransactionError::ExpiryHeightTooHigh); + } + Ok(()) } } @@ -322,17 +441,15 @@ mod tests { extern crate test_data; use chain::{BTC_TX_VERSION, OVERWINTER_TX_VERSION, OVERWINTER_TX_VERSION_GROUP_ID, - SAPLING_TX_VERSION_GROUP_ID}; + SAPLING_TX_VERSION_GROUP_ID, Sapling, JoinSplit, JoinSplitDescription}; use network::{Network, ConsensusParams}; use error::TransactionError; - use super::{TransactionEmpty, TransactionVersion, TransactionJoinSplitInCoinbase, TransactionValueOverflow}; + use super::{TransactionEmpty, TransactionVersion, TransactionJoinSplitInCoinbase, + TransactionOutputValueOverflow, TransactionExpiry, TransactionSapling, TransactionJoinSplit}; #[test] fn transaction_empty_works() { - assert_eq!(TransactionEmpty::new(&test_data::TransactionBuilder::with_version(1) - .add_output(0) - .add_default_join_split() - .into()).check(), Err(TransactionError::Empty)); + // empty inputs assert_eq!(TransactionEmpty::new(&test_data::TransactionBuilder::with_version(2) .add_output(0) @@ -344,7 +461,25 @@ mod tests { .into()).check(), Ok(())); assert_eq!(TransactionEmpty::new(&test_data::TransactionBuilder::with_version(2) + .add_output(0) + .set_sapling(Sapling { spends: vec![Default::default()], ..Default::default() }) + .into()).check(), Ok(())); + + // empty outputs + + assert_eq!(TransactionEmpty::new(&test_data::TransactionBuilder::with_version(2) + .add_default_input(0) .into()).check(), Err(TransactionError::Empty)); + + assert_eq!(TransactionEmpty::new(&test_data::TransactionBuilder::with_version(2) + .add_default_input(0) + .add_default_join_split() + .into()).check(), Ok(())); + + assert_eq!(TransactionEmpty::new(&test_data::TransactionBuilder::with_version(2) + .add_default_input(0) + .set_sapling(Sapling { outputs: vec![Default::default()], ..Default::default() }) + .into()).check(), Ok(())); } #[test] @@ -386,17 +521,149 @@ mod tests { } #[test] - fn transaction_value_overflow_works() { + fn transaction_output_value_overflow_works() { let consensus = ConsensusParams::new(Network::Mainnet); - assert_eq!(TransactionValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() + 1) + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() as u64 + 1) .into(), &consensus).check(), Err(TransactionError::ValueOverflow)); - assert_eq!(TransactionValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() / 2) - .add_output(consensus.max_transaction_value() / 2 + 1) + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() as u64 / 2) + .add_output(consensus.max_transaction_value() as u64 / 2 + 1) .into(), &consensus).check(), Err(TransactionError::ValueOverflow)); - assert_eq!(TransactionValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value()) + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() as u64) .into(), &consensus).check(), Ok(())); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_sapling(Sapling { + amount: consensus.max_transaction_value(), + ..Default::default() + }).into(), &consensus).check(), Ok(())); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_sapling(Sapling { + amount: consensus.max_transaction_value() + 1, + ..Default::default() + }).into(), &consensus).check(), Err(TransactionError::ValueOverflow)); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() as u64 / 2 + 1) + .set_sapling(Sapling { + amount: -consensus.max_transaction_value() / 2, + ..Default::default() + }).into(), &consensus).check(), Err(TransactionError::ValueOverflow)); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: consensus.max_transaction_value() as u64, + value_pub_new: 0, + ..Default::default() + }], + ..Default::default() + }).into(), &consensus).check(), Ok(())); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: consensus.max_transaction_value() as u64 + 1, + value_pub_new: 0, + ..Default::default() + }], + ..Default::default() + }).into(), &consensus).check(), Err(TransactionError::ValueOverflow)); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: 0, + value_pub_new: consensus.max_transaction_value() as u64, + ..Default::default() + }], + ..Default::default() + }).into(), &consensus).check(), Ok(())); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: 0, + value_pub_new: consensus.max_transaction_value() as u64 + 1, + ..Default::default() + }], + ..Default::default() + }).into(), &consensus).check(), Err(TransactionError::ValueOverflow)); + + assert_eq!(TransactionOutputValueOverflow::new(&test_data::TransactionBuilder::with_output(consensus.max_transaction_value() as u64 / 2 + 1) + .set_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: consensus.max_transaction_value() as u64 / 2, + value_pub_new: 0, + ..Default::default() + }], + ..Default::default() + }).into(), &consensus).check(), Err(TransactionError::ValueOverflow)); + } + + #[test] + fn transaction_expiry_works() { + let consensus = ConsensusParams::new(Network::Mainnet); + + assert_eq!(TransactionExpiry::new(&test_data::TransactionBuilder::overwintered() + .set_expiry_height(consensus.transaction_expiry_height_threshold() - 1).into(), &consensus).check(), + Ok(())); + + assert_eq!(TransactionExpiry::new(&test_data::TransactionBuilder::overwintered() + .set_expiry_height(consensus.transaction_expiry_height_threshold()).into(), &consensus).check(), + Err(TransactionError::ExpiryHeightTooHigh)); + } + + #[test] + fn transaction_sapling_works() { + assert_eq!(TransactionSapling::new(&test_data::TransactionBuilder::with_sapling(Sapling { + amount: 100, + spends: vec![Default::default()], + ..Default::default() + }).into()).check(), Ok(())); + + assert_eq!(TransactionSapling::new(&test_data::TransactionBuilder::with_sapling(Sapling { + amount: 100, + outputs: vec![Default::default()], + ..Default::default() + }).into()).check(), Ok(())); + + assert_eq!(TransactionSapling::new(&test_data::TransactionBuilder::with_sapling(Sapling { + amount: 100, + outputs: vec![Default::default()], + spends: vec![Default::default()], + ..Default::default() + }).into()).check(), Ok(())); + + assert_eq!(TransactionSapling::new(&test_data::TransactionBuilder::with_sapling(Sapling { + amount: 100, + ..Default::default() + }).into()).check(), Err(TransactionError::EmptySaplingHasBalance)); + } + + #[test] + fn transaction_join_split_works() { + assert_eq!(TransactionJoinSplit::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: 100, + value_pub_new: 0, + ..Default::default() + }], + ..Default::default() + }).into()).check(), Ok(())); + + assert_eq!(TransactionJoinSplit::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: 0, + value_pub_new: 100, + ..Default::default() + }], + ..Default::default() + }).into()).check(), Ok(())); + + assert_eq!(TransactionJoinSplit::new(&test_data::TransactionBuilder::with_join_split(JoinSplit { + descriptions: vec![JoinSplitDescription { + value_pub_old: 100, + value_pub_new: 100, + ..Default::default() + }], + ..Default::default() + }).into()).check(), Err(TransactionError::JoinSplitBothPubsNonZero)); } }