Merge pull request #204 from ethcore/sigops

regtests sigops fixes (now stuck at b82)
This commit is contained in:
Marek Kotewicz 2016-11-27 23:16:18 +01:00 committed by GitHub
commit b6060f5560
4 changed files with 147 additions and 166 deletions

View File

@ -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)]

View File

@ -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<usize, Error> {
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<usize, Error> {
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);
}
}

View File

@ -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<chain::TransactionOutput> {
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<transaction> -> hashmap<h256, transaction>
.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<usize, TransactionError> {
) -> 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!

View File

@ -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<usize, script::Error> {
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;