Class: Bitcoin::Storage::Backends::StoreBase

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

Overview

Base class for storage backends. Every backend must overwrite the “Not implemented” methods and provide an implementation specific to the storage. Also, before returning the objects, they should be wrapped inside the appropriate Bitcoin::Storage::Models class.

Direct Known Subclasses

DummyStore, SequelStoreBase

Constant Summary collapse

MAIN =

main branch (longest valid chain)

0
SIDE =

side branch (connected, valid, but too short)

1
ORPHAN =

orphan branch (not connected to main branch / genesis block)

2
SCRIPT_TYPES =

possible script types

[:unknown, :pubkey, :hash160, :multisig, :p2sh, :op_return]
ADDRESS_TYPES =

possible address types

[:hash160, :p2sh]
DEFAULT_CONFIG =
{}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = {}, getblocks_callback = nil) ⇒ StoreBase

Returns a new instance of StoreBase.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/bitcoin/storage/storage.rb', line 60

def initialize(config = {}, getblocks_callback = nil)
  # merge all the configuration defaults, keeping the most specific ones.
  store_ancestors = self.class.ancestors.select {|a| a.name =~ /StoreBase$/ }.reverse
  base = store_ancestors.reduce(store_ancestors[0]::DEFAULT_CONFIG) do |config, ancestor|
    config.merge(ancestor::DEFAULT_CONFIG)
  end
  @config = base.merge(self.class::DEFAULT_CONFIG).merge(config)
  @log    = config[:log] || Bitcoin::Storage.log
  @log.level = @config[:log_level]  if @config[:log_level]
  init_store_connection
  @getblocks_callback = getblocks_callback
  @checkpoints = Bitcoin.network[:checkpoints] || {}
  @watched_addrs = []
  @notifiers = {}
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



58
59
60
# File 'lib/bitcoin/storage/storage.rb', line 58

def config
  @config
end

#logObject (readonly)

Returns the value of attribute log.



56
57
58
# File 'lib/bitcoin/storage/storage.rb', line 56

def log
  @log
end

Instance Method Details

#add_watched_address(address) ⇒ Object



381
382
383
384
385
# File 'lib/bitcoin/storage/storage.rb', line 381

def add_watched_address address
  hash160 = Bitcoin.hash160_from_address(address)
  @db[:addr].insert(hash160: hash160)  unless @db[:addr][hash160: hash160]
  @watched_addrs << hash160  unless @watched_addrs.include?(hash160)
end

#backend_nameObject

name of the storage backend currently in use (“sequel” or “utxo”)



80
81
82
# File 'lib/bitcoin/storage/storage.rb', line 80

def backend_name
  self.class.name.split("::")[-1].split("Store")[0].downcase
end

#check_consistency(count) ⇒ Object

check data consistency of the top count blocks.



90
91
92
# File 'lib/bitcoin/storage/storage.rb', line 90

def check_consistency count
  raise "Not implemented"
end

#get_balance(hash160_or_addr, unconfirmed = false) ⇒ Object

get balance for given hash160



340
341
342
343
344
345
346
347
348
349
350
# File 'lib/bitcoin/storage/storage.rb', line 340

def get_balance(hash160_or_addr, unconfirmed = false)
  if Bitcoin.valid_address?(hash160_or_addr)
    txouts = get_txouts_for_address(hash160_or_addr)
  else
    txouts = get_txouts_for_hash160(hash160_or_addr, :hash160, unconfirmed)
  end
  unspent = txouts.select {|o| o.get_next_in.nil?}
  unspent.map(&:value).inject {|a,b| a+=b; a} || 0
rescue
  nil
end

#get_block(blk_hash) ⇒ Object

get block with given blk_hash



252
253
254
# File 'lib/bitcoin/storage/storage.rb', line 252

def get_block(blk_hash)
  raise "Not implemented"
end

#get_block_by_depth(depth) ⇒ Object

get block with given depth from main chain



257
258
259
# File 'lib/bitcoin/storage/storage.rb', line 257

def get_block_by_depth(depth)
  raise "Not implemented"
end

#get_block_by_id(block_id) ⇒ Object

get block by given block_id



272
273
274
# File 'lib/bitcoin/storage/storage.rb', line 272

def get_block_by_id(block_id)
  raise "Not implemented"
end

#get_block_by_prev_hash(prev_hash) ⇒ Object

