Class: Bitcoin::Validation::Tx

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

Constant Summary collapse

RULES =
{
  syntax: [:hash, :lists, :max_size, :output_values, :inputs, :lock_time, :standard],
  context: [:prev_out, :signatures, :not_spent, :input_values, :output_sum]
}
KNOWN_EXCEPTIONS =
[
  # p2sh with invalid inner script, accepted by old miner before 4-2012 switchover
  "6a26d2ecb67f27d1fa5524763b49029d7106e91e3cc05743073461a719776192",
  # p2sh with invalid inner script, accepted by old miner before 4-2012 switchover (testnet)
  "b3c19d78b4953b694717a47d9852f8ea1ccd4cf93a45ba2e43a0f97d7cdb2655"
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tx, store, block = nil, block_validator = nil) ⇒ Tx

Setup new validator for given tx, validating context with store. Also needs the block that includes the tx to be validated, to find prev_outs for chains of txs inside the block. Optionally accepts the validator object for the block, to optimize fetching prev_txs and checking for doublespends.



273
274
275
276
# File 'lib/bitcoin/validation.rb', line 273

def initialize(tx, store, block = nil, block_validator = nil)
  @tx, @store, @block, @errors = tx, store, block, []
  @block_validator = block_validator
end

Instance Attribute Details

#block_validatorObject

Returns the value of attribute block_validator.



234
235
236
# File 'lib/bitcoin/validation.rb', line 234

def block_validator
  @block_validator
end

#errorObject

Returns the value of attribute error.



234
235
236
# File 'lib/bitcoin/validation.rb', line 234

def error
  @error
end

#storeObject

Returns the value of attribute store.



234
235
236
# File 'lib/bitcoin/validation.rb', line 234

def store
  @store
end

#txObject

Returns the value of attribute tx.



234
235
236
# File 'lib/bitcoin/validation.rb', line 234

def tx
  @tx
end

Instance Method Details

#clear_cacheObject

empty prev txs cache



374
375
376
377
378
# File 'lib/bitcoin/validation.rb', line 374

def clear_cache
  @prev_txs = nil
  @total_in = nil
  @total_out = nil
end

#hashObject

check that tx hash matches data



279
280
281
282
# File 'lib/bitcoin/validation.rb', line 279

def hash
  generated_hash = tx.generate_hash(tx.to_payload)
  tx.hash == generated_hash || [tx.hash, generated_hash]
end

#input_valuesObject

check that the total input value doesn’t exceed MAX_MONEY



364
365
366
# File 'lib/bitcoin/validation.rb', line 364

def input_values
  total_in < Bitcoin::network[:max_money] || [total_in, Bitcoin::network[:max_money]]
end

#inputsObject

check that none of the inputs is coinbase (coinbase tx do not get validated)



302
303
304
# File 'lib/bitcoin/validation.rb', line 302

def inputs
  tx.inputs.none?(&:coinbase?) || [tx.inputs.index(tx.inputs.find(&:coinbase?))]
end

#listsObject

check that tx has at least one input and one output



285
286
287
# File 'lib/bitcoin/validation.rb', line 285

def lists
  (tx.in.any? && tx.out.any?) || [tx.in.size, tx.out.size]
end

#lock_timeObject

check that lock_time doesn’t exceed INT_MAX



307
308
309
# File 'lib/bitcoin/validation.rb', line 307

def lock_time
  tx.lock_time <= Bitcoin::UINT32_MAX || [tx.lock_time, Bitcoin::UINT32_MAX]
end

#max_sizeObject

check that tx size doesn’t exceed MAX_BLOCK_SIZE.



290
291
292
# File 'lib/bitcoin/validation.rb', line 290

def max_size
  tx.to_payload.bytesize <= Bitcoin::MAX_BLOCK_SIZE || [tx.to_payload.bytesize, Bitcoin::MAX_BLOCK_SIZE]
end

#min_sizeObject

check that min_size is at least 86 bytes (smaller tx can’t be valid / do anything useful)



313
314
315
# File 'lib/bitcoin/validation.rb', line 313

def min_size
  tx.to_payload.bytesize >= 86 || [tx.to_payload.bytesize, 86]
end

#not_spentObject

check that none of the prev_outs are already spent in the main chain or in the current block



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/bitcoin/validation.rb', line 345

