lib/bitcoin/builder.rb in bitcoin-ruby-0.0.5 vs lib/bitcoin/builder.rb in bitcoin-ruby-0.0.6

- old
+ new

@@ -16,14 +16,14 @@ end alias :blk :build_block # build a Bitcoin::Protocol::Tx. # see TxBuilder for details. - def build_tx + def build_tx opts = {} c = TxBuilder.new yield c - c.tx + c.tx opts end alias :tx :build_tx # build a Bitcoin::Script. # see ScriptBuilder for details. @@ -38,17 +38,16 @@ # b.prev_block "\x00"*32 # b.tx do |t| # t.input {|i| i.coinbase } # t.output do |o| # o.value 5000000000; - # o.script do |s| - # s.type :address - # s.recipient Bitcoin::Key.generate.addr - # end + # o.to Bitcoin::Key.generate.addr # end # end # end + # + # See Bitcoin::Builder::TxBuilder for details on building transactions. class BlockBuilder def initialize @block = P::Block.new(nil) end @@ -67,14 +66,14 @@ def time time @time = time end # add transactions to the block (see TxBuilder). - def tx - c = TxBuilder.new - yield c - @block.tx << c.tx + def tx tx = nil + tx ||= ( c = TxBuilder.new; yield c; c.tx ) + @block.tx << tx + tx end # create the block according to values specified via DSL. def block target @block.ver = @version || 1 @@ -115,22 +114,24 @@ end # DSL to create Bitcoin::Protocol::Tx used by Builder#build_tx. # tx = tx do |t| # t.input do |i| - # i.prev_out prev_tx # previous transaction - # i.prev_out_index 0 # index of previous output - # i.signature_key key # Bitcoin::Key used to sign the input + # i.prev_out prev_tx, 0 + # i.signature_key key # end # t.output do |o| # o.value 12345 # 0.00012345 BTC - # o.script {|s| s.type :address; s.recipient key.addr } + # o.to key.addr # end # end # - # signs every input that has a signature key. if the signature key is - # not specified, the input will include the #sig_hash that needs to be signed. + # Signs every input that has a signature key and where the previous outputs + # 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 @@ -159,132 +160,301 @@ c = TxOutBuilder.new yield c @outs << c end - # create the transaction according to values specified via DSL. - # sign each input that has a signature key specified. if there is + # Create the transaction according to values specified via DSL. + # Sign each input that has a signature key specified. If there is # no key, store the sig_hash in the input, so it can easily be # signed later. - def tx + # + # 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 = {} + 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) } + + if opts[:change_address] + output_value = @tx.out.map(&:value).inject(:+) + change_value = opts[:input_value] - output_value + if opts[:leave_fee] + if change_value >= @tx.minimum_block_fee + change_value -= @tx.minimum_block_fee + else + change_value = 0 + end + end + if change_value > 0 + script = Script.to_address_script(opts[:change_address]) + @tx.add_out(P::TxOut.new(change_value, script)) + end + end + @ins.each_with_index do |inc, i| - if @tx.in[i].coinbase? - script_sig = [inc.coinbase_data].pack("H*") - @tx.in[i].script_sig_length = script_sig.bytesize - @tx.in[i].script_sig = script_sig - next + 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_payload).to_payload == @tx.to_payload + @tx.instance_eval do + @payload = to_payload + @hash = hash_from_payload(@payload) + end + + @tx + end + + # 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*") + @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? + script = Bitcoin::Script.new(sig_script) + return true if script.is_hash160? || script.is_pubkey? + if script.is_multisig? + return inc.has_multiple_keys? && inc.key.size >= script.get_signatures_required + end + raise "Script type must be hash160, pubkey or multisig" + end + + def add_empty_script_sig_to_input(i) + @tx.in[i].script_sig_length = 0 + @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 + end + + def get_script_sig(inc) + if inc.has_multiple_keys? + # multiple keys given, generate signature for each one + sigs = inc.sign(@sig_hash) + if redeem_script = inc.instance_eval { @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 - prev_tx = inc.instance_variable_get(:@prev_out) - @sig_hash = @tx.signature_hash_for_input(i, prev_tx) - if inc.key && inc.key.priv - sig = inc.key.sign(@sig_hash) - script_sig = Script.to_signature_pubkey_script(sig, [inc.key.pub].pack("H*")) - @tx.in[i].script_sig_length = script_sig.bytesize - @tx.in[i].script_sig = script_sig - raise "Signature error" unless @tx.verify_input_signature(i, prev_tx) + 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*")) + end + return 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) + + # 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 + + # when a sig_script was found, generate the sig_hash to be signed + @sig_hash = @tx.signature_hash_for_input(i, sig_script) if sig_script + + # 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 + @tx.in[i].script_sig = get_script_sig(inc) + + # double-check that the script_sig is valid to spend the given prev_script + raise "Signature error" if @prev_script && !@tx.verify_input_signature(i, @prev_script) + elsif inc.has_multiple_keys? + raise "Keys missing for multisig signing" else - @tx.in[i].script_sig_length = 0 - @tx.in[i].script_sig = "" - @tx.in[i].sig_hash = @sig_hash - @tx.in[i].sig_address = Script.new(prev_tx.out[@tx.in[i].prev_out_index].pk_script).get_address + # no sig_hash, add an empty script_sig. + add_empty_script_sig_to_input(i) end end - data = @tx.in.map {|i| [i.sig_hash, i.sig_address] } - tx = P::Tx.new(@tx.to_payload) - data.each.with_index {|d, i| i = tx.in[i]; i.sig_hash = d[0]; i.sig_address = d[1] } - raise "Payload Error" unless tx.to_payload == @tx.to_payload - tx end + + # Randomize the outputs using SecureRandom + def randomize_outputs + @outs.sort_by!{ SecureRandom.random_bytes(4).unpack("I")[0] } + end end - # create a Bitcoin::Protocol::TxIn used by TxBuilder#input. + # Create a Bitcoin::Protocol::TxIn used by TxBuilder#input. # - # inputs need a #prev_out tx and #prev_out_index of the output they spend. + # Inputs need the transaction hash and the index of the output they spend. + # You can pass either the transaction, or just its hash (in hex form). + # To sign the input, builder also needs the pk_script of the previous output. + # If you specify a tx hash instead of the whole tx, you need to specify the + # output script separately. + # + # t.input do |i| + # i.prev_out prev_tx # previous transaction + # i.prev_out_index 0 # index of previous output + # i.signature_key key # Bitcoin::Key used to sign the input + # end + # + # t.input {|i| i.prev_out prev_tx, 0 } + # + # If you want to spend a p2sh output, you also need to specify the +redeem_script+. + # + # t.input do |i| + # i.prev_out prev_tx, 0 + # 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 :key, :coinbase_data + attr_reader :prev_tx, :prev_script, :redeem_script, :key, :coinbase_data def initialize @txin = P::TxIn.new + @prev_out_hash = "\x00" * 32 + @prev_out_index = 0 end - # previous transaction that contains the output we want to use. - def prev_out tx, idx = nil - @prev_out, @prev_out_index = tx, idx + # 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 + 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 + else + @prev_out_hash = tx.htb.reverse + end + @prev_out_script = script if script + @prev_out_index = idx if idx end - # index of the output in the #prev_out transaction. + # Index of the output in the #prev_out transaction. def prev_out_index i @prev_out_index = i + @prev_out_script = @prev_tx.out[i].pk_script if @prev_tx end - # specify sequence. this is usually not needed. + # Previous output's +pk_script+. Needed when only the tx hash is specified as #prev_out. + def prev_out_script script + @prev_out_script = script + 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 + @redeem_script = script + end + + # Specify sequence. This is usually not needed. def sequence s @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 @key = key end - # specify that this is a coinbase input. optionally set +data+. + # 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 @coinbase_data = data || OpenSSL::Random.random_bytes(32) - @prev_out = nil + @prev_out_hash = "\x00" * 32 @prev_out_index = 4294967295 end - # create the txin according to values specified via DSL + # Create the txin according to specified values def txin - @txin.prev_out = (@prev_out ? @prev_out.binary_hash : "\x00"*32) + @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? + @key.is_a?(Array) + end + + def has_keys? + @key && (has_multiple_keys? ? @key.all?(&:priv) : @key.priv) + end + + def sign(sig_hash) + if has_multiple_keys? + @key.map {|k| k.sign(sig_hash) } + else + @key.sign(sig_hash) + end + end end - # create a Bitcoin::Script used by TxOutBuilder#script. + # Create a Bitcoin::Script used by TxOutBuilder#script. class ScriptBuilder - attr_reader :script + attr_reader :script, :redeem_script def initialize @type = :address @script = nil end - # script type (:pubkey, :address/hash160, :multisig). + # Script type (:pubkey, :address/hash160, :multisig). + # Defaults to :address. def type type @type = type.to_sym end - # recipient(s) of the script. - # depending on the #type, either an address, hash160 pubkey, etc. + # 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 - @script = Script.send("to_#{@type}_script", *data) + @script, @redeem_script = *Script.send("to_#{@type}_script", *data) end end - # create a Bitcoin::Protocol::TxOut used by TxBuilder#output. + # Create a Bitcoin::Protocol::TxOut used by TxBuilder#output. + # + # t.output {|o| o.value 12345; o.to address } + # + # t.output do |o| + # o.value 12345 + # o.script {|s| s.recipient address } + # end class TxOutBuilder attr_reader :txout def initialize @txout = P::TxOut.new end - # set output value (in base units / "satoshis") + # Set output value (in base units / "satoshis") def value value @txout.value = value end - # add a script to the output (see ScriptBuilder). + # 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) + end + + # Add a script to the output (see ScriptBuilder). def script &block c = ScriptBuilder.new yield c - @txout.pk_script = c.script + @txout.pk_script, @txout.redeem_script = c.script, c.redeem_script end end end