get block with given prev_hash



262
263
264
# File 'lib/bitcoin/storage/storage.rb', line 262

def get_block_by_prev_hash(prev_hash)
  raise "Not implemented"
end

#get_block_by_tx(tx_hash) ⇒ Object

get block that includes tx with given tx_hash



267
268
269
# File 'lib/bitcoin/storage/storage.rb', line 267

def get_block_by_tx(tx_hash)
  raise "Not implemented"
end

#get_block_id_for_tx_id(tx_id) ⇒ Object

get block id in main chain by given tx_id



277
278
279
# File 'lib/bitcoin/storage/storage.rb', line 277

def get_block_id_for_tx_id(tx_id)
  get_tx_by_id(tx_id).blk_id rescue nil # tx.blk_id is always in main chain
end

#get_depthObject

return depth of the head block



222
223
224
# File 'lib/bitcoin/storage/storage.rb', line 222

def get_depth
  raise "Not implemented"
end

#get_headObject

get the hash of the leading block



217
218
219
# File 'lib/bitcoin/storage/storage.rb', line 217

def get_head
  raise "Not implemented"
end

#get_idx_from_tx_hash(tx_hash) ⇒ Object

Grab the position of a tx in a given block



311
312
313
# File 'lib/bitcoin/storage/storage.rb', line 311

def get_idx_from_tx_hash(tx_hash)
  raise "Not implemented"
end

#get_locator(pointer = get_head) ⇒ Object

compute blockchain locator



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/bitcoin/storage/storage.rb', line 227

def get_locator pointer = get_head
  if @locator
    locator, head = @locator
    if head == pointer
      return locator
    end
  end

  return [("\x00"*32).hth]  if get_depth == -1
  locator, step, orig_pointer = [], 1, pointer
  while pointer && pointer.hash != Bitcoin::network[:genesis_hash]
    locator << pointer.hash
    depth = pointer.depth - step
    break unless depth > 0
    prev_block = get_block_by_depth(depth) # TODO
    break unless prev_block
    pointer = prev_block
    step *= 2  if locator.size > 10
  end
  locator << Bitcoin::network[:genesis_hash]
  @locator = [locator, orig_pointer]
  locator
end

#get_tx(tx_hash) ⇒ Object

get tx with given tx_hash



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

def get_tx(tx_hash)
  raise "Not implemented"
end

#get_tx_by_id(tx_id) ⇒ Object

get tx with given tx_id



306
307
308
# File 'lib/bitcoin/storage/storage.rb', line 306

def get_tx_by_id(tx_id)
  raise "Not implemented"
end

#get_txin_for_txout(tx_hash, txout_idx) ⇒ Object

get corresponding txin for the txout in transaction tx_hash with index txout_idx



283
284
285
# File 'lib/bitcoin/storage/storage.rb', line 283

def get_txin_for_txout(tx_hash, txout_idx)
  raise "Not implemented"
end

#get_txins_for_txouts(txouts) ⇒ Object

get an array of corresponding txins for provided txouts txouts = [tx_hash, tx_idx] can be overwritten by specific storage for opimization



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

def get_txins_for_txouts(txouts)
  txouts.map{|tx_hash, tx_idx| get_txin_for_txout(tx_hash, tx_idx) }.compact
end

#get_txouts_for_address(address, unconfirmed = false) ⇒ Object

collect all txouts containing a standard tx to given address



323
324
325
326
327
# File 'lib/bitcoin/storage/storage.rb', line 323

def get_txouts_for_address(address, unconfirmed = false)
  hash160 = Bitcoin.hash160_from_address(address)
  type = Bitcoin.address_type(address)
  get_txouts_for_hash160(hash160, type, unconfirmed)
end

#get_txouts_for_pk_script(script) ⇒ Object

collect all txouts containing the given script



317
318
319
# File 'lib/bitcoin/storage/storage.rb', line 317

def get_txouts_for_pk_script(script)
  raise "Not implemented"
end

#get_txs(tx_hashes) ⇒ Object

get more than one tx by tx_hashes, returns an array can be reimplemented by specific storage for optimization



301
302
303
# File 'lib/bitcoin/storage/storage.rb', line 301

def get_txs(tx_hashes)
  tx_hashes.map {|h| get_tx(h)}.compact
end

#get_unspent_txouts_for_address(address, unconfirmed = false) ⇒ Object

