Class: Bitcoin::Protocol::Tx

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

Direct Known Subclasses

Storage::Models::Tx

Constant Summary collapse

SIGHASH_TYPE =
{ all: 1, none: 2, single: 3, anyonecanpay: 128 }
DEFAULT_BLOCK_PRIORITY_SIZE =
27000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data = nil) ⇒ Tx

create tx from raw binary data



45
46
47
48
# File 'lib/bitcoin/protocol/tx.rb', line 45

def initialize(data=nil)
  @ver, @lock_time, @in, @out, @scripts = 1, 0, [], [], []
  parse_data_from_io(data) if data
end

Instance Attribute Details

#hashObject (readonly)

transaction hash



11
12
13
# File 'lib/bitcoin/protocol/tx.rb', line 11

def hash
  @hash
end

#inObject (readonly) Also known as: inputs

inputs (Array of TxIn)



14
15
16
# File 'lib/bitcoin/protocol/tx.rb', line 14

def in
  @in
end

#lock_timeObject

lock time



26
27
28
# File 'lib/bitcoin/protocol/tx.rb', line 26

def lock_time
  @lock_time
end

#outObject (readonly) Also known as: outputs

outputs (Array of TxOut)



17
18
19
# File 'lib/bitcoin/protocol/tx.rb', line 17

def out
  @out
end

#payloadObject (readonly)

raw protocol payload



20
21
22
# File 'lib/bitcoin/protocol/tx.rb', line 20

def payload
  @payload
end

#scriptsObject (readonly)

parsed / evaluated input scripts cached for later use



29
30
31
# File 'lib/bitcoin/protocol/tx.rb', line 29

def scripts
  @scripts
end

#verObject

version (usually 1)



23
24
25
# File 'lib/bitcoin/protocol/tx.rb', line 23

def ver
  @ver
end

Class Method Details

.binary_from_hash(h) ⇒ Object

convert ruby hash to raw binary



217
# File 'lib/bitcoin/protocol/tx.rb', line 217

def self.binary_from_hash(h); from_hash(h).to_payload; end

.binary_from_json(json_string) ⇒ Object

convert json representation to raw binary



223
# File 'lib/bitcoin/protocol/tx.rb', line 223

def self.binary_from_json(json_string); from_json(json_string).to_payload; end

.from_file(path) ⇒ Object

read binary block from a file



226
# File 'lib/bitcoin/protocol/tx.rb', line 226

def self.from_file(path); new( Bitcoin::Protocol.read_binary_file(path) ); end

.from_hash(h) ⇒ Object

