Merge pull request #199 from ethcore/bip30

initial support for bip30
This commit is contained in:
Nikolay Volf 2016-11-27 19:20:46 +03:00 committed by GitHub
commit 23b1c804e3
14 changed files with 147 additions and 90 deletions

1
Cargo.lock generated
View File

@ -398,6 +398,7 @@ name = "network"
version = "0.1.0"
dependencies = [
"chain 0.1.0",
"primitives 0.1.0",
"serialization 0.1.0",
]

View File

@ -70,6 +70,10 @@ impl Block {
pub fn hash(&self) -> H256 {
self.block_header.hash()
}
pub fn is_final(&self, height: u32) -> bool {
self.transactions.iter().all(|t| t.is_final(height, self.block_header.time))
}
}
#[cfg(test)]

View File

@ -23,7 +23,8 @@ pub use self::merkle_root::merkle_node_hash;
pub use self::transaction::{
Transaction, TransactionInput, TransactionOutput, OutPoint,
SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_FINAL,
SEQUENCE_LOCKTIME_TYPE_FLAG, SEQUENCE_LOCKTIME_MASK
SEQUENCE_LOCKTIME_TYPE_FLAG, SEQUENCE_LOCKTIME_MASK,
LOCKTIME_THRESHOLD
};
pub type ShortTransactionID = hash::H48;

View File