collect all unspent txouts containing a standard tx to given address



331
332
333
334
335
336
337
# File 'lib/bitcoin/storage/storage.rb', line 331

def get_unspent_txouts_for_address(address, unconfirmed = false)
  txouts = self.get_txouts_for_address(address, unconfirmed)
  txouts.select! do |t|
    not t.get_next_in
  end
  txouts
end

#has_block(blk_hash) ⇒ Object

check if block with given blk_hash is already stored



207
208
209
# File 'lib/bitcoin/storage/storage.rb', line 207

def has_block(blk_hash)
  raise "Not implemented"
end

#has_tx(tx_hash) ⇒ Object

check if tx with given tx_hash is already stored



212
213
214
# File 'lib/bitcoin/storage/storage.rb', line 212

def has_tx(tx_hash)
  raise "Not implemented"
end

#import(filename, max_depth = nil) ⇒ Object

import satoshi bitcoind blk0001.dat blockchain file



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/bitcoin/storage/storage.rb', line 392

def import filename, max_depth = nil
  if File.file?(filename)
    log.info { "Importing #{filename}" }
    File.open(filename) do |file|
      until file.eof?
        magic = file.read(4)

        # bitcoind pads the ends of the block files so that it doesn't
        # have to reallocate space on every new block.
        break if magic == "\0\0\0\0"
        raise "invalid network magic" unless Bitcoin.network[:magic_head] == magic

        size = file.read(4).unpack("L")[0]
        blk = Bitcoin::P::Block.new(file.read(size))
        depth, chain = new_block(blk)
        break  if max_depth && depth >= max_depth
      end
    end
  elsif File.directory?(filename)
    Dir.entries(filename).sort.each do |file|
      next  unless file =~ /^blk.*?\.dat$/
      import(File.join(filename, file), max_depth)
    end
  else
    raise "Import dir/file #{filename} not found"
  end
end

#in_sync?Boolean

Returns:

  • (Boolean)


420
421
422
# File 'lib/bitcoin/storage/storage.rb', line 420

def in_sync?
  (get_head && (Time.now - get_head.time).to_i < 3600) ? true : false
end

#init_store_connectionObject



76
77
# File 'lib/bitcoin/storage/storage.rb', line 76

def init_store_connection
end

#new_block(blk) ⇒ Object

handle a new block incoming from the network



96
97
98
99
100
101
102
103
# File 'lib/bitcoin/storage/storage.rb', line 96

def new_block blk
  time = Time.now
  res = store_block(blk)
  log.info { "block #{blk.hash} " +
    "[#{res[0]}, #{['main', 'side', 'orphan'][res[1]]}] " +
    "(#{"%.4fs, %3dtx, %.3fkb" % [(Time.now - time), blk.tx.size, blk.payload.bytesize.to_f/1000]})" }  if res && res[1]
  res
end

#new_tx(tx) ⇒ Object



197
198
199
# File 'lib/bitcoin/storage/storage.rb', line 197

def new_tx(tx)
  store_tx(tx)
end

#parse_script(txout, i, tx_hash = "", tx_idx) ⇒ Object

parse script and collect address/txout mappings to index



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/bitcoin/storage/storage.rb', line 353

def parse_script txout, i, tx_hash = "", tx_idx
  addrs, names = [], []

  script = Bitcoin::Script.new(txout.pk_script) rescue nil
  if script
    if script.is_hash160? || script.is_pubkey? || script.is_p2sh?
      addrs << [i, script.get_address]
    elsif script.is_multisig?
      script.get_multisig_addresses.map do |address|
        addrs << [i, address]
      end
    elsif Bitcoin.namecoin? && script.is_namecoin?
      addrs << [i, script.get_address]
      names << [i, script]
    elsif script.is_op_return?
      log.info { "Ignoring OP_RETURN script: #{script.get_op_return_data}" }
    else
      log.info { "Unknown script type in txout #{tx_hash}:#{tx_idx}" }
      log.debug { script.to_string }
    end
    script_type = SCRIPT_TYPES.index(script.type)
  else
    log.error { "Error parsing script #{tx_hash}:#{tx_idx}" }
    script_type = SCRIPT_TYPES.index(:unknown)
  end
  [script_type, addrs, names]
end

#persist_block(blk) ⇒ Object

persist given block blk to storage.



