lib/bitcoin/protocol/tx.rb in bitcoin-ruby-0.0.10 vs lib/bitcoin/protocol/tx.rb in bitcoin-ruby-0.0.11

- old
+ new

@@ -5,10 +5,13 @@ module Bitcoin module Protocol class Tx + MARKER = 0 + FLAG = 1 + # transaction hash attr_reader :hash # inputs (Array of TxIn) attr_reader :in @@ -26,10 +29,13 @@ attr_accessor :lock_time # parsed / evaluated input scripts cached for later use attr_reader :scripts + attr_accessor :marker + attr_accessor :flag + alias :inputs :in alias :outputs :out # compare to another tx def ==(other) @@ -61,17 +67,26 @@ def add_out(output); (@out ||= []) << output; end # parse raw binary data def parse_data_from_io(data) buf = data.is_a?(String) ? StringIO.new(data) : data - payload_start = buf.pos @ver = buf.read(4).unpack("V")[0] return false if buf.eof? in_size = Protocol.unpack_var_int_from_io(buf) + + # segwit serialization format is defined by https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki + witness = false + if in_size.zero? + @marker = 0 + @flag = buf.read(1).unpack('c').first + in_size = Protocol.unpack_var_int_from_io(buf) + witness = true + end + @in = [] in_size.times{ break if buf.eof? @in << TxIn.from_io(buf) } @@ -85,16 +100,23 @@ @out << TxOut.from_io(buf) } return false if buf.eof? + if witness + in_size.times do |i| + witness_count = Protocol.unpack_var_int_from_io(buf) + witness_count.times do + size = Protocol.unpack_var_int_from_io(buf) + @in[i].script_witness.stack << buf.read(size) + end + end + end + @lock_time = buf.read(4).unpack("V")[0] - payload_end = buf.pos; - buf.seek(payload_start) - @payload = buf.read( payload_end-payload_start ) - @hash = hash_from_payload(@payload) + @hash = hash_from_payload(to_old_payload) if buf.eof? true else data.is_a?(StringIO) ? buf : buf.read @@ -103,19 +125,39 @@ alias :parse_data :parse_data_from_io # output transaction in raw binary format def to_payload + witness? ? to_witness_payload : to_old_payload + end + + def to_old_payload pin = "" @in.each{|input| pin << input.to_payload } pout = "" @out.each{|output| pout << output.to_payload } [@ver].pack("V") << Protocol.pack_var_int(@in.size) << pin << Protocol.pack_var_int(@out.size) << pout << [@lock_time].pack("V") end + # output transaction in raw binary format with witness + def to_witness_payload + buf = [@ver, MARKER, FLAG].pack('Vcc') + buf << Protocol.pack_var_int(@in.length) << @in.map(&:to_payload).join + buf << Protocol.pack_var_int(@out.length) << @out.map(&:to_payload).join + buf << witness_payload << [@lock_time].pack('V') + buf + end + def witness_payload + @in.map { |i| i.script_witness.to_payload }.join + end + + def witness? + !@in.find { |i| !i.script_witness.empty? }.nil? + end + SIGHASH_TYPE = { all: 1, none: 2, single: 3, anyonecanpay: 128 } # generate a signature hash for input +input_idx+. # either pass the +outpoint_tx+ or the +script_pubkey+ directly. def signature_hash_for_input(input_idx, subscript, hash_type=nil) @@ -168,10 +210,54 @@ buf = [ [@ver].pack("V"), in_size, pin, out_size, pout, [@lock_time, hash_type].pack("VV") ].join Digest::SHA256.digest( Digest::SHA256.digest( buf ) ) end + # generate a witness signature hash for input +input_idx+. + # https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki + def signature_hash_for_witness_input(input_idx, witness_program, prev_out_value, witness_script = nil, hash_type=nil, skip_separator_index = 0) + return "\x01".ljust(32, "\x00") if input_idx >= @in.size # ERROR: SignatureHash() : input_idx=%d out of range + + hash_type ||= SIGHASH_TYPE[:all] + + script = Bitcoin::Script.new(witness_program) + raise "ScriptPubkey does not contain witness program." unless script.is_witness? + + hash_prevouts = Digest::SHA256.digest(Digest::SHA256.digest(@in.map{|i| [i.prev_out_hash, i.prev_out_index].pack("a32V")}.join)) + hash_sequence = Digest::SHA256.digest(Digest::SHA256.digest(@in.map{|i|i.sequence}.join)) + outpoint = [@in[input_idx].prev_out_hash, @in[input_idx].prev_out_index].pack("a32V") + amount = [prev_out_value].pack("Q") + nsequence = @in[input_idx].sequence + + if script.is_witness_v0_keyhash? + script_code = [["1976a914", script.get_hash160, "88ac"].join].pack("H*") + elsif script.is_witness_v0_scripthash? + raise "witness script does not match script pubkey" unless Bitcoin::Script.to_witness_p2sh_script(Digest::SHA256.digest(witness_script).bth) == witness_program + script = skip_separator_index > 0 ? Bitcoin::Script.new(witness_script).subscript_codeseparator(skip_separator_index) : witness_script + script_code = Bitcoin::Protocol.pack_var_string(script) + end + + hash_outputs = Digest::SHA256.digest(Digest::SHA256.digest(@out.map{|o|o.to_payload}.join)) + + case (hash_type & 0x1f) + when SIGHASH_TYPE[:single] + hash_outputs = input_idx >= @out.size ? "\x00".ljust(32, "\x00") : Digest::SHA256.digest(Digest::SHA256.digest(@out[input_idx].to_payload)) + hash_sequence = "\x00".ljust(32, "\x00") + when SIGHASH_TYPE[:none] + hash_sequence = hash_outputs = "\x00".ljust(32, "\x00") + end + + if (hash_type & SIGHASH_TYPE[:anyonecanpay]) != 0 + hash_prevouts = hash_sequence ="\x00".ljust(32, "\x00") + end + + buf = [ [@ver].pack("V"), hash_prevouts, hash_sequence, outpoint, + script_code, amount, nsequence, hash_outputs, [@lock_time, hash_type].pack("VV")].join + + Digest::SHA256.digest( Digest::SHA256.digest( buf ) ) + end + # verify input signature +in_idx+ against the corresponding # output in +outpoint_tx+ # outpoint # # options are: verify_sigpushonly, verify_minimaldata, verify_cleanstack, verify_dersig, verify_low_s, verify_strictenc @@ -202,10 +288,67 @@ return false if opts[:verify_cleanstack] && !@scripts[in_idx].stack.empty? return sig_valid end + # verify witness input signature +in_idx+ against the corresponding + # output in +outpoint_tx+ + # outpoint + # + # options are: verify_sigpushonly, verify_minimaldata, verify_cleanstack, verify_dersig, verify_low_s, verify_strictenc + def verify_witness_input_signature(in_idx, outpoint_tx_or_script, prev_out_amount, block_timestamp=Time.now.to_i, opts={}) + if @enable_bitcoinconsensus + return bitcoinconsensus_verify_script(in_idx, outpoint_tx_or_script, block_timestamp, opts) + end + + outpoint_idx = @in[in_idx].prev_out_index + script_sig = '' + + # If given an entire previous transaction, take the script from it + script_pubkey = if outpoint_tx_or_script.respond_to?(:out) + Bitcoin::Script.new(outpoint_tx_or_script.out[outpoint_idx].pk_script) + else + # Otherwise, it's already a script. + Bitcoin::Script.new(outpoint_tx_or_script) + end + + if script_pubkey.is_p2sh? + redeem_script = Bitcoin::Script.new(@in[in_idx].script_sig).get_pubkey + script_pubkey = Bitcoin::Script.new(redeem_script.htb) if Bitcoin.hash160(redeem_script) == script_pubkey.get_hash160 # P2SH-P2WPKH or P2SH-P2WSH + end + + @in[in_idx].script_witness.stack.each{|s|script_sig << Bitcoin::Script.pack_pushdata(s)} + code_separator_index = 0 + + if script_pubkey.is_witness_v0_keyhash? # P2WPKH + @scripts[in_idx] = Bitcoin::Script.new(script_sig, Bitcoin::Script.to_hash160_script(script_pubkey.get_hash160)) + elsif script_pubkey.is_witness_v0_scripthash? # P2WSH + witness_hex = @in[in_idx].script_witness.stack.last.bth + witness_script = Bitcoin::Script.new(witness_hex.htb) + return false unless Bitcoin.sha256(witness_hex) == script_pubkey.get_hash160 + @scripts[in_idx] = Bitcoin::Script.new(script_sig, Bitcoin::Script.to_p2sh_script(Bitcoin.hash160(witness_hex))) + else + return false + end + + return false if opts[:verify_sigpushonly] && !@scripts[in_idx].is_push_only?(script_sig) + return false if opts[:verify_minimaldata] && !@scripts[in_idx].pushes_are_canonical? + sig_valid = @scripts[in_idx].run(block_timestamp, opts) do |pubkey,sig,hash_type,subscript| + if script_pubkey.is_witness_v0_keyhash? + hash = signature_hash_for_witness_input(in_idx, script_pubkey.to_payload, prev_out_amount, nil, hash_type) + elsif script_pubkey.is_witness_v0_scripthash? + hash = signature_hash_for_witness_input(in_idx, script_pubkey.to_payload, prev_out_amount, witness_hex.htb, hash_type, code_separator_index) + code_separator_index += 1 if witness_script.codeseparator_count > code_separator_index + end + Bitcoin.verify_signature( hash, sig, pubkey.unpack("H*")[0] ) + end + # BIP62 rule #6 + return false if opts[:verify_cleanstack] && !@scripts[in_idx].stack.empty? + + return sig_valid + end + def bitcoinconsensus_verify_script(in_idx, outpoint_tx_or_script, block_timestamp=Time.now.to_i, opts={}) raise "Bitcoin::BitcoinConsensus shared library not found" unless Bitcoin::BitcoinConsensus.lib_available? # If given an entire previous transaction, take the script from it script_pubkey = if outpoint_tx_or_script.respond_to?(:out) @@ -227,16 +370,16 @@ Bitcoin::BitcoinConsensus.verify_script(in_idx, script_pubkey, payload, flags) end # convert to ruby hash (see also #from_hash) def to_hash(options = {}) - @hash ||= hash_from_payload(to_payload) + @hash ||= hash_from_payload(to_old_payload) h = { 'hash' => @hash, 'ver' => @ver, # 'nid' => normalized_hash, 'vin_sz' => @in.size, 'vout_sz' => @out.size, 'lock_time' => @lock_time, 'size' => (@payload ||= to_payload).bytesize, - 'in' => @in.map{|i| i.to_hash(options) }, + 'in' => @in.map{|i|i.to_hash(options)}, 'out' => @out.map{|o| o.to_hash(options) } } h['nid'] = normalized_hash if options[:with_nid] h end @@ -256,21 +399,26 @@ def self.from_hash(h, do_raise=true) tx = new(nil) tx.ver, tx.lock_time = (h['ver'] || h['version']), h['lock_time'] ins = h['in'] || h['inputs'] outs = h['out'] || h['outputs'] - ins .each{|input| tx.add_in TxIn.from_hash(input) } + ins .each{|input| + tx.add_in(TxIn.from_hash(input)) + } outs.each{|output| tx.add_out TxOut.from_hash(output) } - tx.instance_eval{ @hash = hash_from_payload(@payload = to_payload) } + tx.instance_eval{ @hash = hash_from_payload(@payload = to_old_payload) } if h['hash'] && (h['hash'] != tx.hash) raise "Tx hash mismatch! Claimed: #{h['hash']}, Actual: #{tx.hash}" if do_raise end tx end # convert ruby hash to raw binary - def self.binary_from_hash(h); from_hash(h).to_payload; end + def self.binary_from_hash(h) + tx = from_hash(h) + tx.to_payload + end # parse json representation def self.from_json(json_string); from_hash( JSON.load(json_string) ); end # convert json representation to raw binary @@ -364,9 +512,21 @@ def normalized_hash signature_hash_for_input(-1, nil, SIGHASH_TYPE[:all]).reverse.hth end alias :nhash :normalized_hash + + # get witness hash + def witness_hash + hash_from_payload(to_witness_payload) + end + + # sort transaction inputs and outputs under BIP 69 + # https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki + def lexicographical_sort! + inputs.sort_by!{|i| [i.previous_output, i.prev_out_index]} + outputs.sort_by!{|o| [o.amount, o.pk_script.bth]} + end end end end