Commit 82a2f1d6d65f8c77a6298817fb13226fc0a12fe5

Authored by Marius Hanne
1 parent d44357b9c9
Exists in tx_validator

starting tx validator

Showing 4 changed files with 288 additions and 1 deletions Side-by-side Diff

... ... @@ -16,6 +16,8 @@
16 16 autoload :Logger, 'bitcoin/logger'
17 17 autoload :Key, 'bitcoin/key'
18 18 autoload :Config, 'bitcoin/config'
  19 + autoload :KeyGenerator, 'bitcoin/key'
  20 + autoload :TxValidator, 'bitcoin/tx_validator'
19 21  
20 22 module Network
21 23 autoload :ConnectionHandler, 'bitcoin/network/connection_handler'
lib/bitcoin/script.rb
... ... @@ -331,8 +331,12 @@
331 331 to_pubkey_script_sig(*a)
332 332 end
333 333  
334   -
335 334 ## OPCODES
  335 +
  336 + # count signature operations in this script
  337 + def sig_op_count
  338 + @chunks.select{|c| [OP_CHECKSIG, OP_CHECKSIGVERIFY].include?(c) }.count
  339 + end
336 340  
337 341 # Does nothing
338 342 def op_nop
lib/bitcoin/tx_validator.rb
  1 +module Bitcoin
  2 +
  3 + class ValidationError < StandardError
  4 + end
  5 +
  6 + class TxValidator
  7 +
  8 + MAX_BLOCK_SIZE = 1_000_000
  9 + MAX_MONEY = 21_000_000 * 1e8
  10 + MAX_INT = 4294967295
  11 +
  12 + def initialize store, tx
  13 + @store = store
  14 + @tx = tx
  15 + end
  16 +
  17 + def validate
  18 + # tx.verify_input_signature(0, outpoint_tx).should == true
  19 +
  20 + # puts "Validating tx #{@tx.hash}"
  21 +
  22 + [
  23 + :syntax,
  24 + :in_out_size,
  25 + :size,
  26 + :money_range,
  27 + :no_coinbase_inputs,
  28 + :lock_time,
  29 + :min_size,
  30 + :opcount,
  31 + :is_standard,
  32 + :no_duplicate,
  33 + :no_used_outputs,
  34 + :outputs_present,
  35 + :output_index,
  36 + :coinbase_maturity,
  37 + :signatures,
  38 + :no_doublespend,
  39 + :sum_money_range,
  40 + :total_output_value,
  41 + :fee,
  42 + ].each do |rule|
  43 + res = send("validate_#{rule}")
  44 + raise ValidationError.new(rule) unless res
  45 + end
  46 +
  47 + # puts "Tx #{@tx.hash} is valid!"
  48 +
  49 + # :store
  50 + # :add_to_wallet
  51 + # :relay
  52 + # :revalidate_orphans
  53 + true
  54 + end
  55 +
  56 +
  57 + # Check syntactic correctness
  58 + def validate_syntax
  59 + true # implicitly done by parser
  60 + end
  61 +
  62 + # Make sure neither in or out lists are empty
  63 + def validate_in_out_size
  64 + @tx.in.any? && @tx.out.any?
  65 + end
  66 +
  67 + # Size in bytes < MAX_BLOCK_SIZE
  68 + def validate_size
  69 + @tx.payload.bytesize < MAX_BLOCK_SIZE
  70 + end
  71 +
  72 +
  73 + # Each output value, as well as the total, must be in legal money range
  74 + def validate_money_range
  75 + @tx.out.map(&:value).inject{|a,b| a+=b;a} < MAX_MONEY
  76 + end
  77 +
  78 + # Make sure none of the inputs have hash=0, n=-1 (coinbase transactions)
  79 + def validate_no_coinbase_inputs
  80 + !@tx.in.map{|i| i.prev_out == "\x00"*32 && i.prev_out_index == 4294967295 }.any?
  81 + true # TODO
  82 + end
  83 +
  84 + # Check that nLockTime <= INT_MAX[1],
  85 + def validate_lock_time
  86 + @tx.lock_time <= MAX_INT
  87 + end
  88 +
  89 + # size in bytes >= 100[2],
  90 + def validate_min_size
  91 + @tx.payload.bytesize >= 100
  92 + end
  93 +
  94 + # and sig opcount <= 2[3]
  95 + def validate_opcount
  96 + @tx.out.map{|o| Script.new(o.pk_script).sig_op_count}.inject{|a,b| a+=b;a} <= 2
  97 + true # TODO
  98 + end
  99 +
  100 + # Reject "nonstandard" transactions: scriptSig doing anything other than pushing numbers on the stack, or scriptPubkey not matching the two usual forms[4]
  101 + def validate_is_standard
  102 + true # TODO
  103 + end
  104 +
  105 + # Reject if we already have matching tx in the pool, or in a block in the main branch
  106 + def validate_no_duplicate
  107 + !@store.has_tx(@tx.hash)
  108 + end
  109 +
  110 + # Reject if any other tx in the pool uses the same transaction output as one used by this tx.[5]
  111 + def validate_no_used_outputs # TODO: sequence handling
  112 + true # TODO
  113 + end
  114 +
  115 + # For each input, look in the main branch and the transaction pool to find the referenced output transaction. If the output transaction is missing for any input, this will be an orphan transaction. Add to the orphan transactions, if a matching transaction is not in there already.
  116 + def validate_outputs_present
  117 + true # TODO
  118 + end
  119 +
  120 + # For each input, if we are using the nth output of the earlier transaction, but it has fewer than n+1 outputs, reject this transaction
  121 + def validate_output_index
  122 + true # TODO
  123 + end
  124 +
  125 + # For each input, if the referenced output transaction is coinbase (i.e. only 1 input, with hash=0, n=-1), it must have at least COINBASE_MATURITY confirmations; else reject this transaction
  126 + def validate_coinbase_maturity
  127 + true # TODO
  128 + end
  129 +
  130 + # Verify crypto signatures for each input; reject if any are bad
  131 + def validate_signatures
  132 + @tx.in.each_with_index do |txin, idx|
  133 + prev_tx = @store.get_tx(Bitcoin::hth(txin.prev_out.reverse))
  134 + next unless prev_tx # TODO
  135 +
  136 + result = @tx.verify_input_signature(idx, prev_tx)
  137 +
  138 + # txout = prev_tx.out[txin.prev_out_index]
  139 + # script = Script.new(txin.script_sig + txout.pk_script)
  140 +
  141 + # debug = []
  142 + # result = script.run(debug) do |pubkey, sig, hash_type|
  143 + # hash = @tx.signature_hash_for_input(idx, nil, txout.pk_script)
  144 + # Bitcoin.verify_signature(hash, sig, pubkey.unpack("H*")[0])
  145 + # end
  146 +
  147 + # binding.pry if result != true
  148 +
  149 + return false unless result
  150 + end
  151 + return true
  152 + end
  153 +
  154 + # For each input, if the referenced output has already been spent by a transaction in the main branch, reject this transaction[6]
  155 + def validate_no_doublespend
  156 + true # TODO
  157 + end
  158 +
  159 + # Using the referenced output transactions to get input values, check that each input value, as well as the sum, are in legal money range
  160 + def validate_sum_money_range
  161 + true # TODO
  162 + end
  163 +
  164 + # Reject if the sum of input values < sum of output values
  165 + def validate_total_output_value
  166 + true # TODO
  167 + end
  168 +
  169 + # Reject if transaction fee (defined as sum of input values minus sum of output values) would be too low to get into an empty block
  170 + def validate_fee
  171 + true # TODO
  172 + end
  173 +
  174 + # Add to transaction pool[7]
  175 + def store
  176 +
  177 + end
  178 +
  179 + # "Add to wallet if mine"
  180 + def add_to_wallet
  181 +
  182 + end
  183 +
  184 + # Relay transaction to peers
  185 + def relay
  186 +
  187 + end
  188 +
  189 + # For each orphan transaction that uses this one as one of its inputs, run all these steps (including this one) recursively on that orphan
  190 + def revalidate_orphans
  191 +
  192 + end
  193 +
  194 + end
  195 +
  196 +end