def not_spent
  # if we received cached spents, use it
  return block_validator.spent_outs_txins.empty? if block_validator

  # find all spent txouts
  next_ins = store.get_txins_for_txouts(tx.in.map.with_index {|txin, idx| [txin.prev_out.reverse_hth, txin.prev_out_index] })

  # no txouts found spending these txins, we can safely return true
  return true if next_ins.empty?

  # there were some txouts spending these txins, verify that they are not on the main chain
  next_ins.select! {|i| i.get_tx.blk_id } # blk_id is only set for tx in the main chain
  return true if next_ins.empty?

  # now we know some txouts are already spent, return tx_idxs for debugging purposes
  return next_ins.map {|i| i.get_prev_out.tx_idx }
end

#output_sumObject

check that the total output value doesn’t exceed the total input value



369
370
371
# File 'lib/bitcoin/validation.rb', line 369

def output_sum
  total_in >= total_out || [total_out, total_in]
end

#output_valuesObject

check that total output value doesn’t exceed MAX_MONEY.



295
296
297
298
# File 'lib/bitcoin/validation.rb', line 295

def output_values
  total = tx.out.inject(0) {|e, out| e + out.value }
  total <= Bitcoin::network[:max_money] || [total, Bitcoin::network[:max_money]]
end

#prev_outObject

check that all prev_outs exist (and are in a block in the main chain, or the current block; see #prev_txs)



327
328
329
330
331
332
333
334
# File 'lib/bitcoin/validation.rb', line 327

def prev_out
  missing = tx.in.reject.with_index {|txin, idx|
    prev_txs[idx].out[txin.prev_out_index] rescue false }
  return true  if prev_txs.size == tx.in.size && missing.empty?

  missing.each {|i| store.log.warn { "prev out #{i.prev_out.reverse_hth}:#{i.prev_out_index} missing" } }
  missing.map {|i| [i.prev_out.reverse_hth, i.prev_out_index] }
end

#prev_txsObject

collect prev_txs needed to verify the inputs of this tx. only returns tx that are in a block in the main chain or the current block.



382
383
384
385
386
387
388
# File 'lib/bitcoin/validation.rb', line 382

def prev_txs
  @prev_txs ||= tx.in.map {|i|
    prev_tx = block_validator ? block_validator.prev_txs_hash[i.prev_out.reverse_hth] : store.get_tx(i.prev_out.reverse_hth)
    next prev_tx if prev_tx && prev_tx.blk_id # blk_id is set only if it's in the main chain
    @block.tx.find {|t| t.binary_hash == i.prev_out } if @block
  }.compact
end

#signaturesObject

check that all input signatures are valid



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

def signatures
  sigs = tx.in.map.with_index {|txin, idx| tx.verify_input_signature(idx, prev_txs[idx], (@block ? @block.time : 0)) }
  sigs.all? || sigs.map.with_index {|s, i| s ? nil : i }.compact
end

#standardObject

check that tx matches “standard” rules. this is currently disabled since not all miners enforce it.



319
320
321
322
323
# File 'lib/bitcoin/validation.rb', line 319

def standard
  return true  # not enforced by all miners
  return false  unless min_size
  tx.out.all? {|o| Bitcoin::Script.new(o.pk_script).is_standard? }
end

#total_inObject



391
392
393
# File 'lib/bitcoin/validation.rb', line 391

def total_in
  @total_in ||= tx.in.each_with_index.inject(0){|acc,(input,idx)| acc + prev_txs[idx].out[input.prev_out_index].value }
end

#total_outObject



395
396
397
# File 'lib/bitcoin/validation.rb', line 395

def total_out
  @total_out ||= tx.out.inject(0){|acc,output| acc + output.value }
end

#validate(opts = {}) ⇒ Object

validate tx rules. opts are:

rules

which rulesets to validate (default: [:syntax, :context])

raise_errors

whether to raise ValidationError on failure (default: false)



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/bitcoin/validation.rb', line 244

def validate(opts = {})
  return true  if KNOWN_EXCEPTIONS.include?(tx.hash)
  opts[:rules] ||= [:syntax, :context]
  opts[:rules].each do |name|
    store.log.debug { "validating tx #{name} #{tx.hash} (#{tx.to_payload.bytesize} bytes)" } if store
    RULES[name].each.with_index do |rule, i|
      unless (res = send(rule)) && res == true
        raise ValidationError, "tx error: #{name} check #{i} - #{rule} failed"  if opts[:raise_errors]
        @error = [rule, res]
        return false
      end
    end
  end
  clear_cache # memory optimizatons
  true
end