Class: BTC::TransactionBuilder

Inherits:
Object
  • Object
show all
Includes:
Opcodes
Defined in:
lib/btcruby/transaction_builder.rb,
lib/btcruby/transaction_builder/errors.rb,
lib/btcruby/transaction_builder/result.rb,
lib/btcruby/transaction_builder/signer.rb,
lib/btcruby/transaction_builder/provider.rb

Overview

TransactionBuilder composes and optionally sings a transaction.

Defined Under Namespace

Modules: Provider, Signer Classes: Error, InsufficientFundsError, MissingChangeAddressError, MissingUnspentOutputsError, Result

Constant Summary

Constants included from Opcodes

Opcodes::OPCODE_NAME_TO_VALUE, Opcodes::OPCODE_VALUE_TO_NAME, Opcodes::OP_0, Opcodes::OP_0NOTEQUAL, Opcodes::OP_1, Opcodes::OP_10, Opcodes::OP_11, Opcodes::OP_12, Opcodes::OP_13, Opcodes::OP_14, Opcodes::OP_15, Opcodes::OP_16, Opcodes::OP_1ADD, Opcodes::OP_1NEGATE, Opcodes::OP_1SUB, Opcodes::OP_2, Opcodes::OP_2DIV, Opcodes::OP_2DROP, Opcodes::OP_2DUP, Opcodes::OP_2MUL, Opcodes::OP_2OVER, Opcodes::OP_2ROT, Opcodes::OP_2SWAP, Opcodes::OP_3, Opcodes::OP_3DUP, Opcodes::OP_4, Opcodes::OP_5, Opcodes::OP_6, Opcodes::OP_7, Opcodes::OP_8, Opcodes::OP_9, Opcodes::OP_ABS, Opcodes::OP_ADD, Opcodes::OP_AND, Opcodes::OP_BOOLAND, Opcodes::OP_BOOLOR, Opcodes::OP_CAT, Opcodes::OP_CHECKLOCKTIMEVERIFY, Opcodes::OP_CHECKMULTISIG, Opcodes::OP_CHECKMULTISIGVERIFY, Opcodes::OP_CHECKSIG, Opcodes::OP_CHECKSIGVERIFY, Opcodes::OP_CODESEPARATOR, Opcodes::OP_DEPTH, Opcodes::OP_DIV, Opcodes::OP_DROP, Opcodes::OP_DUP, Opcodes::OP_ELSE, Opcodes::OP_ENDIF, Opcodes::OP_EQUAL, Opcodes::OP_EQUALVERIFY, Opcodes::OP_FALSE, Opcodes::OP_FROMALTSTACK, Opcodes::OP_GREATERTHAN, Opcodes::OP_GREATERTHANOREQUAL, Opcodes::OP_HASH160, Opcodes::OP_HASH256, Opcodes::OP_IF, Opcodes::OP_IFDUP, Opcodes::OP_INVALIDOPCODE, Opcodes::OP_INVERT, Opcodes::OP_LEFT, Opcodes::OP_LESSTHAN, Opcodes::OP_LESSTHANOREQUAL, Opcodes::OP_LSHIFT, Opcodes::OP_MAX, Opcodes::OP_MIN, Opcodes::OP_MOD, Opcodes::OP_MUL, Opcodes::OP_NEGATE, Opcodes::OP_NIP, Opcodes::OP_NOP, Opcodes::OP_NOP1, Opcodes::OP_NOP10, Opcodes::OP_NOP2, Opcodes::OP_NOP3, Opcodes::OP_NOP4, Opcodes::OP_NOP5, Opcodes::OP_NOP6, Opcodes::OP_NOP7, Opcodes::OP_NOP8, Opcodes::OP_NOP9, Opcodes::OP_NOT, Opcodes::OP_NOTIF, Opcodes::OP_NUMEQUAL, Opcodes::OP_NUMEQUALVERIFY, Opcodes::OP_NUMNOTEQUAL, Opcodes::OP_OR, Opcodes::OP_OVER, Opcodes::OP_PICK, Opcodes::OP_PUSHDATA1, Opcodes::OP_PUSHDATA2, Opcodes::OP_PUSHDATA4, Opcodes::OP_RESERVED, Opcodes::OP_RESERVED1, Opcodes::OP_RESERVED2, Opcodes::OP_RETURN, Opcodes::OP_RIGHT, Opcodes::OP_RIPEMD160, Opcodes::OP_ROLL, Opcodes::OP_ROT, Opcodes::OP_RSHIFT, Opcodes::OP_SHA1, Opcodes::OP_SHA256, Opcodes::OP_SIZE, Opcodes::OP_SUB, Opcodes::OP_SUBSTR, Opcodes::OP_SWAP, Opcodes::OP_TOALTSTACK, Opcodes::OP_TRUE, Opcodes::OP_TUCK, Opcodes::OP_VER, Opcodes::OP_VERIF, Opcodes::OP_VERIFY, Opcodes::OP_VERNOTIF, Opcodes::OP_WITHIN, Opcodes::OP_XOR

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#change_addressObject

