lib/bitcoin/validation.rb in bitcoin-ruby-0.0.5 vs lib/bitcoin/validation.rb in bitcoin-ruby-0.0.6
- old
+ new
@@ -1,27 +1,19 @@
# encoding: ascii-8bit
+# Validates blocks and transactions before they are accepted into the local blockchain.
+# There are two modes of validation, "syntax" and "context". "syntax" validates everything
+# that can be validated without access to the rest of the blockchain, for example that the
+# block hash matches the claimed difficulty, and the tx hashes add up to the given merkle
+# root, etc. The "context" rules include the checks that need to cross-reference data
+# against the local database, like comparing the difficulty target to the last blocks, or
+# checking for doublespends. (Suggestions for better names for these modes are welcome!)
+# Everything accepted into the local storage should at least be syntax-validated, but it
+# should be possible to skip context-validation when the current block is already known,
+# for example when checkpoints are used.
module Bitcoin::Validation
-
- # maximum size of a block (in bytes)
- MAX_BLOCK_SIZE = 1_000_000
-
- # maximum number of signature operations in a block
- MAX_BLOCK_SIGOPS = MAX_BLOCK_SIZE / 50
-
- # maximum integer value
- INT_MAX = 0xffffffff
-
- # number of confirmations required before coinbase tx can be spent
- COINBASE_MATURITY = 100
-
- # interval (in blocks) for difficulty retarget
- RETARGET = 2016
-
- # interval (in blocks) for mining reward reduction
- REWARD_DROP = 210_000
-
+
class ValidationError < StandardError
end
class Block
@@ -36,10 +28,15 @@
if Bitcoin.namecoin?
RULES[:syntax] -= [:bits, :coinbase, :coinbase_scriptsig, :mrkl_root]
RULES[:context] -= [:difficulty, :coinbase_value]
end
+ if Bitcoin.litecoin?
+ RULES[:syntax] -= [:bits]
+ RULES[:syntax] += [:scrypt_bits]
+ end
+
# validate block rules. +opts+ are:
# rules:: which rulesets to validate (default: [:syntax, :context])
# raise_errors:: whether to raise ValidationError on failure (default: false)
def validate(opts = {})
return true if KNOWN_EXCEPTIONS.include?(block.hash)
@@ -80,10 +77,17 @@
actual = block.hash.to_i(16)
expected = Bitcoin.decode_compact_bits(block.bits).to_i(16)
actual <= expected || [actual, expected]
end
+ # check that block hash matches claimed bits using Scrypt hash
+ def scrypt_bits
+ actual = block.recalc_block_scrypt_hash.to_i(16)
+ expected = Bitcoin.decode_compact_bits(block.bits).to_i(16)
+ actual <= expected || [actual, expected]
+ end
+
# check that block time is not greater than max
def max_timestamp
time, max = block.time, Time.now.to_i + 2*60*60
time < max || [time, max]
end
@@ -100,11 +104,11 @@
size.between?(2,100) || [size, 2, 100]
end
# check that coinbase value is valid; no more than reward + fees
def coinbase_value
- reward = ((50.0 / (2 ** (store.get_depth / REWARD_DROP.to_f).floor)) * 1e8).to_i
+ reward = ((50.0 / (2 ** (store.get_depth / Bitcoin::REWARD_DROP.to_f).floor)) * 1e8).to_i
fees = 0
block.tx[1..-1].map.with_index do |t, idx|
val = tx_validators[idx]
fees += t.in.map.with_index {|i, idx|
val.prev_txs[idx].out[i.prev_out_index].value rescue 0
@@ -125,11 +129,11 @@
@prev_block && @prev_block.hash == block.prev_block.reverse_hth
end
# check that bits satisfy required difficulty
def difficulty
- return true if Bitcoin.network_name == :testnet3
+ return true if Bitcoin.network[:no_difficulty] == true
block.bits == next_bits_required || [block.bits, next_bits_required]
end
# check that timestamp is newer than the median of the last 11 blocks
def min_timestamp
@@ -144,22 +148,26 @@
min_time = (rem == 0 ? times[mid-1, 2].inject(:+) / 2.0 : times[mid])
block.time > min_time || [block.time, min_time]
end
- # check transactions
+ # Run all syntax checks on transactions
def transactions_syntax
+ # check if there are no double spends within this block
+ return false if block.tx.map(&:in).flatten.map {|i| [i.prev_out, i.prev_out_index] }.uniq! != nil
+
tx_validators.all?{|v|
begin
v.validate(rules: [:syntax], raise_errors: true)
rescue ValidationError
store.log.info { $!.message }
return false
end
}
end
+ # Run all context checks on transactions
def transactions_context
tx_validators.all?{|v|
begin
v.validate(rules: [:context], raise_errors: true)
rescue ValidationError
@@ -167,25 +175,46 @@
return false
end
}
end
+ # Get validators for all tx objects in the current block
def tx_validators
- @tx_validators ||= block.tx[1..-1].map {|tx| tx.validator(store, block) }
+ @tx_validators ||= block.tx[1..-1].map {|tx| tx.validator(store, block, self)}
end
+ # Fetch all prev_txs that will be needed for validation
+ # Used for optimization in tx validators
+ def prev_txs_hash
+ @prev_tx_hash ||= (
+ inputs = block.tx[1..-1].map {|tx| tx.in }.flatten
+ txs = store.get_txs(inputs.map{|i| i.prev_out.reverse_hth })
+ Hash[*txs.map {|tx| [tx.hash, tx] }.flatten]
+ )
+ end
+
+ # Fetch all prev_outs that already have a next_in, i.e. are already spent.
+ def spent_outs_txins
+ @spent_outs_txins ||= (
+ next_ins = store.get_txins_for_txouts(block.tx[1..-1].map(&:in).flatten.map.with_index {|txin, idx| [txin.prev_out.reverse_hth, txin.prev_out_index] })
+ # Only returns next_ins that are in blocks in the main chain
+ next_ins.select {|i| store.get_block_id_for_tx_id(i.tx_id) }
+ )
+ end
+
def next_bits_required
- index = (prev_block.depth + 1) / RETARGET
+ retarget = (Bitcoin.network[:retarget_interval] || Bitcoin::RETARGET_INTERVAL)
+ index = (prev_block.depth + 1) / retarget
max_target = Bitcoin.decode_compact_bits(Bitcoin.network[:proof_of_work_limit]).to_i(16)
return Bitcoin.network[:proof_of_work_limit] if index == 0
- return prev_block.bits if (prev_block.depth + 1) % RETARGET != 0
+ return prev_block.bits if (prev_block.depth + 1) % retarget != 0
last = store.db[:blk][hash: prev_block.hash.htb.blob]
first = store.db[:blk][hash: last[:prev_hash].blob]
- (RETARGET-2).times { first = store.db[:blk][hash: first[:prev_hash].blob] }
+ (retarget - 2).times { first = store.db[:blk][hash: first[:prev_hash].blob] }
nActualTimespan = last[:time] - first[:time]
- nTargetTimespan = RETARGET * 600
+ nTargetTimespan = retarget * 600
nActualTimespan = [nActualTimespan, nTargetTimespan/4].max
nActualTimespan = [nActualTimespan, nTargetTimespan*4].min
target = Bitcoin.decode_compact_bits(last[:bits]).to_i(16)
@@ -200,15 +229,15 @@
]
end
class Tx
- attr_accessor :tx, :store, :error
+ attr_accessor :tx, :store, :error, :block_validator
RULES = {
syntax: [:hash, :lists, :max_size, :output_values, :inputs, :lock_time, :standard],
- context: [:prev_out, :signatures, :spent, :input_values, :output_sum]
+ context: [:prev_out, :signatures, :not_spent, :input_values, :output_sum]
}
# validate tx rules. +opts+ are:
# rules:: which rulesets to validate (default: [:syntax, :context])
# raise_errors:: whether to raise ValidationError on failure (default: false)
@@ -234,14 +263,18 @@
"6a26d2ecb67f27d1fa5524763b49029d7106e91e3cc05743073461a719776192",
# p2sh with invalid inner script, accepted by old miner before 4-2012 switchover (testnet)
"b3c19d78b4953b694717a47d9852f8ea1ccd4cf93a45ba2e43a0f97d7cdb2655"
]
- # setup new validator for given +tx+, validating context with +store+.
- # also needs the +block+ to find prev_outs for chains of tx inside one block.
- def initialize tx, store, block = nil
+ # Setup new validator for given +tx+, validating context with +store+.
+ # Also needs the +block+ that includes the tx to be validated, to find
+ # prev_outs for chains of txs inside the block.
+ # Optionally accepts the validator object for the block, to optimize fetching
+ # prev_txs and checking for doublespends.
+ def initialize(tx, store, block = nil, block_validator = nil)
@tx, @store, @block, @errors = tx, store, block, []
+ @block_validator = block_validator
end
# check that tx hash matches data
def hash
generated_hash = tx.generate_hash(tx.to_payload)
@@ -253,11 +286,11 @@
(tx.in.any? && tx.out.any?) || [tx.in.size, tx.out.size]
end
# check that tx size doesn't exceed MAX_BLOCK_SIZE.
def max_size
- tx.to_payload.bytesize <= MAX_BLOCK_SIZE || [tx.to_payload.bytesize, MAX_BLOCK_SIZE]
+ tx.to_payload.bytesize <= Bitcoin::MAX_BLOCK_SIZE || [tx.to_payload.bytesize, Bitcoin::MAX_BLOCK_SIZE]
end
# check that total output value doesn't exceed MAX_MONEY.
def output_values
total = tx.out.inject(0) {|e, out| e + out.value }
@@ -270,11 +303,11 @@
tx.inputs.none?(&:coinbase?) || [tx.inputs.index(tx.inputs.find(&:coinbase?))]
end
# check that lock_time doesn't exceed INT_MAX
def lock_time
- tx.lock_time <= INT_MAX || [tx.lock_time, INT_MAX]
+ tx.lock_time <= Bitcoin::UINT32_MAX || [tx.lock_time, Bitcoin::UINT32_MAX]
end
# check that min_size is at least 86 bytes
# (smaller tx can't be valid / do anything useful)
def min_size
@@ -306,20 +339,27 @@
def signatures
sigs = tx.in.map.with_index {|txin, idx| tx.verify_input_signature(idx, prev_txs[idx], (@block ? @block.time : 0)) }
sigs.all? || sigs.map.with_index {|s, i| s ? nil : i }.compact
end
- # check that none of the prev_outs are already spent in the main chain
- def spent
- spent = tx.in.map.with_index {|txin, idx|
- next false if @block && @block.tx.include?(prev_txs[idx])
- next false unless next_in = prev_txs[idx].out[txin.prev_out_index].get_next_in
- next false unless next_tx = next_in.get_tx
- next false unless next_block = next_tx.get_block
- next_block.chain == Bitcoin::Storage::Backends::StoreBase::MAIN
- }
- spent.none? || spent.map.with_index {|s, i| s ? i : nil }
+ # check that none of the prev_outs are already spent in the main chain or in the current block
+ def not_spent
+ # if we received cached spents, use it
+ return block_validator.spent_outs_txins.empty? if block_validator
+
+ # find all spent txouts
+ next_ins = store.get_txins_for_txouts(tx.in.map.with_index {|txin, idx| [txin.prev_out.reverse_hth, txin.prev_out_index] })
+
+ # no txouts found spending these txins, we can safely return true
+ return true if next_ins.empty?
+
+ # there were some txouts spending these txins, verify that they are not on the main chain
+ next_ins.select! {|i| i.get_tx.blk_id } # blk_id is only set for tx in the main chain
+ return true if next_ins.empty?
+
+ # now we know some txouts are already spent, return tx_idxs for debugging purposes
+ return next_ins.map {|i| i.get_prev_out.tx_idx }
end
# check that the total input value doesn't exceed MAX_MONEY
def input_values
total_in < Bitcoin::network[:max_money] || [total_in, Bitcoin::network[:max_money]]
@@ -339,21 +379,12 @@
# collect prev_txs needed to verify the inputs of this tx.
# only returns tx that are in a block in the main chain or the current block.
def prev_txs
@prev_txs ||= tx.in.map {|i|
- prev_tx = store.get_tx(i.prev_out.reverse_hth)
- next prev_tx if store.class.name =~ /UtxoStore/ && prev_tx
- next nil if !prev_tx && !@block
-
- if store.class.name =~ /SequelStore/
- block = store.db[:blk][id: prev_tx.blk_id] if prev_tx
- next prev_tx if block && block[:chain] == 0
- else
- next prev_tx if prev_tx && prev_tx.get_block && prev_tx.get_block.chain == 0
- end
- next nil if !@block
- @block.tx.find {|t| t.binary_hash == i.prev_out }
+ prev_tx = block_validator ? block_validator.prev_txs_hash[i.prev_out.reverse_hth] : store.get_tx(i.prev_out.reverse_hth)
+ next prev_tx if prev_tx && prev_tx.blk_id # blk_id is set only if it's in the main chain
+ @block.tx.find {|t| t.binary_hash == i.prev_out } if @block
}.compact
end
def total_in