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, SequelStore, UtxoStore

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]
DEFAULT_CONFIG =
{
  sqlite_pragmas: {
    # journal_mode pragma
    journal_mode: false,
    # synchronous pragma
    synchronous: false,
    # cache_size pragma
    # positive specifies number of cache pages to use,
    # negative specifies cache size in kilobytes.
    cache_size: -200_000,
  }
}
SEQUEL_ADAPTERS =
{ :sqlite => "sqlite3", :postgres => "pg", :mysql => "mysql" }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of StoreBase.



70
71
72
73
74
75
76
77
78
79
# File 'lib/bitcoin/storage/storage.rb', line 70

def initialize(config = {}, getblocks_callback = nil)
  base = self.class.ancestors.select {|a| a.name =~ /StoreBase$/ }[0]::DEFAULT_CONFIG
  @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_sequel_store
  @getblocks_callback = getblocks_callback
  @checkpoints = Bitcoin.network[:checkpoints] || {}
  @watched_addrs = []
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



66
67
68
# File 'lib/bitcoin/storage/storage.rb', line 66

def config
  @config
end

#logObject (readonly)

Returns the value of attribute log.



66
67
68
# File 'lib/bitcoin/storage/storage.rb', line 66

def log
  @log
end

Instance Method Details

#add_watched_address(address) ⇒ Object



405
406
407
408
409
# File 'lib/bitcoin/storage/storage.rb', line 405

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



140
141
142
# File 'lib/bitcoin/storage/storage.rb', line 140

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

#check_metadataObject

check that database network magic and backend match the ones we are using



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/bitcoin/storage/storage.rb', line 114

