Class: Bitcoin::Storage::Backends::StoreBase
- Inherits:
-
Object
- Object
- Bitcoin::Storage::Backends::StoreBase
- 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
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
-
#config ⇒ Object
Returns the value of attribute config.
-
#log ⇒ Object
readonly
Returns the value of attribute log.
Instance Method Summary collapse
- #add_watched_address(address) ⇒ Object
-
#backend_name ⇒ Object
name of the storage backend currently in use (“sequel” or “utxo”).
-
#check_metadata ⇒ Object
check that database network magic and backend match the ones we are using.
-
#connect ⇒ Object
connect to database.
-
#get_balance(hash160, unconfirmed = false) ⇒ Object
get balance for given
hash160. -
#get_block(blk_hash) ⇒ Object
get block with given
blk_hash. -
#get_block_by_depth(depth) ⇒ Object
get block with given
depthfrom main chain. -
#get_block_by_id(block_id) ⇒ Object
get block by given
block_id. -
#get_block_by_prev_hash(prev_hash) ⇒ Object
get block with given
prev_hash. -
#get_block_by_tx(tx_hash) ⇒ Object
get block that includes tx with given
tx_hash. -
#get_depth ⇒ Object
return depth of the head block.
-
#get_head ⇒ Object
get the hash of the leading block.
-
#get_idx_from_tx_hash(tx_hash) ⇒ Object
Grab the position of a tx in a given block.
-
#get_locator(pointer = get_head) ⇒ Object
compute blockchain locator.
-
#get_tx(tx_hash) ⇒ Object
get tx with given
tx_hash. -
#get_tx_by_id(tx_id) ⇒ Object
get tx with given
tx_id. -
#get_txin_for_txout(tx_hash, txout_idx) ⇒ Object
get corresponding txin for the txout in transaction
tx_hashwith indextxout_idx. -
#get_txouts_for_address(address, unconfirmed = false) ⇒ Object
collect all txouts containing a standard tx to given
address. -
#get_txouts_for_pk_script(script) ⇒ Object
collect all txouts containing the given
script. -
#has_block(blk_hash) ⇒ Object
check if block with given
blk_hashis already stored. -
#has_tx(tx_hash) ⇒ Object
check if tx with given
tx_hashis already stored. -
#import(filename, max_depth = nil) ⇒ Object
import satoshi bitcoind blk0001.dat blockchain file.
- #in_sync? ⇒ Boolean
- #init_sequel_store ⇒ Object
-
#initialize(config = {}, getblocks_callback = nil) ⇒ StoreBase
constructor
A new instance of StoreBase.
-
#migrate ⇒ Object
check if schema is up to date and migrate to current version if necessary.
-
#new_block(blk) ⇒ Object
handle a new block incoming from the network.
- #new_tx(tx) ⇒ Object
-
#parse_script(txout, i) ⇒ Object
parse script and collect address/txout mappings to index.
-
#persist_block(blk) ⇒ Object
persist given block
blkto storage. - #rescan ⇒ Object
-
#reset ⇒ Object
reset the store; delete all data.
-
#sqlite_pragmas ⇒ Object
set pragma options for sqlite (if it is sqlite).
-
#store_addr(txout_id, hash160) ⇒ Object
store address
hash160. -
#store_block(blk) ⇒ Object
store given block
blk. -
#store_tx(tx, validate = true) ⇒ Object
store given
tx. -
#update_block(hash, attrs) ⇒ Object
update
attrsfor block with givenhash.
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
#config ⇒ Object
Returns the value of attribute config.
66 67 68 |
# File 'lib/bitcoin/storage/storage.rb', line 66 def config @config end |
#log ⇒ Object (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_name ⇒ Object
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_metadata ⇒ Object
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 |
#connect ⇒ Object
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_depth ⇒ Object
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_head ⇒ Object
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
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_store ⇒ Object
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 |
#migrate ⇒ Object
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 |
#rescan ⇒ Object
411 412 413 |
# File 'lib/bitcoin/storage/storage.rb', line 411 def rescan raise "Not implemented" end |
#reset ⇒ Object
reset the store; delete all data
145 146 147 |
# File 'lib/bitcoin/storage/storage.rb', line 145 def reset raise "Not implemented" end |
#sqlite_pragmas ⇒ Object
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 |