Change address (base58-encoded string or BTC::Address). Must be specified, but may not be used if change is too small.



29
30
31
# File 'lib/btcruby/transaction_builder.rb', line 29

def change_address
  @change_address
end

#dust_changeObject

Amount of change that can be forgone as a mining fee if there are no more unspent outputs available. If equals zero, no amount is allowed to be forgone. Default value equals minimum_change. This means builder will never fail with “insufficient funds” just because it could not find enough unspents for big enough change. In worst case it will forgo the change as a part of the mining fee. Set to 0 to avoid wasting a single satoshi.



75
76
77
# File 'lib/btcruby/transaction_builder.rb', line 75

def dust_change
  @dust_change
end

#fee_rateObject

Miner’s fee per kilobyte (1000 bytes). Default is Transaction::DEFAULT_FEE_RATE



61
62
63
# File 'lib/btcruby/transaction_builder.rb', line 61

def fee_rate
  @fee_rate
end

#input_addressesObject

Addresses from which to fetch the inputs. Could be base58-encoded address or BTC::Address instances. If any address is a WIF, the corresponding input will be automatically signed with SIGHASH_ALL. Otherwise, the signature_script in the input will be set to output script from unspent output.



35
36
37
# File 'lib/btcruby/transaction_builder.rb', line 35

def input_addresses
  @input_addresses
end

#keep_unspent_outputs_orderObject

If true, does not sort unspent_outputs by confirmations number. Default is false, but order will be preserved if #confirmations attribute is nil.



79
80
81
# File 'lib/btcruby/transaction_builder.rb', line 79

def keep_unspent_outputs_order
  @keep_unspent_outputs_order
end

#minimum_changeObject

Minimum amount of change below which transaction is not composed. If change amount is non-zero and below this value, more unspent outputs are used. If change amount is zero, change output is not even created and this attribute is not used. Default value equals fee_rate.



67
68
69
# File 'lib/btcruby/transaction_builder.rb', line 67

def minimum_change
  @minimum_change
end

#networkObject

Implementation of the attributes declared above



21
22
23
# File 'lib/btcruby/transaction_builder.rb', line 21

def network
  @network
end

#outputsObject

An array of BTC::TransactionOutput instances specifying how many coins to spend and how. If the array is empty, all unspent outputs are spent to the change address.



25
26
27
# File 'lib/btcruby/transaction_builder.rb', line 25

def outputs
  @outputs
end

#outputs_amountObject (readonly)

A total amount in satoshis to be spent in all outputs (not including change output). If equals ‘nil`, all available unspent outputs for the public_address are expected to be returned (“swiping the keys”).



90
91
92
# File 'lib/btcruby/transaction_builder.rb', line 90

def outputs_amount
  @outputs_amount
end

#outputs_sizeObject (readonly)

A total size of all outputs in bytes (including change output).

Raises:

  • (ArgumentError)


93
94
95
# File 'lib/btcruby/transaction_builder.rb', line 93

def outputs_size
  @outputs_size
end

#parent_transactionsObject

Array of non-published transactions, which outputs can be consumed.



49
50
51
# File 'lib/btcruby/transaction_builder.rb', line 49

def parent_transactions
  @parent_transactions
end

#prepended_unspent_outputsObject

Prepended unspent outputs with valid ‘index` and `transaction_hash` attributes. These will be included before some optional unspent outputs.



39
40
41
# File 'lib/btcruby/transaction_builder.rb', line 39

