some more verification rules

This commit is contained in:
Svyatoslav Nikolsky 2018-11-19 11:46:40 +03:00
parent c5e91d033c
commit 17a7c16447
5 changed files with 337 additions and 34 deletions

View File

@ -3,7 +3,7 @@ use hex::ToHex;
#[derive(Clone)]
pub struct Sapling {
pub amount: u64,
pub amount: i64,
pub spends: Vec<SaplingSpendDescription>,
pub outputs: Vec<SaplingOutputDescription>,
pub binding_sig: [u8; 64],

View File

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

View File

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

View File

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

View File

@ -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));
}
}