@ -30,6 +30,9 @@ pub const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = (1 << 22);
// applied to extract that lock-time from the sequence field.
pub const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000ffff;
/// Threshold for `nLockTime`: below this value it is interpreted as block number,
/// otherwise as UNIX timestamp.
pub const LOCKTIME_THRESHOLD: u32 = 500000000; // Tue Nov 5 00:53:20 1985 UTC
#[derive(Debug, PartialEq, Clone, Default)]
pub struct OutPoint {
@ -66,10 +69,6 @@ impl OutPoint {
&self.hash
}
pub fn index(&self) -> u32 {
self.index
}
pub fn is_null(&self) -> bool {
self.hash.is_zero() && self.index == u32::max_value()
}
@ -82,6 +81,12 @@ pub struct TransactionInput {
pub sequence: u32,
}
impl TransactionInput {
pub fn is_final(&self) -> bool {
self.sequence == SEQUENCE_FINAL
}
}
impl Serializable for TransactionInput {
fn serialize(&self, stream: &mut Stream) {
stream
@ -116,20 +121,6 @@ impl HeapSizeOf for TransactionInput {
}
}
impl TransactionInput {
pub fn previous_output(&self) -> &OutPoint {
&self.previous_output
}
pub fn script_sig(&self) -> &[u8] {
&self.script_sig
}
pub fn sequence(&self) -> u32 {
self.sequence
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct TransactionOutput {
pub value: u64,
@ -176,16 +167,6 @@ impl HeapSizeOf for TransactionOutput {
}
}
impl TransactionOutput {
pub fn value(&self) -> u64 {
self.value
}
pub fn script_pubkey(&self) -> &[u8] {
&self.script_pubkey
}
}
#[derive(Debug, PartialEq, Default, Clone)]
pub struct Transaction {
pub version: i32,
@ -254,6 +235,24 @@ impl Transaction {
self.inputs.len() == 1 && self.inputs[0].previous_output.is_null()
}
pub fn is_final(&self, block_height: u32, block_time: u32) -> bool {
if self.lock_time == 0 {
return true;
}
let max_lock_time = if self.lock_time < LOCKTIME_THRESHOLD {
block_height
} else {
block_time
};
if self.lock_time < max_lock_time {
return true;
}
self.inputs.iter().all(TransactionInput::is_final)
}
pub fn total_spends(&self) -> u64 {
self.outputs
.iter()

View File

@ -175,7 +175,7 @@ impl Storage {
if let Some(accepted_tx) = accepted_txs.iter().next() {
context.meta.insert(
accepted_tx.hash(),
TransactionMeta::new(number, accepted_tx.outputs.len()).coinbase()
TransactionMeta::new_coinbase(number, accepted_tx.outputs.len())
);
}
@ -186,29 +186,27 @@ impl Storage {
);
for input in &accepted_tx.inputs {
if !match context.meta.get_mut(&input.previous_output.hash) {
Some(ref mut meta) => {
use std::collections::hash_map::Entry;
match context.meta.entry(input.previous_output.hash.clone()) {
Entry::Occupied(mut entry) => {
let meta = entry.get_mut();
if meta.is_spent(input.previous_output.index as usize) {
return Err(Error::double_spend(&input.previous_output.hash));
}
meta.denote_used(input.previous_output.index as usize);
},
Entry::Vacant(entry) => {
let mut meta = self.transaction_meta(&input.previous_output.hash)
.ok_or(Error::unknown_spending(&input.previous_output.hash))?;
if meta.is_spent(input.previous_output.index as usize) {
return Err(Error::double_spend(&input.previous_output.hash));
}
meta.denote_used(input.previous_output.index as usize);
true
entry.insert(meta);
},
None => false,
} {
let mut meta = self.transaction_meta(&input.previous_output.hash)
.ok_or(Error::unknown_spending(&input.previous_output.hash))?;
if meta.is_spent(input.previous_output.index as usize) {
return Err(Error::double_spend(&input.previous_output.hash));
}
meta.denote_used(input.previous_output.index as usize);
context.meta.insert(
input.previous_output.hash.clone(),
meta);
}
}
}
@ -238,30 +236,24 @@ impl Storage {
// remove meta
context.db_transaction.delete(Some(COL_TRANSACTIONS_META), &**tx_hash);
// denote outputs used
if tx_hash_num == 0 { continue; } // coinbase transaction does not have inputs
// coinbase transaction does not have inputs
if tx_hash_num == 0 {
continue;
}
// denote outputs as unused
for input in &tx.inputs {
if !match context.meta.get_mut(&input.previous_output.hash) {
Some(ref mut meta) => {
meta.denote_unused(input.previous_output.index as usize);
true
use std::collections::hash_map::Entry;
match context.meta.entry(input.previous_output.hash.clone()) {
Entry::Occupied(mut entry) => {
entry.get_mut().denote_unused(input.previous_output.index as usize);
},
Entry::Vacant(entry) => {
let mut meta = self.transaction_meta(&input.previous_output.hash)
.expect("No transaction metadata! Possible db corruption");
meta.denote_unused(input.previous_output.index as usize);
entry.insert(meta);
},
None => false,
} {
let mut meta =
self.transaction_meta(&input.previous_output.hash)
.unwrap_or_else(|| panic!(
// decanonization should always have meta
// because block could not have made canonical without writing meta
"No transaction metadata for {}! Corrupted DB? Reindex?",
&input.previous_output.hash
));
meta.denote_unused(input.previous_output.index as usize);
context.meta.insert(
input.previous_output.hash.clone(),
meta);
}
}
}

View File

@ -7,7 +7,8 @@ use byteorder::{LittleEndian, ByteOrder};
#[derive(Debug, Clone)]
pub struct TransactionMeta {
block_height: u32,
// first bit is coinbase flag, others - one per output listed
/// first bit indicate if transaction is a coinbase transaction
/// next bits indicate if transaction has spend outputs
bits: BitVec,
}
@ -17,7 +18,7 @@ pub enum Error {
}
impl TransactionMeta {
/// new transaction description for indexing
/// New transaction description for indexing
pub fn new(block_height: u32, outputs: usize) -> Self {
TransactionMeta {
block_height: block_height,
@ -25,22 +26,25 @@ impl TransactionMeta {
}
}
pub fn coinbase(mut self) -> Self {
self.bits.set(0, true);
self
/// New coinbase transaction
pub fn new_coinbase(block_height: u32, outputs: usize) -> Self {
let mut result = Self::new(block_height, outputs);
result.bits.set(0, true);
result
}
/// Returns true if it is a coinbase transaction
pub fn is_coinbase(&self) -> bool {
self.bits.get(0)
.expect("One bit should always exists, since it is created as usize + 1; minimum value of usize is 0; 0 + 1 = 1; qed")
.expect("One bit should always exists, since it is created as usize + 1; minimum value of usize is 0; 0 + 1 = 1; qed")
}
/// denote particular output as used
/// Denote particular output as used
pub fn denote_used(&mut self, index: usize) {
self.bits.set(index + 1 , true);
}
/// denote particular output as not used
/// Denote particular output as not used
pub fn denote_unused(&mut self, index: usize) {
self.bits.set(index + 1, false);
}
@ -61,8 +65,34 @@ impl TransactionMeta {
})
}
pub fn height(&self) -> u32 { self.block_height }
pub fn height(&self) -> u32 {
self.block_height
}
pub fn is_spent(&self, idx: usize) -> bool { self.bits.get(idx + 1).expect("Index should be verified by the caller") }
pub fn is_spent(&self, idx: usize) -> bool {
self.bits.get(idx + 1).expect("Index should be verified by the caller")
}
pub fn is_fully_spent(&self) -> bool {
// skip coinbase bit, the rest needs to true
self.bits.iter().skip(1).all(|x| x)
}
}
#[cfg(test)]
mod tests {
use super::TransactionMeta;
#[test]
fn test_is_fully_spent() {
let t = TransactionMeta::new(0, 0);
assert!(t.is_fully_spent());
let mut t = TransactionMeta::new(0, 1);
assert!(!t.is_fully_spent());
t.denote_used(0);
assert!(t.is_fully_spent());
t.denote_unused(0);
assert!(!t.is_fully_spent());
}
}

View File

@ -6,3 +6,4 @@ authors = ["debris <marek.kotewicz@gmail.com>"]
[dependencies]
serialization = { path = "../serialization" }
chain = { path = "../chain" }
primitives = { path = "../primitives" }

View File

@ -1,3 +1,4 @@
use hash::H256;
use super::Magic;
#[derive(Debug, Clone)]
@ -28,6 +29,11 @@ impl ConsensusParams {
},
}
}
pub fn is_bip30_exception(&self, hash: &H256, height: u32) -> bool {
(height == 91842 && hash == &H256::from_reversed_str("0x00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec")) ||
(height == 91880 && hash == &H256::from_reversed_str("0x00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721"))
}
}
#[cfg(test)]

View File

@ -1,8 +1,12 @@
extern crate chain;
extern crate primitives;
extern crate serialization as ser;
mod consensus;
mod magic;
pub use primitives::hash;
pub use consensus::ConsensusParams;
pub use magic::Magic;

View File

@ -16,10 +16,6 @@ pub const MAX_PUBKEYS_PER_MULTISIG: usize = 20;
/// Maximum script length in bytes
pub const MAX_SCRIPT_SIZE: usize = 10000;
/// Threshold for `nLockTime`: below this value it is interpreted as block number,
/// otherwise as UNIX timestamp.
pub const LOCKTIME_THRESHOLD: u32 = 500000000; // Tue Nov 5 00:53:20 1985 UTC
#[derive(PartialEq, Debug)]
pub enum ScriptType {
NonStandard,

View File

@ -1,9 +1,9 @@
use keys::{Public, Signature};
use chain::{
SEQUENCE_FINAL, SEQUENCE_LOCKTIME_DISABLE_FLAG,
self, SEQUENCE_FINAL, SEQUENCE_LOCKTIME_DISABLE_FLAG,
SEQUENCE_LOCKTIME_MASK, SEQUENCE_LOCKTIME_TYPE_FLAG
};
use {script, SignatureVersion, Script, TransactionInputSigner, Num};
use {SignatureVersion, Script, TransactionInputSigner, Num};
pub trait SignatureChecker {
fn check_signature(
@ -64,8 +64,8 @@ impl SignatureChecker for TransactionSignatureChecker {
// the nLockTime in the transaction.
let lock_time_u32: u32 = lock_time.into();
if !(
(self.signer.lock_time < script::LOCKTIME_THRESHOLD && lock_time_u32 < script::LOCKTIME_THRESHOLD) ||
(self.signer.lock_time >= script::LOCKTIME_THRESHOLD && lock_time_u32 >= script::LOCKTIME_THRESHOLD)
(self.signer.lock_time < chain::LOCKTIME_THRESHOLD && lock_time_u32 < chain::LOCKTIME_THRESHOLD) ||
(self.signer.lock_time >= chain::LOCKTIME_THRESHOLD && lock_time_u32 >= chain::LOCKTIME_THRESHOLD)
) {
return false;
}

View File

@ -57,6 +57,13 @@ impl ChainVerifier {
}
fn ordered_verify(&self, block: &chain::Block, at_height: u32) -> Result<(), Error> {
if !block.is_final(at_height) {
return Err(Error::NonFinalBlock);
}
let block_hash = block.hash();
let consensus_params = self.network.consensus_params();
// check that difficulty matches the adjusted level
if let Some(work) = self.work_required(block, at_height) {
if !self.skip_pow && work != block.header().nbits {
@ -68,6 +75,15 @@ impl ChainVerifier {
let coinbase_spends = block.transactions()[0].total_spends();
// bip30
for (tx_index, tx) in block.transactions.iter().enumerate() {
if let Some(meta) = self.store.transaction_meta(&tx.hash()) {
if !meta.is_fully_spent() && !consensus_params.is_bip30_exception(&block_hash, at_height) {
return Err(Error::Transaction(tx_index, TransactionError::UnspentTransactionWithTheSameHash));
}
}
}
let mut total_unspent = 0u64;
for (tx_index, tx) in block.transactions().iter().enumerate().skip(1) {
@ -168,7 +184,7 @@ impl ChainVerifier {
signer: signer,
input_index: input_index,
};
let input: Script = input.script_sig().to_vec().into();
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() {
@ -240,7 +256,7 @@ impl ChainVerifier {
// check that coinbase has a valid signature
let coinbase = &block.transactions()[0];
// is_coinbase() = true above guarantees that there is at least one input
let coinbase_script_len = coinbase.inputs[0].script_sig().len();
let coinbase_script_len = coinbase.inputs[0].script_sig.len();
if coinbase_script_len < 2 || coinbase_script_len > 100 {
return Err(Error::CoinbaseSignatureLength(coinbase_script_len));
}
@ -455,6 +471,7 @@ mod tests {
let genesis = test_data::block_builder()
.transaction()
.coinbase()
.output().value(1).build()
.build()
.transaction()
.output().value(50).build()
@ -488,6 +505,7 @@ mod tests {
let genesis = test_data::block_builder()
.transaction()
.coinbase()
.output().value(1).build()
.build()
.transaction()
.output().value(50).build()
@ -525,6 +543,7 @@ mod tests {
let genesis = test_data::block_builder()
.transaction()
.coinbase()
.output().value(1).build()
.build()
.transaction()
.output().value(50).build()

View File

@ -59,6 +59,8 @@ pub enum Error {
CoinbaseSignatureLength(usize),
/// Block size is invalid
Size(usize),
/// Block transactions are not final.
NonFinalBlock,
}
#[derive(Debug, PartialEq)]
@ -84,6 +86,8 @@ pub enum TransactionError {
SigopsP2SH(usize),
/// Coinbase transaction is found at position that is not 0
MisplacedCoinbase(usize),
/// Not fully spent transaction with the same hash already exists, bip30.
UnspentTransactionWithTheSameHash,
}
#[derive(PartialEq, Debug)]

View File

@ -121,7 +121,7 @@ pub fn transaction_sigops(transaction: &chain::Transaction) -> Result<usize, scr
if transaction.is_coinbase() { return Ok(result); }
for input in &transaction.inputs {
let input_script: Script = input.script_sig().to_vec().into();
let input_script: Script = input.script_sig.to_vec().into();
result += try!(input_script.sigop_count(false));
}