parse ruby hash (see also #to_hash)



207
208
209
210
211
212
213
214
# File 'lib/bitcoin/protocol/tx.rb', line 207

def self.from_hash(h)
  tx = new(nil)
  tx.ver, tx.lock_time = *h.values_at('ver', 'lock_time')
  h['in'] .each{|input|   tx.add_in  TxIn.from_hash(input)   }
  h['out'].each{|output|  tx.add_out TxOut.from_hash(output) }
  tx.instance_eval{ @hash = hash_from_payload(@payload = to_payload) }
  tx
end

.from_json(json_string) ⇒ Object

parse json representation



220
# File 'lib/bitcoin/protocol/tx.rb', line 220

def self.from_json(json_string); from_hash( JSON.load(json_string) ); end

.from_json_file(path) ⇒ Object

read json block from a file



229
# File 'lib/bitcoin/protocol/tx.rb', line 229

def self.from_json_file(path); from_json( Bitcoin::Protocol.read_binary_file(path) ); end

Instance Method Details

#==(other) ⇒ Object

compare to another tx



35
36
37
# File 'lib/bitcoin/protocol/tx.rb', line 35

def ==(other)
  @hash == other.hash
end

#add_in(input) ⇒ Object

add an input



57
# File 'lib/bitcoin/protocol/tx.rb', line 57

def add_in(input); (@in ||= []) << input; end

#add_out(output) ⇒ Object

add an output



60
# File 'lib/bitcoin/protocol/tx.rb', line 60

def add_out(output); (@out ||= []) << output; end

#binary_hashObject

return the tx hash in binary format



40
41
42
# File 'lib/bitcoin/protocol/tx.rb', line 40

def binary_hash
  @binary_hash ||= [@hash].pack("H*").reverse
end

#calculate_minimum_fee(allow_free = true, mode = :block) ⇒ Object



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
# File 'lib/bitcoin/protocol/tx.rb', line 277

def calculate_minimum_fee(allow_free=true, mode=:block)
  # Base fee is either nMinTxFee or nMinRelayTxFee
  base_fee  = (mode == :relay) ? Bitcoin.network[:min_relay_tx_fee] : Bitcoin.network[:min_tx_fee]
  tx_size   = to_payload.bytesize
  min_fee   = (1 + tx_size / 1_000) * base_fee

  if allow_free
    # There is a free transaction area in blocks created by most miners,
    # * If we are relaying we allow transactions up to DEFAULT_BLOCK_PRIORITY_SIZE - 1000
    #   to be considered to fall into this category. We don't want to encourage sending
    #   multiple transactions instead of one big transaction to avoid fees.
    # * If we are creating a transaction we allow transactions up to 1,000 bytes
    #   to be considered safe and assume they can likely make it into this section.
    min_fee = 0 if tx_size < (mode == :block ? Bitcoin.network[:free_tx_bytes] : DEFAULT_BLOCK_PRIORITY_SIZE - 1_000)
  end

  # This code can be removed after enough miners have upgraded to version 0.9.
  # Until then, be safe when sending and require a fee if any output is less than CENT
  if min_fee < base_fee && mode == :block
    outputs.each do |output|
      if output.value < Bitcoin.network[:dust]
        # If per dust fee, then we add min fee for each output less than dust.
        # Otherwise, we set to min fee if there is any output less than dust.
        if Bitcoin.network[:per_dust_fee]
          min_fee += base_fee
        else
          min_fee = base_fee
          break
        end
      end
    end
  end

  min_fee = Bitcoin::network[:max_money] unless min_fee.between?(0, Bitcoin::network[:max_money])
  min_fee
end

#hash_from_payload(payload) ⇒ Object Also known as: generate_hash

generate the tx hash for given payload in hex format



51
52
53
# File 'lib/bitcoin/protocol/tx.rb', line 51

def hash_from_payload(payload)
  Digest::SHA256.digest(Digest::SHA256.digest( payload )).reverse_hth
end

#is_coinbase?Boolean

Returns:

  • (Boolean)


314
315
316
# File 'lib/bitcoin/protocol/tx.rb', line 314

def is_coinbase?
  inputs.size == 1 and inputs.first.coinbase?
end

#is_final?(block_height, block_time) ⇒ Boolean

Checks if transaction is final taking into account height and time of a block in which it is located (or about to be included if it’s unconfirmed tx).

Returns:

  • (Boolean)


244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/bitcoin/protocol/tx.rb', line 244

def is_final?(block_height, block_time)
  # No time lock - tx is final.
  return true if lock_time == 0

  # Time based nLockTime implemented in 0.1.6
  # If lock_time is below the magic threshold treat it as a block height.
  # If lock_time is above the threshold, it's a unix timestamp.
  return true if lock_time < (lock_time < Bitcoin::LOCKTIME_THRESHOLD ? block_height : block_time)

  inputs.each{|input| return false if !input.is_final? }

  return true
end

#legacy_sigops_countObject



258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/bitcoin/protocol/tx.rb', line 258

def legacy_sigops_count
  # Note: input scripts normally never have any opcodes since every input script 
  # can be statically reduced to a pushdata-only script.
  # However, anyone is allowed to create a non-standard transaction with any opcodes in the inputs.
  count = 0
  self.in.each do |txin|
    count += Bitcoin::Script.new(txin.script_sig).sigops_count_accurate(false)
  end
  self.out.each do |txout|
    count += Bitcoin::Script.new(txout.pk_script).sigops_count_accurate(false)
  end
  count
end

#minimum_block_feeObject



275
# File 'lib/bitcoin/protocol/tx.rb', line 275

def minimum_block_fee; calculate_minimum_fee(allow_free=true, :block); end

#minimum_relay_feeObject



274
# File 'lib/bitcoin/protocol/tx.rb', line 274

def minimum_relay_fee; calculate_minimum_fee(allow_free=true, :relay); end

#normalized_hashObject Also known as: nhash



318
319
320
# File 'lib/bitcoin/protocol/tx.rb', line 318

def normalized_hash
  signature_hash_for_input(-1, nil, SIGHASH_TYPE[:all]).unpack("H*")[0]
end

#parse_data_from_io(data) ⇒ Object Also known as: parse_data

parse raw binary data



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/bitcoin/protocol/tx.rb', line 63

def parse_data_from_io(data)
  buf = data.is_a?(String) ? StringIO.new(data) : data
  payload_start = buf.pos

  @ver = buf.read(4).unpack("V")[0]

  in_size = Protocol.unpack_var_int_from_io(buf)
  @in = []
  in_size.times{ @in << TxIn.from_io(buf) }

  out_size = Protocol.unpack_var_int_from_io(buf)
  @out = []
  out_size.times{ @out << TxOut.from_io(buf) }

  @lock_time = buf.read(4).unpack("V")[0]

  payload_end = buf.pos;
  buf.seek(payload_start)
  @payload = buf.read( payload_end-payload_start )
  @hash = hash_from_payload(@payload)

  if buf.eof?
    true
  else
    data.is_a?(StringIO) ? buf : buf.read
  end
end

#signature_hash_for_input(input_idx, subscript, hash_type = nil) ⇒ Object

generate a signature hash for input input_idx. either pass the outpoint_tx or the script_pubkey directly.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/bitcoin/protocol/tx.rb', line 108

def signature_hash_for_input(input_idx, subscript, hash_type=nil)
  # https://github.com/bitcoin/bitcoin/blob/e071a3f6c06f41068ad17134189a4ac3073ef76b/script.cpp#L834
  # http://code.google.com/p/bitcoinj/source/browse/trunk/src/com/google/bitcoin/core/Script.java#318
  # https://en.bitcoin.it/wiki/OP_CHECKSIG#How_it_works
  # https://github.com/bitcoin/bitcoin/blob/c2e8c8acd8ae0c94c70b59f55169841ad195bb99/src/script.cpp#L1058
  # https://en.bitcoin.it/wiki/OP_CHECKSIG

  # Note: BitcoinQT checks if input_idx >= @in.size and returns 1 with an error message.
  # But this check is never actually useful because BitcoinQT would crash 
  # right before VerifyScript if input index is out of bounds (inside CScriptCheck::operator()()).
  # That's why we don't need to do such a check here.
  #
  # However, if you look at the case SIGHASH_TYPE[:single] below, we must 
  # return 1 because it's possible to have more inputs than outputs and BitcoinQT returns 1 as well.
  return "\x01".ljust(32, "\x00") if input_idx >= @in.size # ERROR: SignatureHash() : input_idx=%d out of range

  hash_type ||= SIGHASH_TYPE[:all]

  pin  = @in.map.with_index{|input,idx|
    if idx == input_idx
      subscript = subscript.out[ input.prev_out_index ].script if subscript.respond_to?(:out) # legacy api (outpoint_tx)
      input.to_payload(subscript)
    else
      case (hash_type & 0x1f)
      when SIGHASH_TYPE[:none];   input.to_payload("", "\x00\x00\x00\x00")
      when SIGHASH_TYPE[:single]; input.to_payload("", "\x00\x00\x00\x00")
      else;                       input.to_payload("")
      end
    end
  }

  pout = @out.map(&:to_payload)
  in_size, out_size = Protocol.pack_var_int(@in.size), Protocol.pack_var_int(@out.size)

  case (hash_type & 0x1f)
  when SIGHASH_TYPE[:none]
    pout = ""
    out_size = Protocol.pack_var_int(0)
  when SIGHASH_TYPE[:single]
    return "\x01".ljust(32, "\x00") if input_idx >= @out.size # ERROR: SignatureHash() : input_idx=%d out of range (SIGHASH_SINGLE)
    pout = @out[0...(input_idx+1)].map.with_index{|out,idx| (idx==input_idx) ? out.to_payload : out.to_null_payload }.join
    out_size = Protocol.pack_var_int(input_idx+1)
  end

  if (hash_type & SIGHASH_TYPE[:anyonecanpay]) != 0
    in_size, pin = Protocol.pack_var_int(1), [ pin[input_idx] ]
  end

  buf = [ [@ver].pack("V"), in_size, pin, out_size, pout, [@lock_time, hash_type].pack("VV") ].join
  Digest::SHA256.digest( Digest::SHA256.digest( buf ) )
end

#sizeObject



238
239
240
# File 'lib/bitcoin/protocol/tx.rb', line 238

def size
  payload.bytesize
end

#to_hash(options = {}) ⇒ Object

convert to ruby hash (see also #from_hash)



183
184
185
186
187
188
189
190
191
192
193
# File 'lib/bitcoin/protocol/tx.rb', line 183

def to_hash(options = {})
  @hash ||= hash_from_payload(to_payload)
  h = {
    'hash' => @hash, 'ver' => @ver, # 'nid' => normalized_hash,
    'vin_sz' => @in.size, 'vout_sz' => @out.size,
    'lock_time' => @lock_time, 'size' => (@payload ||= to_payload).bytesize,
    'in'  =>  @in.map{|i| i.to_hash(options) },
    'out' => @out.map{|o| o.to_hash(options) }
  }
  h
end

#to_json(options = {:space => ''}, *a) ⇒ Object

generates rawblock json as seen in the block explorer.



196
197
198
# File 'lib/bitcoin/protocol/tx.rb', line 196

def to_json(options = {:space => ''}, *a)
  JSON.pretty_generate( to_hash(options), options )
end

#to_json_file(path) ⇒ Object

write json representation to a file (see also #to_json)



202
203
204
# File 'lib/bitcoin/protocol/tx.rb', line 202

def to_json_file(path)
  File.open(path, 'wb'){|f| f.print to_json; }
end

#to_payloadObject

output transaction in raw binary format



94
95
96
97
98
99
100
101
# File 'lib/bitcoin/protocol/tx.rb', line 94

def to_payload
  pin = ""
  @in.each{|input| pin << input.to_payload }
  pout = ""
  @out.each{|output| pout << output.to_payload }

  [@ver].pack("V") << Protocol.pack_var_int(@in.size) << pin << Protocol.pack_var_int(@out.size) << pout << [@lock_time].pack("V")
end

#validator(store, block = nil, block_validator = nil) ⇒ Object

Get a Bitcoin::Validation object to validate this block. It needs a store to validate against, a block to validate tx chains inside one block, and optionally takes the block_validator as an optimization.



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

def validator(store, block = nil, block_validator = nil)
  @validator ||= Bitcoin::Validation::Tx.new(self, store, block, block_validator)
end

#verify_input_signature(in_idx, outpoint_tx_or_script, block_timestamp = Time.now.to_i) ⇒ Object

verify input signature in_idx against the corresponding output in outpoint_tx outpoint



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/bitcoin/protocol/tx.rb', line 163

def verify_input_signature(in_idx, outpoint_tx_or_script, block_timestamp=Time.now.to_i)
  outpoint_idx  = @in[in_idx].prev_out_index
  script_sig    = @in[in_idx].script_sig
  
  # If given an entire previous transaction, take the script from it
  script_pubkey = if outpoint_tx_or_script.respond_to?(:out) 
    outpoint_tx_or_script.out[outpoint_idx].pk_script
  else
    # Otherwise, it's already a script.
    outpoint_tx_or_script
  end

  @scripts[in_idx] = Bitcoin::Script.new(script_sig, script_pubkey)
  @scripts[in_idx].run(block_timestamp) do |pubkey,sig,hash_type,subscript|
    hash = signature_hash_for_input(in_idx, subscript, hash_type)
    Bitcoin.verify_signature( hash, sig, pubkey.unpack("H*")[0] )
  end
end