def prepended_unspent_outputs
  @prepended_unspent_outputs
end

#providerObject

Provider of the unspent outputs. If not specified, ‘unspent_outputs` must be provided.



53
54
55
# File 'lib/btcruby/transaction_builder.rb', line 53

def provider
  @provider
end

#public_addressesObject (readonly)

Public addresses used on the inputs. Composed by converting all ‘WIF` instance in `input_addresses` to corresponding public addresses.



85
86
87
# File 'lib/btcruby/transaction_builder.rb', line 85

def public_addresses
  @public_addresses
end

#signerObject

Signer for the inputs. If not provided, inputs will be left unsigned.



57
58
59
# File 'lib/btcruby/transaction_builder.rb', line 57

def signer
  @signer
end

#unspent_outputsObject

Actual available BTC::TransactionOutput’s to spend. If not specified, builder will fetch and remember unspent outputs using ‘provider`. Only necessary inputs will be selected for spending. If TransactionOutput#confirmations attribute is not nil, outputs are sorted from oldest to newest, unless #keep_unspent_outputs_order is set to true.



46
47
48
# File 'lib/btcruby/transaction_builder.rb', line 46

def unspent_outputs
  @unspent_outputs
end

Instance Method Details

#buildObject

Attempts to build a transaction



177
178
179
180
181
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/btcruby/transaction_builder.rb', line 177

def build

  if !self.change_address
    raise MissingChangeAddressError
  end

  result = Result.new

  add_input_for_utxo = proc do |utxo|
    raise ArgumentError, "Unspent output must contain index" if !utxo.index
    raise ArgumentError, "Unspent output must contain transaction_hash" if !utxo.transaction_hash

    if !self.internal_provider.consumed_unspent_output?(utxo)
      self.internal_provider.consume_unspent_output(utxo)

      result.inputs_amount += utxo.value
      txin = TransactionInput.new(
                                  previous_hash: utxo.transaction_hash,
                                  previous_index: utxo.index,
                                  # put the output script here so the signer knows which key to use.
                                  signature_script: utxo.script)
      txin.transaction_output = utxo
      result.transaction.add_input(txin)
    else
      # UTXO was already consumed possibly by another Tx Builder sharing the same provider.
    end
  end

  # If no outputs are specified, spend all utxos to a change address, minus the mining fee.
  if (self.outputs || []).size == 0
    result.inputs_amount = 0

    (self.prepended_unspent_outputs || []).each(&add_input_for_utxo)
    (self.unspent_outputs || []).each(&add_input_for_utxo)

    if result.transaction.inputs.size == 0
      raise MissingUnspentOutputsError, "Missing unspent outputs"
    end

    # Value will be determined after computing the fee
    change_output = TransactionOutput.new(value: 0, script: self.change_address.public_address.script)
    result.transaction.add_output(change_output)

    result.fee = compute_fee_for_transaction(result.transaction, self.fee_rate)
    result.outputs_amount = result.inputs_amount - result.fee
    result.change_amount = 0

    # Check if inputs cover the fees
    if result.outputs_amount < 0
      raise InsufficientFundsError
    end

    # Warn if the output amount is relatively small.
    if result.outputs_amount < result.fee
      Diagnostics.current.add_message("BTC::TransactionBuilder: Warning! Spending all unspent outputs returns less than a mining fee. (#{result.outputs_amount} < #{result.fee})")
    end

    # Set the output value as needed
    result.transaction.outputs[0].value = result.outputs_amount

    # For each address that is a WIF, locate the matching input and sign it.
    attempt_to_sign_transaction(result)

    return result
  end # if no specific outputs required

  # We are having one or more outputs (e.g. normal payment)
  # Need to find appropriate unspents and compose a transaction.

  # Prepare all outputs.
  # result.outputs_amount will also contain a fee after it's calculated.
  (self.outputs || []).each do |txout|
    result.outputs_amount += txout.value
    result.transaction.add_output(txout)
  end

  # We'll determine final change value depending on inputs.
  # Setting default to MAX_MONEY will protect against a bug when we fail to update the amount and
  # spend unexpected amount on mining fees.
  change_output = TransactionOutput.new(value: MAX_MONEY, script: self.change_address.public_address.script)
  result.transaction.add_output(change_output)

  mandatory_utxos = (self.prepended_unspent_outputs || []).to_a.dup

  # We have specific outputs with specific amounts, so we need to select the best amount of coins.
  # To play nice with BIP32/BIP44 change addresses (that need to monitor an increasing amount of addresses),
  # we'll spend oldest outputs first and will add more and more newer outputs until we cover fees
  # and change output is either zero or above the dust limit.
  # If `confirmations` attribute is nil, order is preserved.
  utxos = self.unspent_outputs || []
  if self.keep_unspent_outputs_order
    sorted_utxos = utxos.to_a.dup
  else
    sorted_utxos = utxos.to_a.sort_by{|txout| -(txout.confirmations || -1) } # oldest first
  end
  
  if self.parent_transactions
    # Can repeat some outputs in mandatory_utxos or sorted_utxos, 
    # but double-spending will be prevented by provider.
    self.parent_transactions.each do |parenttx|
      sorted_utxos += parenttx.outputs
    end
  end
  
  all_utxos = (sorted_utxos + mandatory_utxos)

  while true
    if (sorted_utxos.size + mandatory_utxos.size) == 0
      raise InsufficientFundsError
    end

    utxo = nil
    while (sorted_utxos.size + mandatory_utxos.size) > 0
      utxo = if mandatory_utxos.size > 0
        mandatory_utxos.shift
      else
        sorted_utxos.shift
      end 
      if utxo.value > 0 &&
        !utxo.script.op_return_script? &&
        !self.internal_provider.consumed_unspent_output?(utxo)
        self.internal_provider.consume_unspent_output(utxo)
        break
      else
        #puts "CONSUMED UTXO, SKIPPING: #{utxo.transaction_id}:#{utxo.index} (#{utxo.value})"
        # Continue reading utxos to find one that is not consumed yet.
        utxo = nil
      end
    end
    
    if !utxo
      # puts "ALL UTXOS:"
      # all_utxos.each do |utxo|
      #   puts "--> #{utxo.transaction_id}:#{utxo.index} (#{utxo.value})"
      # end
      raise InsufficientFundsError
    end
    
    result.inputs_amount += utxo.value

    raise ArgumentError, "Transaction hash must be non-nil in unspent output" if !utxo.transaction_hash
    raise ArgumentError, "Index must be non-nil in unspent output" if !utxo.index

    txin = TransactionInput.new(previous_hash: utxo.transaction_hash,
                                previous_index: utxo.index,
                                # put the output script here so the signer knows which key to use.
                                signature_script: utxo.script)
    txin.transaction_output = utxo
    result.transaction.add_input(txin)

    # Do not check the result before we included all the mandatory utxos.
    if mandatory_utxos.size == 0
      # Before computing the fee, quick check if we have enough inputs to cover the outputs.
      # If not, go and add one more utxo before wasting time computing fees.
      if result.inputs_amount >= result.outputs_amount
        fee = compute_fee_for_transaction(result.transaction, self.fee_rate)

        change = result.inputs_amount - result.outputs_amount - fee

        if change >= self.minimum_change

          # We have a big enough change, set missing values and return.

          change_output.value = change
          result.change_amount = change
          result.outputs_amount += change
          result.fee = fee
          attempt_to_sign_transaction(result)
          return result

        elsif change > self.dust_change && change < self.minimum_change

          # We have a shitty change: not small enough to forgo, not big enough to be useful.
          # Try adding more utxos on the next cycle (or fail if no more utxos are available).

        elsif change >= 0 && change <= self.dust_change
          # This also includes the case when change is exactly zero satoshis.
          # Remove the change output, keep existing outputs_amount, set fee and try to sign.
          result.transaction.outputs = result.transaction.outputs[0, result.transaction.outputs.size - 1]
          result.change_amount = 0
          result.fee = fee
          attempt_to_sign_transaction(result)
          return result

        else
          # Change is negative, we need more funds for this transaction.
          # Try adding more utxos on the next cycle.

        end

      end # if inputs_amount >= outputs_amount
    end # if no more mandatory outputs to add
  end # while true
end

#internal_providerObject



131
132
133
# File 'lib/btcruby/transaction_builder.rb', line 131

def internal_provider
  @internal_provider ||= (self.provider || Provider.new{|txb| []})
end