diff --git a/script/src/opcode.rs b/script/src/opcode.rs index 90b003e2..5fde8eae 100644 --- a/script/src/opcode.rs +++ b/script/src/opcode.rs @@ -455,6 +455,17 @@ impl Opcode { pub fn is_push_value(&self) -> bool { *self >= Opcode::OP_1NEGATE && *self <= Opcode::OP_16 } + + pub fn is_within_op_n(&self) -> bool { + *self >= Opcode::OP_1 && *self <= Opcode::OP_16 + } + + pub fn decode_op_n(&self) -> u8 { + assert!(self.is_within_op_n()); + let value = *self as u8; + let op0 = Opcode::OP_1 as u8 - 1; + value - op0 + } } #[cfg(test)] diff --git a/script/src/script.rs b/script/src/script.rs index 54acbca2..724f8b03 100644 --- a/script/src/script.rs +++ b/script/src/script.rs @@ -209,15 +209,15 @@ impl Script { let slice = try!(self.take(position + 1, len)); let n = try!(read_usize(slice, len)); - let bytes = try!(self.take_checked(position + 1 + len, n)); + let bytes = try!(self.take(position + 1 + len, n)); Instruction { opcode: opcode, step: len + n + 1, data: Some(bytes), } }, - o if o >= Opcode::OP_0 && o <= Opcode::OP_PUSHBYTES_75 => { - let bytes = try!(self.take_checked(position+ 1, opcode as usize)); + o if o <= Opcode::OP_PUSHBYTES_75 => { + let bytes = try!(self.take(position + 1, opcode as usize)); Instruction { opcode: o, step: opcode as usize + 1, @@ -243,15 +243,6 @@ impl Script { } } - #[inline] - pub fn take_checked(&self, offset: usize, len: usize) -> Result<&[u8], Error> { - if len > MAX_SCRIPT_ELEMENT_SIZE { - Err(Error::ScriptSize) - } else { - self.take(offset, len) - } - } - /// Returns Script without OP_CODESEPARATOR opcodes pub fn without_separators(&self) -> Script { let mut pc = 0; @@ -318,78 +309,53 @@ impl Script { Opcodes { position: 0, script: self } } - pub fn sigop_count(&self, accurate: bool) -> Result { + pub fn sigops_count(&self, serialized_script: bool) -> usize { let mut last_opcode = Opcode::OP_0; - let mut result = 0; + let mut total = 0; for opcode in self.opcodes() { - let opcode = try!(opcode); + let opcode = match opcode { + Ok(opcode) => opcode, + // If we push an invalid element, all previous CHECKSIGs are counted + _ => return total, + }; match opcode { - Opcode::OP_CHECKSIG | Opcode::OP_CHECKSIGVERIFY => { result += 1; }, + Opcode::OP_CHECKSIG | Opcode::OP_CHECKSIGVERIFY => { + total += 1; + }, Opcode::OP_CHECKMULTISIG | Opcode::OP_CHECKMULTISIGVERIFY => { - if accurate { - match last_opcode { - Opcode::OP_1 | - Opcode::OP_2 | - Opcode::OP_3 | - Opcode::OP_4 | - Opcode::OP_5 | - Opcode::OP_6 | - Opcode::OP_7 | - Opcode::OP_8 | - Opcode::OP_9 | - Opcode::OP_10 | - Opcode::OP_11 | - Opcode::OP_12 | - Opcode::OP_13 | - Opcode::OP_14 | - Opcode::OP_15 | - Opcode::OP_16 => { - result += (last_opcode as u8 - (Opcode::OP_1 as u8 - 1)) as usize; - }, - _ => { - result += MAX_PUBKEYS_PER_MULTISIG; - } - } - } - else { - result += MAX_PUBKEYS_PER_MULTISIG; + if serialized_script && last_opcode.is_within_op_n() { + total += last_opcode.decode_op_n() as usize; + } else { + total += MAX_PUBKEYS_PER_MULTISIG; } }, - _ => { } + _ => (), }; last_opcode = opcode; } - Ok(result) + total } - pub fn sigop_count_p2sh(&self, input_ref: &Script) -> Result { - if !self.is_pay_to_script_hash() { return self.sigop_count(true); } - - let mut script_data: Option<&[u8]> = None; - // we need last command - for next in input_ref.iter() { - let instruction = match next { - Err(_) => return Ok(0), - Ok(i) => i, - }; - - if instruction.opcode as u8 > Opcode::OP_16 as u8 { - return Ok(0); - } - - script_data = instruction.data; + pub fn pay_to_script_hash_sigops(&self, prev_out: &Script) -> usize { + if !prev_out.is_pay_to_script_hash() { + return 0; } - match script_data { - Some(slc) => { - let nested_script: Script = slc.to_vec().into(); - nested_script.sigop_count(true) - }, - None => Ok(0), + if self.data.is_empty() || !self.is_push_only() { + return 0; } + + let script: Script = self.iter().last() + .expect("self.data.is_empty() == false; qed") + .expect("self.data.is_push_only()") + .data.expect("self.data.is_push_only()") + .to_vec() + .into(); + + script.sigops_count(true) } } @@ -492,7 +458,7 @@ impl fmt::Display for Script { #[cfg(test)] mod tests { use {Builder, Opcode}; - use super::{Script, ScriptType}; + use super::{Script, ScriptType, MAX_SCRIPT_ELEMENT_SIZE}; #[test] fn test_is_pay_to_script_hash() { @@ -573,10 +539,39 @@ OP_ADD #[test] fn test_sigops_count() { - assert_eq!(1usize, Script::from("76a914aab76ba4877d696590d94ea3e02948b55294815188ac").sigop_count(false).unwrap()); - assert_eq!(2usize, Script::from("522102004525da5546e7603eefad5ef971e82f7dad2272b34e6b3036ab1fe3d299c22f21037d7f2227e6c646707d1c61ecceb821794124363a2cf2c1d2a6f28cf01e5d6abe52ae").sigop_count(true).unwrap()); - assert_eq!(20usize, Script::from("522102004525da5546e7603eefad5ef971e82f7dad2272b34e6b3036ab1fe3d299c22f21037d7f2227e6c646707d1c61ecceb821794124363a2cf2c1d2a6f28cf01e5d6abe52ae").sigop_count(false).unwrap()); - assert_eq!(0usize, Script::from("a9146262b64aec1f4a4c1d21b32e9c2811dd2171fd7587").sigop_count(false).unwrap()); - assert_eq!(1usize, Script::from("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac").sigop_count(false).unwrap()); + assert_eq!(1usize, Script::from("76a914aab76ba4877d696590d94ea3e02948b55294815188ac").sigops_count(false)); + assert_eq!(2usize, Script::from("522102004525da5546e7603eefad5ef971e82f7dad2272b34e6b3036ab1fe3d299c22f21037d7f2227e6c646707d1c61ecceb821794124363a2cf2c1d2a6f28cf01e5d6abe52ae").sigops_count(true)); + assert_eq!(20usize, Script::from("522102004525da5546e7603eefad5ef971e82f7dad2272b34e6b3036ab1fe3d299c22f21037d7f2227e6c646707d1c61ecceb821794124363a2cf2c1d2a6f28cf01e5d6abe52ae").sigops_count(false)); + assert_eq!(0usize, Script::from("a9146262b64aec1f4a4c1d21b32e9c2811dd2171fd7587").sigops_count(false)); + assert_eq!(1usize, Script::from("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac").sigops_count(false)); + } + + #[test] + fn test_sigops_count_b73() { + let max_block_sigops = 20000; + let block_sigops = 0; + let mut script = vec![Opcode::OP_CHECKSIG as u8; max_block_sigops - block_sigops + MAX_SCRIPT_ELEMENT_SIZE + 1 + 5 + 1]; + script[max_block_sigops - block_sigops] = Opcode::OP_PUSHDATA4 as u8; + let overmax = MAX_SCRIPT_ELEMENT_SIZE + 1; + script[max_block_sigops - block_sigops + 1] = overmax as u8; + script[max_block_sigops - block_sigops + 2] = (overmax >> 8) as u8; + script[max_block_sigops - block_sigops + 3] = (overmax >> 16) as u8; + script[max_block_sigops - block_sigops + 4] = (overmax >> 24) as u8; + let script: Script = script.into(); + assert_eq!(script.sigops_count(false), 20001); + } + + #[test] + fn test_sigops_count_b74() { + let max_block_sigops = 20000; + let block_sigops = 0; + let mut script = vec![Opcode::OP_CHECKSIG as u8; max_block_sigops - block_sigops + MAX_SCRIPT_ELEMENT_SIZE + 42]; + script[max_block_sigops - block_sigops + 1] = Opcode::OP_PUSHDATA4 as u8; + script[max_block_sigops - block_sigops + 2] = 0xfe; + script[max_block_sigops - block_sigops + 3] = 0xff; + script[max_block_sigops - block_sigops + 4] = 0xff; + script[max_block_sigops - block_sigops + 5] = 0xff; + let script: Script = script.into(); + assert_eq!(script.sigops_count(false), 20001); } } diff --git a/verification/src/chain_verifier.rs b/verification/src/chain_verifier.rs index b317951b..d7b2a0e9 100644 --- a/verification/src/chain_verifier.rs +++ b/verification/src/chain_verifier.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use db::{self, BlockRef, BlockLocation}; use network::Magic; +use script::Script; use super::{Verify, VerificationResult, Chain, Error, TransactionError, ContinueVerify}; use {chain, utils}; @@ -11,8 +12,6 @@ const COINBASE_MATURITY: u32 = 100; // 2 hours const MAX_BLOCK_SIGOPS: usize = 20000; const MAX_BLOCK_SIZE: usize = 1000000; -const BIP16_TIME: u32 = 1333238400; - pub struct ChainVerifier { store: db::SharedStore, verify_p2sh: bool, @@ -56,11 +55,61 @@ impl ChainVerifier { self } + /// Returns previous transaction output. + /// NOTE: This function expects all previous blocks to be already in database. + fn previous_transaction_output(&self, block: &chain::Block, prevout: &chain::OutPoint) -> Option { + self.store.transaction(&prevout.hash) + .as_ref() + .or_else(|| block.transactions.iter().find(|t| t.hash() == prevout.hash)) + .and_then(|tx| tx.outputs.iter().nth(prevout.index as usize).cloned()) + } + + /// Returns number of transaction signature operations. + /// NOTE: This function expects all previous blocks to be already in database. + fn transaction_sigops(&self, block: &chain::Block, transaction: &chain::Transaction, bip16_active: bool) -> usize { + let output_sigops: usize = transaction.outputs.iter().map(|output| { + let output_script: Script = output.script_pubkey.clone().into(); + output_script.sigops_count(false) + }).sum(); + + if transaction.is_coinbase() { + return output_sigops; + } + + let input_sigops: usize = transaction.inputs.iter().map(|input| { + let input_script: Script = input.script_sig.clone().into(); + let mut sigops = input_script.sigops_count(false); + if bip16_active { + let previous_output = self.previous_transaction_output(block, &input.previous_output) + .expect("missing tx, out of order verification or malformed db"); + let prevout_script: Script = previous_output.script_pubkey.into(); + sigops += input_script.pay_to_script_hash_sigops(&prevout_script); + } + sigops + }).sum(); + + input_sigops + output_sigops + } + + /// Returns number of block signature operations. + /// NOTE: This function expects all previous blocks to be already in database. + fn block_sigops(&self, block: &chain::Block) -> usize { + // strict pay-to-script-hash signature operations count toward block + // signature operations limit is enforced with BIP16 + let bip16_active = block.block_header.time >= self.network.consensus_params().bip16_time; + block.transactions.iter().map(|tx| self.transaction_sigops(block, tx, bip16_active)).sum() + } + fn ordered_verify(&self, block: &chain::Block, at_height: u32) -> Result<(), Error> { if !block.is_final(at_height) { return Err(Error::NonFinalBlock); } + // transaction verification including number of signature operations checking + if self.block_sigops(block) > MAX_BLOCK_SIGOPS { + return Err(Error::MaximumSigops); + } + let block_hash = block.hash(); let consensus_params = self.network.consensus_params(); @@ -86,39 +135,22 @@ impl ChainVerifier { let mut total_unspent = 0u64; for (tx_index, tx) in block.transactions().iter().enumerate().skip(1) { - let mut total_claimed: u64 = 0; - for input in &tx.inputs { - // Coinbase maturity check if let Some(previous_meta) = self.store.transaction_meta(&input.previous_output.hash) { // check if it exists only // it will fail a little later if there is no transaction at all if previous_meta.is_coinbase() && - (at_height < COINBASE_MATURITY || at_height - COINBASE_MATURITY < previous_meta.height()) - { + (at_height < COINBASE_MATURITY || at_height - COINBASE_MATURITY < previous_meta.height()) { return Err(Error::Transaction(tx_index, TransactionError::Maturity)); } } - let reference_tx = try!( + let previous_output = self.previous_transaction_output(block, &input.previous_output) + .expect("missing tx, out of order verification or malformed db"); - self.store.transaction(&input.previous_output.hash) - // todo: optimize block decomposition vec -> hashmap - .or(block.transactions().iter().find(|tx| !tx.is_coinbase() && tx.hash() == input.previous_output.hash).cloned()) - .ok_or( - Error::Transaction(tx_index, TransactionError::UnknownReference(input.previous_output.hash.clone())) - ) - ); - - let output = try!(reference_tx.outputs.get(input.previous_output.index as usize) - .ok_or( - Error::Transaction(tx_index, TransactionError::Input(input.previous_output.index as usize)) - ) - ); - - total_claimed += output.value; + total_claimed += previous_output.value; } let total_spends = tx.total_spends(); @@ -143,7 +175,7 @@ impl ChainVerifier { block: &chain::Block, transaction: &chain::Transaction, sequence: usize, - ) -> Result { + ) -> Result<(), TransactionError> { use script::{ TransactionInputSigner, TransactionSignatureChecker, @@ -152,46 +184,27 @@ impl ChainVerifier { verify_script, }; - let mut sigops = utils::transaction_sigops(transaction) - .map_err(|e| TransactionError::SignatureMallformed(e.to_string()))?; - - if sequence == 0 { return Ok(sigops); } + if sequence == 0 { + return Ok(()); + } // must not be coinbase (sequence = 0 is returned above) if transaction.is_coinbase() { return Err(TransactionError::MisplacedCoinbase(sequence)); } - if sigops >= MAX_BLOCK_SIGOPS { return Err(TransactionError::Sigops(sigops)); } - - // strict pay-to-script-hash signature operations count toward block - // signature operations limit is enforced with BIP16 - let is_strict_p2sh = block.header().time >= BIP16_TIME; - for (input_index, input) in transaction.inputs().iter().enumerate() { - let store_parent_transaction = self.store.transaction(&input.previous_output.hash); - let parent_transaction = store_parent_transaction - .as_ref() - .or_else(|| block.transactions.iter().find(|t| t.hash() == input.previous_output.hash)) - .ok_or_else(|| TransactionError::Inconclusive(input.previous_output.hash.clone()))?; - - if parent_transaction.outputs.len() <= input.previous_output.index as usize { - return Err(TransactionError::Input(input_index)); - } - // signature verification let signer: TransactionInputSigner = transaction.clone().into(); - let paired_output = &parent_transaction.outputs[input.previous_output.index as usize]; + let paired_output = match self.previous_transaction_output(block, &input.previous_output) { + Some(output) => output, + _ => return Err(TransactionError::Inconclusive(input.previous_output.hash.clone())) + }; + let checker = TransactionSignatureChecker { signer: signer, input_index: input_index, }; - let input: Script = input.script_sig.to_vec().into(); - let output: Script = paired_output.script_pubkey.to_vec().into(); - - if is_strict_p2sh && output.is_pay_to_script_hash() { - sigops += utils::p2sh_sigops(&output, &input); - - if sigops >= MAX_BLOCK_SIGOPS { return Err(TransactionError::SigopsP2SH(sigops)); } - } + let input: Script = input.script_sig.clone().into(); + let output: Script = paired_output.script_pubkey.into(); let flags = VerificationFlags::default() .verify_p2sh(self.verify_p2sh) @@ -209,7 +222,7 @@ impl ChainVerifier { } } - Ok(sigops) + Ok(()) } fn verify_block(&self, block: &chain::Block) -> VerificationResult { @@ -261,20 +274,8 @@ impl ChainVerifier { return Err(Error::CoinbaseSignatureLength(coinbase_script_len)); } - // transaction verification including number of signature operations checking - let mut block_sigops = 0; for (idx, transaction) in block.transactions().iter().enumerate() { - block_sigops += try!( - self.verify_transaction( - block, - transaction, - idx, - ).map_err(|e| Error::Transaction(idx, e)) - ); - - if block_sigops > MAX_BLOCK_SIGOPS { - return Err(Error::MaximumSigops); - } + try!(self.verify_transaction(block, transaction, idx).map_err(|e| Error::Transaction(idx, e))) } // todo: pre-process projected block number once verification is parallel! diff --git a/verification/src/utils.rs b/verification/src/utils.rs index 9719d079..e7cd094c 100644 --- a/verification/src/utils.rs +++ b/verification/src/utils.rs @@ -4,8 +4,6 @@ use std::cmp; use hash::H256; use uint::U256; use byteorder::{BigEndian, ByteOrder}; -use script::{self, Script}; -use chain; use compact::Compact; // Timespan constants @@ -109,30 +107,6 @@ pub fn block_reward_satoshi(block_height: u32) -> u64 { res } -pub fn transaction_sigops(transaction: &chain::Transaction) -> Result { - let mut result = 0usize; - - for output in &transaction.outputs { - let output_script: Script = output.script_pubkey.to_vec().into(); - // todo: not always allow malformed output? - result += output_script.sigop_count(false).unwrap_or(0); - } - - if transaction.is_coinbase() { return Ok(result); } - - for input in &transaction.inputs { - let input_script: Script = input.script_sig.to_vec().into(); - result += try!(input_script.sigop_count(false)); - } - - Ok(result) -} - -pub fn p2sh_sigops(output: &Script, input_ref: &Script) -> usize { - // todo: not always skip malformed output? - output.sigop_count_p2sh(input_ref).unwrap_or(0) -} - #[cfg(test)] mod tests { use network::Magic;