187
188
189
# File 'lib/bitcoin/storage/storage.rb', line 187

def persist_block(blk)
  raise "Not implemented"
end

#push_notification(channel, message) ⇒ Object



424
425
426
# File 'lib/bitcoin/storage/storage.rb', line 424

def push_notification channel, message
  @notifiers[channel.to_sym].push(message)  if @notifiers[channel.to_sym]
end

#rescanObject



387
388
389
# File 'lib/bitcoin/storage/storage.rb', line 387

def rescan
  raise "Not implemented"
end

#resetObject

reset the store; delete all data



85
86
87
# File 'lib/bitcoin/storage/storage.rb', line 85

def reset
  raise "Not implemented"
end

#store_block(blk) ⇒ Object

store given block blk. determine branch/chain and dept of block. trigger reorg if side branch becomes longer than current main chain and connect orpans.



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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/bitcoin/storage/storage.rb', line 108

def store_block blk
  log.debug { "new block #{blk.hash}" }

  existing = get_block(blk.hash)
  if existing && existing.chain == MAIN
    log.debug { "=> exists (#{existing.depth}, #{existing.chain})" }
    return [existing.depth]
  end

  prev_block = get_block(blk.prev_block.reverse_hth)
  unless @config[:skip_validation]
    validator = blk.validator(self, prev_block)
    validator.validate(rules: [:syntax], raise_errors: true)
  end

  if !prev_block || prev_block.chain == ORPHAN
    if blk.hash == Bitcoin.network[:genesis_hash]
      log.debug { "=> genesis (0)" }
      return persist_block(blk, MAIN, 0)
    else
      depth = prev_block ? prev_block.depth + 1 : 0
      log.debug { "=> orphan (#{depth})" }
      return [0, 2]  unless (in_sync? || Bitcoin.network_name =~ /testnet/)
      return persist_block(blk, ORPHAN, depth)
    end
  end
  depth = prev_block.depth + 1

  checkpoint = @checkpoints[depth]
  if checkpoint && blk.hash != checkpoint
    log.warn "Block #{depth} doesn't match checkpoint #{checkpoint}"
    exit  if depth > get_depth # TODO: handle checkpoint mismatch properly
  end
  if prev_block.chain == MAIN
    if prev_block == get_head
      log.debug { "=> main (#{depth})" }
      if !@config[:skip_validation] && ( !@checkpoints.any? || depth > @checkpoints.keys.last )
        if self.class.name =~ /UtxoStore/
          @config[:utxo_cache] = 0
          @config[:block_cache] = 120
        end
        validator.validate(rules: [:context], raise_errors: true)
      end
      res = persist_block(blk, MAIN, depth, prev_block.work)
      push_notification(:block, [blk, *res])
      return res
    else
      log.debug { "=> side (#{depth})" }
      return persist_block(blk, SIDE, depth, prev_block.work)
    end
  else
    head = get_head
    if prev_block.work + blk.block_work  <= head.work
      log.debug { "=> side (#{depth})" }
      return persist_block(blk, SIDE, depth, prev_block.work)
    else
      log.debug { "=> reorg" }
      new_main, new_side = [], []
      fork_block = prev_block
      while fork_block.chain != MAIN
        new_main << fork_block.hash
        fork_block = fork_block.get_prev_block
      end
      b = fork_block
      while b = b.get_next_block
        new_side << b.hash
      end
      log.debug { "new main: #{new_main.inspect}" }
      log.debug { "new side: #{new_side.inspect}" }

      push_notification(:reorg, [ new_main, new_side ])

      reorg(new_side.reverse, new_main.reverse)
      return persist_block(blk, MAIN, depth, prev_block.work)
    end
  end
end

#store_tx(tx, validate = true) ⇒ Object

store given tx



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

def store_tx(tx, validate = true)
  raise "Not implemented"
end

#subscribe(channel) ⇒ Object



428
429
430
431
# File 'lib/bitcoin/storage/storage.rb', line 428

def subscribe channel
  @notifiers[channel.to_sym] ||= EM::Channel.new
  @notifiers[channel.to_sym].subscribe {|*data| yield(*data) }
end

#update_block(hash, attrs) ⇒ Object

update attrs for block with given hash. typically used to update the chain value during reorg.



193
194
195
# File 'lib/bitcoin/storage/storage.rb', line 193

def update_block(hash, attrs)
  raise "Not implemented"
end