Class: Bitcoin::Builder::TxBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/bitcoin/builder.rb

Overview

DSL to create Bitcoin::Protocol::Tx used by Builder#build_tx.

tx = tx do |t|
  t.input do |i|
    i.prev_out prev_tx, 0
    i.signature_key key
  end
  t.output do |o|
    o.value 12345 # 0.00012345 BTC
    o.to key.addr
  end
end

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.

Instance Method Summary collapse

Constructor Details

#initializeTxBuilder

Returns a new instance of TxBuilder.



137
138
139
140
141
142
143
# File 'lib/bitcoin/builder.rb', line 137

def initialize
  @tx = P::Tx.new(nil)
  @tx.ver = 1
  @tx.lock_time = 0
  @ins = []
  @outs = []
end

Instance Method Details

#add_empty_script_sig_to_input(i) ⇒ Object



242
243
244
245
246
247
248
249
250
# File 'lib/bitcoin/builder.rb', line 242

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

#get_script_sig(inc, hash_type) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/bitcoin/builder.rb', line 252

def get_script_sig(inc, hash_type)
  if inc.multiple_keys?
    # multiple keys given, generate signature for each one
    sigs = inc.sign(@sig_hash)
    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)
  end
  script_sig
end

#include_coinbase_data(i, inc) ⇒ Object

coinbase inputs don’t need to be signed, they only include the given coinbase_data



223
224
225
226
227
# File 'lib/bitcoin/builder.rb', line 223

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

#input {|c| ... } ⇒ Object

add an input to the transaction (see TxInBuilder).

Yields:

  • (c)


156
157
158
159
160
# File 'lib/bitcoin/builder.rb', line 156

def input
  c = TxInBuilder.new
  yield c
  @ins << c
end

#lock_time(n) ⇒ Object

specify tx lock_time. this is usually not necessary. defaults to 0.



151
152
153
# File 'lib/bitcoin/builder.rb', line 151

def lock_time(n)
  @tx.lock_time = n
end

#output(value = nil, recipient = nil, type = :address) {|c| ... } ⇒ Object

add an output to the transaction (see TxOutBuilder).

Yields:

  • (c)


163
164
165
166
167
168
169
# File 'lib/bitcoin/builder.rb', line 163

def output(value = nil, recipient = nil, type = :address)
  c = TxOutBuilder.new
  c.value(value)  if value
  c.to(recipient, type) if recipient
  yield c  if block_given?
  @outs << c
end

#randomize_outputsObject

Randomize the outputs using SecureRandom



339
340
341
# File 'lib/bitcoin/builder.rb', line 339

def randomize_outputs
  @outs.sort_by! { SecureRandom.random_bytes(4).unpack('I')[0] }
end

#sig_hash_and_all_keys_exist?(inc, sig_script) ⇒ Boolean

Returns:

  • (Boolean)


229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/bitcoin/builder.rb', line 229

def sig_hash_and_all_keys_exist?(inc, sig_script)
  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?)
  if script.is_multisig?
    return inc.multiple_keys? && inc.key.size >= script.get_signatures_required
  end
  raise 'Script type must be hash160, pubkey, p2wpkh or multisig'
end

#sign_input(i, inc) ⇒ Object

Sign input number i with data from given inc object (a TxInBuilder). rubocop:disable CyclomaticComplexity,PerceivedComplexity



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/bitcoin/builder.rb', line 274

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

  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)
    @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

      redeem_script = inc.instance_eval { @redeem_script }
      @tx.in[i].script_sig = Bitcoin::Script.pack_pushdata(redeem_script) if redeem_script
    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
      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

#tx(opts = {}) ⇒ Object

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.

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. rubocop:disable CyclomaticComplexity,PerceivedComplexity



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/bitcoin/builder.rb', line 182

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) }
  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)
      if change_value >= fee
        change_value -= 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|
    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
  @tx.instance_eval do
    @payload = to_payload
    @hash = hash_from_payload(@payload)
  end

  @tx
end

#version(n) ⇒ Object

specify tx version. this is usually not necessary. defaults to 1.



146
147
148
# File 'lib/bitcoin/builder.rb', line 146

def version(n)
  @tx.ver = n
end