lib/segwit/tx.rb in openassets-ruby-0.6.1 vs lib/segwit/tx.rb in openassets-ruby-0.6.2
- old
+ new
@@ -1,76 +1,235 @@
-# extension for Bitcoin::Protocol::Tx to support segwit
-class Bitcoin::Protocol::Tx
- include Bitcoin::Util
+# encoding: ascii-8bit
- attr_reader :witness
+require 'bitcoin/script'
- def initialize(data=nil)
- @ver, @lock_time, @in, @out, @scripts, @witness = 1, 0, [], [], [], Bitcoin::Protocol::TxWitness.new
- @enable_bitcoinconsensus = !!ENV['USE_BITCOINCONSENSUS']
- if data
- begin
- parse_witness_data_from_io(data) unless parse_data_from_io(data).is_a?(TrueClass)
- rescue Exception
- parse_witness_data_from_io(data)
+module Bitcoin
+ module Protocol
+
+ class Tx
+
+ # witness (TxWitness)
+ attr_reader :witness
+
+ # create tx from raw binary +data+
+ def initialize(data=nil)
+ @ver, @lock_time, @in, @out, @scripts, @witness = 1, 0, [], [], [], TxWitness.new
+ @enable_bitcoinconsensus = !!ENV['USE_BITCOINCONSENSUS']
+ if data
+ begin
+ parse_witness_data_from_io(data) unless parse_data_from_io(data).is_a?(TrueClass)
+ rescue Exception
+ parse_witness_data_from_io(data)
+ end
+ end
end
- end
- end
- # get witness hash
- def witness_hash
- hash_from_payload(to_witness_payload)
- end
+ # parse witness raw binary data
+ # serialization format is defined by https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki
+ def parse_witness_data_from_io(data)
+ buf = data.is_a?(String) ? StringIO.new(data) : data
- # parse raw data which include witness data
- # serialization format is defined by https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki
- def parse_witness_data_from_io(data)
- buf = data.is_a?(String) ? StringIO.new(data) : data
+ @ver = buf.read(4).unpack("V").first
- @ver = buf.read(4).unpack("V").first
+ @marker = buf.read(1).unpack("c").first
- @marker = buf.read(1).unpack("c").first
+ @flag = buf.read(1).unpack("c").first
- @flag = buf.read(1).unpack("c").first
+ in_size = Bitcoin::Protocol.unpack_var_int_from_io(buf)
+ @in = []
+ in_size.times{
+ break if buf.eof?
+ @in << Bitcoin::Protocol::TxIn.from_io(buf)
+ }
- in_size = Bitcoin::Protocol.unpack_var_int_from_io(buf)
- @in = []
- in_size.times{
- break if buf.eof?
- @in << Bitcoin::Protocol::TxIn.from_io(buf)
- }
+ out_size = Bitcoin::Protocol.unpack_var_int_from_io(buf)
+ @out = []
+ out_size.times{
+ break if buf.eof?
+ @out << Bitcoin::Protocol::TxOut.from_io(buf)
+ }
- out_size = Bitcoin::Protocol.unpack_var_int_from_io(buf)
- @out = []
- out_size.times{
- break if buf.eof?
- @out << Bitcoin::Protocol::TxOut.from_io(buf)
- }
+ @witness = Bitcoin::Protocol::TxWitness.new
+ in_size.times{
+ witness_count = Bitcoin::Protocol.unpack_var_int_from_io(buf)
+ in_witness = Bitcoin::Protocol::TxInWitness.new
+ witness_count.times{
+ length = Bitcoin::Protocol.unpack_var_int_from_io(buf)
+ in_witness.add_stack(buf.read(length).unpack("H*").first)
+ }
+ @witness.add_witness(in_witness)
+ }
- @witness = Bitcoin::Protocol::TxWitness.new
- in_size.times{
- witness_count = Bitcoin::Protocol.unpack_var_int_from_io(buf)
- in_witness = Bitcoin::Protocol::TxInWitness.new
- witness_count.times{
- length = Bitcoin::Protocol.unpack_var_int_from_io(buf)
- in_witness.add_stack(buf.read(length).unpack("H*").first)
- }
- @witness.add_witness(in_witness)
- }
+ @lock_time = buf.read(4).unpack("V").first
- @lock_time = buf.read(4).unpack("V").first
+ @hash = hash_from_payload(to_payload)
- @hash = hash_from_payload(to_payload)
- end
+ if buf.eof?
+ true
+ else
+ data.is_a?(StringIO) ? buf : buf.read
+ end
+ end
- # output transaction in raw binary format with witness
- def to_witness_payload
- pin = ""
- @in.each{|input| pin << input.to_payload }
- pout = ""
- @out.each{|output| pout << output.to_payload }
- payload = [@ver].pack("V") << [0].pack("c") << [1].pack("c") << Bitcoin::Protocol.pack_var_int(@in.size) << pin <<
- Bitcoin::Protocol.pack_var_int(@out.size) << pout << @witness.to_payload << [@lock_time].pack("V")
- payload
- end
+ alias :parse_witness_data :parse_witness_data_from_io
-end
\ No newline at end of file
+ # output transaction in raw binary format with witness
+ def to_witness_payload
+ pin = ""
+ @in.each{|input| pin << input.to_payload }
+ pout = ""
+ @out.each{|output| pout << output.to_payload }
+ [@ver].pack("V") << [0].pack("c") << [1].pack("c") << Bitcoin::Protocol.pack_var_int(@in.size) << pin <<
+ Bitcoin::Protocol.pack_var_int(@out.size) << pout << @witness.to_payload << [@lock_time].pack("V")
+ 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 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
+
+ witness.tx_in_wit[in_idx].stack.each{|s|script_sig << Bitcoin::Script.pack_pushdata(s.htb)}
+ 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 = witness.tx_in_wit[in_idx].stack.last
+ 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
+
+ # convert to ruby hash (see also #from_hash)
+ def to_hash(options = {})
+ @hash ||= hash_from_payload(to_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.with_index{|i, index|
+ h = i.to_hash(options)
+ h.merge!('witness' => @witness.tx_in_wit[index].stack) if @witness.tx_in_wit[index]
+ h
+ },
+ 'out' => @out.map{|o| o.to_hash(options) }
+ }
+ h['nid'] = normalized_hash if options[:with_nid]
+ h
+ end
+
+ # parse ruby hash (see also #to_hash)
+ 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))
+ tx.witness.add_witness(TxInWitness.from_hash(input['witness'])) if input['witness']
+ }
+ outs.each{|output| tx.add_out TxOut.from_hash(output) }
+ tx.instance_eval{ @hash = hash_from_payload(@payload = to_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)
+ tx = from_hash(h)
+ tx.witness.empty? ? tx.to_payload : tx.to_witness_payload
+ end
+
+ # get witness hash
+ def witness_hash
+ hash_from_payload(to_witness_payload)
+ end
+
+ end
+ end
+end