def 
  version = @db[:schema_info].first
  unless version[:magic] == Bitcoin.network[:magic_head].hth
    name = Bitcoin::NETWORKS.find{|n,d| d[:magic_head].hth == version[:magic]}[0]
    raise "Error: DB #{@db.url} was created for '#{name}' network!"
  end
  unless version[:backend] == backend_name
    if version[:backend] == "sequel" && backend_name == "utxo"
      log.warn { "Note: The 'utxo' store is now the default backend.
      To keep using the full storage, change the configuration to use storage: 'sequel::#{@db.url}'.
      To use the new storage backend, delete or move #{@db.url}, or specify a different database path in the config." }
    end
    raise "Error: DB #{@db.url} was created for '#{version[:backend]}' backend!"
  end
end

#connectObject

connect to database



91
92
93
94
95
96
97
# File 'lib/bitcoin/storage/storage.rb', line 91

def connect
  Sequel.extension(:core_extensions, :sequel_3_dataset_methods)
  @db = Sequel.connect(@config[:db].sub("~", ENV["HOME"]))
  @db.extend_datasets(Sequel::Sequel3DatasetMethods)
  sqlite_pragmas; migrate; 
  log.info { "opened #{backend_name} store #{@db.uri}" }
end

#get_balance(hash160, unconfirmed = false) ⇒ Object

get balance for given hash160



361
362
363
364
365
366
367
# File 'lib/bitcoin/storage/storage.rb', line 361

def get_balance(hash160, unconfirmed = false)
  txouts = get_txouts_for_hash160(hash160, unconfirmed)
  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



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

def get_block(blk_hash)
  raise "Not implemented"
end

#get_block_by_depth(depth) ⇒ Object

get block with given depth from main chain



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

def get_block_by_depth(depth)
  raise "Not implemented"
end

#get_block_by_id(block_id) ⇒ Object

get block by given block_id



322
323
324
# File 'lib/bitcoin/storage/storage.rb', line 322

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



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

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



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

def get_block_by_tx(tx_hash)
  raise "Not implemented"
end

#get_depthObject

return depth of the head block



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

def get_depth
  raise "Not implemented"
end

#get_headObject

get the hash of the leading block



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

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



343
344
345
# File 'lib/bitcoin/storage/storage.rb', line 343

def get_idx_from_tx_hash(tx_hash)
  raise "Not implemented"
end

#get_locator(pointer = get_head) ⇒ Object

compute blockchain locator



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/bitcoin/storage/storage.rb', line 277

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



333
334
335
# File 'lib/bitcoin/storage/storage.rb', line 333

def get_tx(tx_hash)
  raise "Not implemented"
end

#get_tx_by_id(tx_id) ⇒ Object

get tx with given tx_id



338
339
340
# File 'lib/bitcoin/storage/storage.rb', line 338

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



328
329
330
# File 'lib/bitcoin/storage/storage.rb', line 328

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

#get_txouts_for_address(address, unconfirmed = false) ⇒ Object

collect all txouts containing a standard tx to given address



355
356
357
358
# File 'lib/bitcoin/storage/storage.rb', line 355

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

#get_txouts_for_pk_script(script) ⇒ Object

collect all txouts containing the given script



349
350
351
# File 'lib/bitcoin/storage/storage.rb', line 349

def get_txouts_for_pk_script(script)
  raise "Not implemented"
end

#has_block(blk_hash) ⇒ Object

check if block with given blk_hash is already stored



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

def has_block(blk_hash)
  raise "Not implemented"
end

#has_tx(tx_hash) ⇒ Object

check if tx with given tx_hash is already stored



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

def has_tx(tx_hash)
  raise "Not implemented"
end

#import(filename, max_depth = nil) ⇒ Object

import satoshi bitcoind blk0001.dat blockchain file



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/bitcoin/storage/storage.rb', line 416

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


439
440
441
# File 'lib/bitcoin/storage/storage.rb', line 439

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

#init_sequel_storeObject



81
82
83
84
85
86
87
88
# File 'lib/bitcoin/storage/storage.rb', line 81

def init_sequel_store
  return  unless (self.is_a?(SequelStore) || self.is_a?(UtxoStore)) && @config[:db]
  @config[:db].sub!("~", ENV["HOME"])
  @config[:db].sub!("<network>", Bitcoin.network_name.to_s)
  adapter = SEQUEL_ADAPTERS[@config[:db].split(":").first] rescue nil
  Bitcoin.require_dependency(adapter, gem: adapter)  if adapter
  connect
end

#migrateObject

check if schema is up to date and migrate to current version if necessary



100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/bitcoin/storage/storage.rb', line 100

def migrate
  migrations_path = File.join(File.dirname(__FILE__), "#{backend_name}/migrations")
  Sequel.extension :migration
  unless Sequel::Migrator.is_current?(@db, migrations_path)
    log = @log; @db.instance_eval { @log = log }
    Sequel::Migrator.run(@db, migrations_path)
    unless (v = @db[:schema_info].first) && v[:magic] && v[:backend]
      @db[:schema_info].update(
        magic: Bitcoin.network[:magic_head].hth, backend: backend_name)
    end
  end
end

#new_block(blk) ⇒ Object

handle a new block incoming from the network



150
151
152
153
154
155
156
157
# File 'lib/bitcoin/storage/storage.rb', line 150

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



247
248
249
# File 'lib/bitcoin/storage/storage.rb', line 247

def new_tx(tx)
  store_tx(tx)
end

#parse_script(txout, i) ⇒ Object

parse script and collect address/txout mappings to index



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/bitcoin/storage/storage.rb', line 379

def parse_script txout, i
  addrs, names = [], []
  # skip huge script in testnet3 block 54507 (998000 bytes)
  return [SCRIPT_TYPES.index(:unknown), [], []]  if txout.pk_script.bytesize > 10_000
  script = Bitcoin::Script.new(txout.pk_script) rescue nil
  if script
    if script.is_hash160? || script.is_pubkey?
      addrs << [i, script.get_hash160]
    elsif script.is_multisig?
      script.get_multisig_pubkeys.map do |pubkey|
        addrs << [i, Bitcoin.hash160(pubkey.unpack("H*")[0])]
      end
    elsif Bitcoin.namecoin? && script.is_namecoin?
      addrs << [i, script.get_hash160]
      names << [i, script]
    else
      log.debug { "Unknown script type"}# #{tx.hash}:#{txout_idx}" }
    end
    script_type = SCRIPT_TYPES.index(script.type)
  else
    log.error { "Error parsing script"}# #{tx.hash}:#{txout_idx}" }
    script_type = SCRIPT_TYPES.index(:unknown)
  end
  [script_type, addrs, names]
end

#persist_block(blk) ⇒ Object

persist given block blk to storage.



237
238
239
# File 'lib/bitcoin/storage/storage.rb', line 237

def persist_block(blk)
  raise "Not implemented"
end

#rescanObject



411
412
413
# File 'lib/bitcoin/storage/storage.rb', line 411

def rescan
  raise "Not implemented"
end

#resetObject

reset the store; delete all data



145
146
147
# File 'lib/bitcoin/storage/storage.rb', line 145

def reset
  raise "Not implemented"
end

#sqlite_pragmasObject

set pragma options for sqlite (if it is sqlite)



131
132
133
134
135
136
137
# File 'lib/bitcoin/storage/storage.rb', line 131

def sqlite_pragmas
  return  unless (@db.is_a?(Sequel::SQLite::Database) rescue false)
  @config[:sqlite_pragmas].each do |name, value|
    @db.pragma_set name, value
    log.debug { "set sqlite pragma #{name} to #{value}" }
  end
end

#store_addr(txout_id, hash160) ⇒ Object

store address hash160



371
372
373
374
375
376
# File 'lib/bitcoin/storage/storage.rb', line 371

def store_addr(txout_id, hash160)
  addr = @db[:addr][:hash160 => hash160]
  addr_id = addr[:id]  if addr
  addr_id ||= @db[:addr].insert({:hash160 => hash160})
  @db[:addr_txout].insert({:addr_id => addr_id, :txout_id => txout_id})
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.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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
# File 'lib/bitcoin/storage/storage.rb', line 162

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?
      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
      return persist_block(blk, MAIN, depth, prev_block.work)
    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})" }
      validator.validate(rules: [:context], raise_errors: true)  unless @config[:skip_validation]
      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}" }
      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



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

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

#update_block(hash, attrs) ⇒ Object

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



243
244
245
# File 'lib/bitcoin/storage/storage.rb', line 243

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