lib/bitcoin/builder.rb in bitcoin-ruby-0.0.18 vs lib/bitcoin/builder.rb in bitcoin-ruby-0.0.19

- old
+ new

@@ -1,25 +1,23 @@ # encoding: ascii-8bit module Bitcoin - # Optional DSL to help create blocks and transactions. # # see also BlockBuilder, TxBuilder, TxInBuilder, TxOutBuilder, ScriptBuilder module Builder - # build a Bitcoin::Protocol::Block matching the given +target+. # see BlockBuilder for details. - def build_block(target = "00".ljust(64, 'f')) + def build_block(target = '00'.ljust(64, 'f')) c = BlockBuilder.new yield c c.block(target) end # build a Bitcoin::Protocol::Tx. # see TxBuilder for details. - def build_tx opts = {} + def build_tx(opts = {}) c = TxBuilder.new yield c c.tx opts end @@ -43,74 +41,81 @@ # end # end # # See Bitcoin::Builder::TxBuilder for details on building transactions. class BlockBuilder + attr_writer :prev_block, :time, :version def initialize @block = P::Block.new(nil) end # specify block version. this is usually not necessary. defaults to 1. - def version v + def version(v) # rubocop:disable Style/TrivialAccessors @version = v end # set the hash of the previous block. - def prev_block hash + def prev_block(hash) # rubocop:disable Style/TrivialAccessors @prev_block = hash end # set the block timestamp (defaults to current time). - def time time + def time(time) # rubocop:disable Style/TrivialAccessors @time = time end # add transactions to the block (see TxBuilder). - def tx tx = nil - tx ||= ( c = TxBuilder.new; yield c; c.tx ) + def tx(tx = nil) + tx ||= begin + c = TxBuilder.new + yield c + c.tx + end @block.tx << tx tx end # create the block according to values specified via DSL. - def block target + def block(target) + @version ||= nil + @mrkl_root ||= nil + @time ||= nil + @block.ver = @version || 1 @block.prev_block = @prev_block.htb.reverse @block.mrkl_root = @mrkl_root @block.time = @time || Time.now.to_i @block.nonce = 0 @block.mrkl_root = Bitcoin.hash_mrkl_tree(@block.tx.map(&:hash)).last.htb.reverse find_hash(target) block = P::Block.new(@block.to_payload) - raise "Payload Error" unless block.to_payload == @block.to_payload + raise 'Payload Error' unless block.to_payload == @block.to_payload block end private # increment nonce/time to find a block hash matching the +target+. - def find_hash target + def find_hash(target) @block.bits = Bitcoin.encode_compact_bits(target) t = Time.now @block.recalc_block_hash until @block.hash.to_i(16) < target.to_i(16) @block.nonce += 1 @block.recalc_block_hash - if @block.nonce == 100000 - if t - tt = 1 / ((Time.now - t) / 100000) / 1000 - print "\r%.2f khash/s" % tt - end - t = Time.now - @block.time = Time.now.to_i - @block.nonce = 0 - $stdout.flush + next unless @block.nonce == 100_000 + if t + tt = 1 / ((Time.now - t) / 100_000) / 1000 + print format("\r%.2f khash/s", tt) end + t = Time.now + @block.time = Time.now.to_i + @block.nonce = 0 + $stdout.flush end end - end # DSL to create Bitcoin::Protocol::Tx used by Builder#build_tx. # tx = tx do |t| # t.input do |i| @@ -127,24 +132,25 @@ # pk_script is known. If unable to sign, the resulting txin will include # the #sig_hash that needs to be signed. # # See TxInBuilder and TxOutBuilder for details on how to build in/outputs. class TxBuilder - def initialize @tx = P::Tx.new(nil) - @tx.ver, @tx.lock_time = 1, 0 - @ins, @outs = [], [] + @tx.ver = 1 + @tx.lock_time = 0 + @ins = [] + @outs = [] end # specify tx version. this is usually not necessary. defaults to 1. - def version n + def version(n) @tx.ver = n end # specify tx lock_time. this is usually not necessary. defaults to 0. - def lock_time n + def lock_time(n) @tx.lock_time = n end # add an input to the transaction (see TxInBuilder). def input @@ -152,14 +158,14 @@ yield c @ins << c end # add an output to the transaction (see TxOutBuilder). - def output value = nil, recipient = nil, type = :address + def output(value = nil, recipient = nil, type = :address) c = TxOutBuilder.new c.value(value) if value - c.to(recipient, type) if recipient + c.to(recipient, type) if recipient yield c if block_given? @outs << c end # Create the transaction according to values specified via DSL. @@ -170,18 +176,19 @@ # When :change_address and :input_value options are given, it will # automatically create a change output sending the remaining funds # to the given address. The :leave_fee option can be used in this # case to specify a tx fee that should be left unclaimed by the # change output. - def tx opts = {} + # rubocop:disable CyclomaticComplexity,PerceivedComplexity + def tx(opts = {}) return @tx if @tx.hash if opts[:change_address] && !opts[:input_value] raise "Must give 'input_value' when auto-generating change output!" end - @ins.each {|i| @tx.add_in(i.txin) } - @outs.each {|o| @tx.add_out(o.txout) } + @ins.each { |i| @tx.add_in(i.txin) } + @outs.each { |o| @tx.add_out(o.txout) } if opts[:change_address] output_value = @tx.out.map(&:value).inject(:+) || 0 change_value = opts[:input_value] - output_value if opts[:leave_fee] fee = @tx.minimum_block_fee + (opts[:extra_fee] || 0) @@ -200,126 +207,139 @@ @ins.each_with_index do |inc, i| sign_input(i, inc) end # run our tx through an encode/decode cycle to make sure that the binary format is sane - raise "Payload Error" unless P::Tx.new(@tx.to_witness_payload).to_payload == @tx.to_payload + raise 'Payload Error' unless P::Tx.new(@tx.to_witness_payload).to_payload == @tx.to_payload @tx.instance_eval do @payload = to_payload @hash = hash_from_payload(@payload) end @tx end + # rubocop:enable CyclomaticComplexity,PerceivedComplexity # coinbase inputs don't need to be signed, they only include the given +coinbase_data+ - def include_coinbase_data i, inc - script_sig = [inc.coinbase_data].pack("H*") + def include_coinbase_data(i, inc) + script_sig = [inc.coinbase_data].pack('H*') @tx.in[i].script_sig_length = script_sig.bytesize @tx.in[i].script_sig = script_sig end def sig_hash_and_all_keys_exist?(inc, sig_script) - return false unless @sig_hash && inc.has_keys? + return false unless @sig_hash && inc.keys? script = Bitcoin::Script.new(sig_script) - return true if script.is_hash160? || script.is_pubkey? || script.is_witness_v0_keyhash? || (Bitcoin.namecoin? && script.is_namecoin?) + return true if script.is_hash160? || + script.is_pubkey? || + script.is_witness_v0_keyhash? || + (Bitcoin.namecoin? && script.is_namecoin?) if script.is_multisig? - return inc.has_multiple_keys? && inc.key.size >= script.get_signatures_required + return inc.multiple_keys? && inc.key.size >= script.get_signatures_required end - raise "Script type must be hash160, pubkey, p2wpkh or multisig" + raise 'Script type must be hash160, pubkey, p2wpkh or multisig' end def add_empty_script_sig_to_input(i) @tx.in[i].script_sig_length = 0 - @tx.in[i].script_sig = "" + @tx.in[i].script_sig = '' # add the sig_hash that needs to be signed, so it can be passed on to a signing device @tx.in[i].sig_hash = @sig_hash - # add the address the sig_hash needs to be signed with as a convenience for the signing device - @tx.in[i].sig_address = Script.new(@prev_script).get_address if @prev_script + # add the address the sig_hash needs to be signed with as a convenience for the + # signing device + @tx.in[i].sig_address = Script.new(@prev_script).get_address if @prev_script end def get_script_sig(inc, hash_type) - if inc.has_multiple_keys? + if inc.multiple_keys? # multiple keys given, generate signature for each one sigs = inc.sign(@sig_hash) - if redeem_script = inc.instance_eval { @redeem_script } + redeem_script = inc.instance_eval { @redeem_script } + if redeem_script # when a redeem_script was specified, assume we spend a p2sh multisig script script_sig = Script.to_p2sh_multisig_script_sig(redeem_script, sigs) else # when no redeem_script is given, do a regular multisig spend script_sig = Script.to_multisig_script_sig(*sigs) end else # only one key given, generate signature and script_sig sig = inc.sign(@sig_hash) - script_sig = Script.to_signature_pubkey_script(sig, [inc.key.pub].pack("H*"), hash_type) + script_sig = Script.to_signature_pubkey_script(sig, [inc.key.pub].pack('H*'), hash_type) end - return script_sig + script_sig end # Sign input number +i+ with data from given +inc+ object (a TxInBuilder). - def sign_input i, inc - if @tx.in[i].coinbase? - include_coinbase_data(i, inc) - else - @prev_script = inc.instance_variable_get(:@prev_out_script) + # rubocop:disable CyclomaticComplexity,PerceivedComplexity + def sign_input(i, inc) + return include_coinbase_data(i, inc) if @tx.in[i].coinbase? + @prev_script = inc.instance_variable_get(:@prev_out_script) - # get the signature script; use +redeem_script+ if given - # (indicates spending a p2sh output), otherwise use the prev_script - sig_script = inc.instance_eval { @redeem_script } - sig_script ||= @prev_script + # get the signature script; use +redeem_script+ if given + # (indicates spending a p2sh output), otherwise use the prev_script + sig_script = inc.instance_eval { @redeem_script } + sig_script ||= @prev_script - hash_type = if inc.prev_out_forkid - Script::SIGHASH_TYPE[:all] | Script::SIGHASH_TYPE[:forkid] - else - Script::SIGHASH_TYPE[:all] - end + hash_type = if inc.prev_out_forkid + Script::SIGHASH_TYPE[:all] | Script::SIGHASH_TYPE[:forkid] + else + Script::SIGHASH_TYPE[:all] + end - # when a sig_script was found, generate the sig_hash to be signed - if sig_script - script = Script.new(sig_script) - if script.is_witness_v0_keyhash? - @sig_hash = @tx.signature_hash_for_witness_input(i, sig_script, inc.value) - else - @sig_hash = if inc.prev_out_forkid - @tx.signature_hash_for_input( - i, - sig_script, - hash_type, - inc.value, - inc.prev_out_forkid) - else - @tx.signature_hash_for_input(i, sig_script) - end - end - end + # when a sig_script was found, generate the sig_hash to be signed + if sig_script + script = Script.new(sig_script) + @sig_hash = if script.is_witness_v0_keyhash? + @tx.signature_hash_for_witness_input(i, sig_script, inc.value) + elsif inc.prev_out_forkid + @tx.signature_hash_for_input( + i, + sig_script, + hash_type, + inc.value, + inc.prev_out_forkid + ) + else + @tx.signature_hash_for_input(i, sig_script) + end + end - # when there is a sig_hash and one or more signature_keys were specified - if sig_hash_and_all_keys_exist?(inc, sig_script) - # add the script_sig to the txin - if script.is_witness_v0_keyhash? # for p2wpkh - @tx.in[i].script_witness.stack << inc.sign(@sig_hash) + [Script::SIGHASH_TYPE[:all]].pack("C") - @tx.in[i].script_witness.stack << inc.key.pub.htb - else - @tx.in[i].script_sig = get_script_sig(inc, hash_type) - end - # double-check that the script_sig is valid to spend the given prev_script - if @prev_script && !inc.prev_out_forkid && !@tx.verify_input_signature(i, @prev_script) - raise "Signature error" - end - elsif inc.has_multiple_keys? - raise "Keys missing for multisig signing" + # when there is a sig_hash and one or more signature_keys were specified + if sig_hash_and_all_keys_exist?(inc, sig_script) + # add the script_sig to the txin + if script.is_witness_v0_keyhash? # for p2wpkh + @tx.in[i].script_witness.stack << inc.sign(@sig_hash) + \ + [Script::SIGHASH_TYPE[:all]].pack('C') + @tx.in[i].script_witness.stack << inc.key.pub.htb + + redeem_script = inc.instance_eval { @redeem_script } + @tx.in[i].script_sig = Bitcoin::Script.pack_pushdata(redeem_script) if redeem_script else - # no sig_hash, add an empty script_sig. - add_empty_script_sig_to_input(i) + @tx.in[i].script_sig = get_script_sig(inc, hash_type) end + # double-check that the script_sig is valid to spend the given prev_script + if @prev_script && !inc.prev_out_forkid + verified = if script.is_witness_v0_keyhash? + @tx.verify_witness_input_signature(i, @prev_script, inc.value) + else + @tx.verify_input_signature(i, @prev_script) + end + raise 'Signature error' unless verified + end + elsif inc.multiple_keys? + raise 'Keys missing for multisig signing' + else + # no sig_hash, add an empty script_sig. + add_empty_script_sig_to_input(i) end end + # rubocop:enable CyclomaticComplexity,PerceivedComplexity # Randomize the outputs using SecureRandom def randomize_outputs - @outs.sort_by!{ SecureRandom.random_bytes(4).unpack("I")[0] } + @outs.sort_by! { SecureRandom.random_bytes(4).unpack('I')[0] } end end # Create a Bitcoin::Protocol::TxIn used by TxBuilder#input. # @@ -344,104 +364,125 @@ # i.redeem_script prev_out.redeem_script # end # # If you want to spend a multisig output, just provide an array of keys to #signature_key. class TxInBuilder - attr_reader :prev_tx, :prev_script, :redeem_script, :key, :coinbase_data, :prev_out_value, :prev_out_forkid + attr_reader :coinbase_data, :key, :prev_out_forkid, :prev_script, :prev_tx + attr_writer :prev_out_script, :prev_out_value, :redeem_script, :sequence def initialize @txin = P::TxIn.new @prev_out_hash = "\x00" * 32 @prev_out_index = 0 + @redeem_script = nil + @key = nil end # Previous transaction that contains the output we want to use. # You can either pass the transaction, or just the tx hash. # If you pass only the hash, you need to pass the previous outputs # +script+ separately if you want the txin to be signed. - def prev_out tx, idx = nil, script = nil, prev_value = nil, prev_forkid = nil + def prev_out(tx, idx = nil, script = nil, prev_value = nil, prev_forkid = nil) @prev_out_forkid = prev_forkid if tx.is_a?(Bitcoin::P::Tx) @prev_tx = tx @prev_out_hash = tx.binary_hash - @prev_out_script = tx.out[idx].pk_script if idx + @prev_out_script = tx.out[idx].pk_script if idx else @prev_out_hash = tx.htb.reverse end - @prev_out_script = script if script - @prev_out_index = idx if idx + @prev_out_script = script if script + @prev_out_index = idx if idx @prev_out_value = prev_value if prev_value end # Index of the output in the #prev_out transaction. - def prev_out_index i + def prev_out_index(i) @prev_out_index = i - @prev_out_script = @prev_tx.out[i].pk_script if @prev_tx + @prev_out_script = @prev_tx.out[i].pk_script if @prev_tx end # Previous output's +pk_script+. Needed when only the tx hash is specified as #prev_out. - def prev_out_script script + def prev_out_script(script) # rubocop:disable Style/TrivialAccessors @prev_out_script = script end # Previous output's +value+. Needed when only spend segwit utxo. - def prev_out_value value + def prev_out_value(value) # rubocop:disable Style/TrivialAccessors @prev_out_value = value end def value @prev_out_value end # Redeem script for P2SH output. To spend from a P2SH output, you need to provide # the script with a hash matching the P2SH address. - def redeem_script script + def redeem_script(script) # rubocop:disable Style/TrivialAccessors @redeem_script = script end # Specify sequence. This is usually not needed. - def sequence s + def sequence(s) # rubocop:disable Style/TrivialAccessors @sequence = s end # Bitcoin::Key used to sign the signature_hash for the input. # see Bitcoin::Script.signature_hash_for_input and Bitcoin::Key.sign. - def signature_key key + def signature_key(key) @key = key end # Specify that this is a coinbase input. Optionally set +data+. # If this is set, no other options need to be given. - def coinbase data = nil + def coinbase(data = nil) @coinbase_data = data || OpenSSL::Random.random_bytes(32) @prev_out_hash = "\x00" * 32 - @prev_out_index = 4294967295 + @prev_out_index = 4_294_967_295 end # Create the txin according to specified values def txin + @sequence ||= nil @txin.prev_out = @prev_out_hash @txin.prev_out_index = @prev_out_index @txin.sequence = @sequence || "\xff\xff\xff\xff" @txin end - def has_multiple_keys? + def multiple_keys? @key.is_a?(Array) end - def has_keys? - @key && (has_multiple_keys? ? @key.all?(&:priv) : @key.priv) + def has_multiple_keys? # rubocop:disable Naming/PredicateName + warn '[DEPRECATION] `TxInBuilder.has_multiple_keys?` is deprecated. ' \ + 'Use `multiple_keys?` instaed.' + multiple_keys? end - def is_witness_v0_keyhash? + def keys? + @key && (multiple_keys? ? @key.all?(&:priv) : @key.priv) + end + + def has_keys? # rubocop:disable Naming/PredicateName + warn '[DEPRECATION] `TxInBuilder.has_keys?` is deprecated. Use `keys?` instead.' + keys? + end + + def witness_v0_keyhash? @prev_out_script && Script.new(@prev_out_script).is_witness_v0_keyhash? end + def is_witness_v0_keyhash? # rubocop:disable Naming/PredicateName + warn '[DEPRECATION] `TxInBuilder.is_witness_v0_keyhash?` is deprecated. ' \ + 'Use `witness_v0_keyhash?` instead.' + witness_v0_keyhash? + end + def sign(sig_hash) - if has_multiple_keys? - @key.map {|k| k.sign(sig_hash) } + if multiple_keys? + @key.map { |k| k.sign(sig_hash) } else @key.sign(sig_hash) end end end @@ -455,18 +496,18 @@ @script = nil end # Script type (:pubkey, :address/hash160, :multisig). # Defaults to :address. - def type type + def type(type) @type = type.to_sym end # Recipient(s) of the script. # Depending on the #type, this should be an address, a hash160 pubkey, # or an array of multisig pubkeys. - def recipient *data + def recipient(*data) @script, @redeem_script = *Script.send("to_#{@type}_script", *data) end end # Create a Bitcoin::Protocol::TxOut used by TxBuilder#output. @@ -488,25 +529,26 @@ def initialize @txout = P::TxOut.new(0) end # Set output value (in base units / "satoshis") - def value value + def value(value) @txout.value = value end # Set recipient address and script type (defaults to :address). - def to recipient, type = :address - @txout.pk_script, @txout.redeem_script = *Bitcoin::Script.send("to_#{type}_script", *recipient) + def to(recipient, type = :address) + @txout.pk_script, @txout.redeem_script = *Bitcoin::Script.send( + "to_#{type}_script", *recipient + ) end # Add a script to the output (see ScriptBuilder). - def script &block + def script c = ScriptBuilder.new yield c - @txout.pk_script, @txout.redeem_script = c.script, c.redeem_script + @txout.pk_script = c.script + @txout.redeem_script = c.redeem_script end - end - end end