# Copyright (c) 2016, Neil Booth # # All rights reserved. # # See the file "LICENCE" for information about the copyright # and warranty status of this software. '''Interface to the blockchain database.''' import array import ast import os import struct from bisect import bisect_right from collections import namedtuple from lib.util import chunks, LoggedClass from lib.hash import double_sha256 from server.storage import open_db UTXO = namedtuple("UTXO", "tx_num tx_pos tx_hash height value") class DB(LoggedClass): '''Simple wrapper of the backend database for querying. Performs no DB update, though the DB will be cleaned on opening if it was shutdown uncleanly. ''' class DBError(Exception): pass def __init__(self, env): super().__init__() self.env = env self.coin = env.coin self.logger.info('switching current directory to {}' .format(env.db_dir)) os.chdir(env.db_dir) # Open DB and metadata files. Record some of its state. db_name = '{}-{}'.format(self.coin.NAME, self.coin.NET) self.db = open_db(db_name, env.db_engine) if self.db.is_new: self.logger.info('created new {} database {}' .format(env.db_engine, db_name)) else: self.logger.info('successfully opened {} database {}' .format(env.db_engine, db_name)) self.init_state_from_db() self.tx_count = self.db_tx_count self.height = self.db_height self.tip = self.db_tip # -- FS related members -- self.tx_hash_file_size = 16 * 1024 * 1024 # On-disk height updated by a flush self.fs_height = self.height # Unflushed items self.headers = [] self.tx_hashes = [] create = self.height == -1 self.headers_file = self.open_file('headers', create) self.txcount_file = self.open_file('txcount', create) # tx_counts[N] has the cumulative number of txs at the end of # height N. So tx_counts[0] is 1 - the genesis coinbase self.tx_counts = array.array('I') self.txcount_file.seek(0) self.tx_counts.fromfile(self.txcount_file, self.height + 1) if self.tx_counts: assert self.tx_count == self.tx_counts[-1] else: assert self.tx_count == 0 def init_state_from_db(self): if self.db.is_new: self.db_height = -1 self.db_tx_count = 0 self.db_tip = b'\0' * 32 self.flush_count = 0 self.utxo_flush_count = 0 self.wall_time = 0 self.first_sync = True else: state = self.db.get(b'state') state = ast.literal_eval(state.decode()) if state['genesis'] != self.coin.GENESIS_HASH: raise self.DBError('DB genesis hash {} does not match coin {}' .format(state['genesis_hash'], self.coin.GENESIS_HASH)) self.db_height = state['height'] self.db_tx_count = state['tx_count'] self.db_tip = state['tip'] self.flush_count = state['flush_count'] self.utxo_flush_count = state['utxo_flush_count'] self.wall_time = state['wall_time'] self.first_sync = state.get('first_sync', True) def open_file(self, filename, create=False): '''Open the file name. Return its handle.''' try: return open(filename, 'rb+') except FileNotFoundError: if create: return open(filename, 'wb+') raise def read_headers(self, start, count): result = b'' # Read some from disk disk_count = min(count, self.fs_height + 1 - start) if disk_count > 0: header_len = self.coin.HEADER_LEN assert start >= 0 self.headers_file.seek(start * header_len) result = self.headers_file.read(disk_count * header_len) count -= disk_count start += disk_count # The rest from memory start -= self.fs_height + 1 assert count >= 0 and start + count <= len(self.headers) result += b''.join(self.headers[start: start + count]) return result def get_tx_hash(self, tx_num): '''Returns the tx_hash and height of a tx number.''' height = bisect_right(self.tx_counts, tx_num) # Is this on disk or unflushed? if height > self.fs_height: tx_hashes = self.tx_hashes[height - (self.fs_height + 1)] tx_hash = tx_hashes[tx_num - self.tx_counts[height - 1]] else: file_pos = tx_num * 32 file_num, offset = divmod(file_pos, self.tx_hash_file_size) filename = 'hashes{:04d}'.format(file_num) with self.open_file(filename) as f: f.seek(offset) tx_hash = f.read(32) return tx_hash, height def block_hashes(self, height, count): headers = self.read_headers(height, count) # FIXME: move to coins.py hlen = self.coin.HEADER_LEN return [double_sha256(header) for header in chunks(headers, hlen)] @staticmethod def _resolve_limit(limit): if limit is None: return -1 assert isinstance(limit, int) and limit >= 0 return limit def get_history(self, hash168, limit=1000): '''Generator that returns an unpruned, sorted list of (tx_hash, height) tuples of confirmed transactions that touched the address, earliest in the blockchain first. Includes both spending and receiving transactions. By default yields at most 1000 entries. Set limit to None to get them all. ''' limit = self._resolve_limit(limit) prefix = b'H' + hash168 for key, hist in self.db.iterator(prefix=prefix): a = array.array('I') a.frombytes(hist) for tx_num in a: if limit == 0: return yield self.get_tx_hash(tx_num) limit -= 1 def get_balance(self, hash168): '''Returns the confirmed balance of an address.''' return sum(utxo.value for utxo in self.get_utxos(hash168, limit=None)) def get_utxos(self, hash168, limit=1000): '''Generator that yields all UTXOs for an address sorted in no particular order. By default yields at most 1000 entries. Set limit to None to get them all. ''' limit = self._resolve_limit(limit) unpack = struct.unpack prefix = b'u' + hash168 for k, v in self.db.iterator(prefix=prefix): (tx_pos,) = unpack('