spec/bitcoin/tx_validator_spec.rb
  1 +require_relative 'spec_helper.rb'
  2 +
  3 +describe 'Bitcoin::Validator' do
  4 +
  5 + before do
  6 + @tx = Bitcoin::Protocol::Tx.new( fixtures_file('rawtx-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16.bin') )
  7 + @prev_tx = Bitcoin::Protocol::Tx.new( fixtures_file('rawtx-0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9.bin') )
  8 +
  9 + @store = Bitcoin::Storage.dummy({})
  10 + @store.store_tx(@prev_tx)
  11 + end
  12 +
  13 + def validate(in_tx)
  14 + validator = Bitcoin::TxValidator.new(@store, in_tx)
  15 + validator.validate.should == true
  16 + end
  17 +
  18 + def assert_error(message, &block)
  19 + proc { block.call }.should.raise(Bitcoin::ValidationError).message.should == message
  20 + end
  21 +
  22 + it "should validate" do
  23 + validate(@tx).should == true
  24 + end
  25 +
  26 +# it "should not validate if syntax is incorrect"
  27 +
  28 + it "should not validate if in list empty" do
  29 + @tx.instance_eval { @in = [] }
  30 + assert_error("in_out_size") { validate(@tx) }
  31 + end
  32 +
  33 + it "should not validate if out list empty" do
  34 + @tx.instance_eval { @out = [] }
  35 + assert_error("in_out_size") { validate(@tx) }
  36 + end
  37 +
  38 + it "should not validate if too lange" do
  39 + @tx.instance_eval do
  40 + def @payload.bytesize
  41 + 1_000_001
  42 + end
  43 + end
  44 + assert_error("size") { validate(@tx) }
  45 + end
  46 +
  47 + it "should not validate if output total is outside legal money range" do
  48 + @tx.out[0].value = 20_000_000_000_000_00
  49 + @tx.out[1].value = 2_000_000_000_000_00
  50 + assert_error("money_range") { validate(@tx) }
  51 + end
  52 +
  53 +# it "should not validate if tx references coinbase tx" do
  54 +# # TODO: ???
  55 +# end
  56 +
  57 + it "should not validate if locktime is too high" do
  58 + @tx.lock_time = 4294967296
  59 + assert_error("lock_time") { validate(@tx) }
  60 + end
  61 +
  62 + it "should not validate if tx is too small (to possibly do anything useful)" do
  63 + @tx.instance_eval do
  64 + def @payload.bytesize
  65 + 99
  66 + end
  67 + end
  68 + assert_error("min_size") { validate(@tx) }
  69 + end
  70 +
  71 +# it "should not validate if sig opcount > 2" do
  72 +# # TODO: ???
  73 +# end
  74 +
  75 + it "should not validate duplicate transaction" do
  76 + @store.store_tx(@tx)
  77 + assert_error("no_duplicate") { validate(@tx) }
  78 + end
  79 +
  80 + it "should not validate transaction with invalid signatures" do
  81 + @tx.in[0].script_sig[5..10] = "foobar"
  82 + assert_error("signatures") { validate(@tx) }
  83 + end
  84 +
  85 +end