import plyvel, ast, hashlib, traceback, os from processor import print_log from utils import * """ Patricia tree for hashing unspents """ DEBUG = 0 KEYLENGTH = 20 + 32 + 4 #56 class Storage(object): def __init__(self, config, shared, test_reorgs): self.dbpath = config.get('leveldb', 'path_fulltree') if not os.path.exists(self.dbpath): os.mkdir(self.dbpath) self.pruning_limit = config.getint('leveldb', 'pruning_limit') self.shared = shared self.hash_list = {} self.parents = {} self.test_reorgs = test_reorgs try: self.db_utxo = plyvel.DB(os.path.join(self.dbpath,'utxo'), create_if_missing=True, compression=None) self.db_addr = plyvel.DB(os.path.join(self.dbpath,'addr'), create_if_missing=True, compression=None) self.db_hist = plyvel.DB(os.path.join(self.dbpath,'hist'), create_if_missing=True, compression=None) self.db_undo = plyvel.DB(os.path.join(self.dbpath,'undo'), create_if_missing=True, compression=None) except: traceback.print_exc(file=sys.stdout) self.shared.stop() self.db_version = 2 # increase this when database needs to be updated try: self.last_hash, self.height, db_version = ast.literal_eval(self.db_undo.get('height')) print_log("Database version", self.db_version) print_log("Blockchain height", self.height) except: #traceback.print_exc(file=sys.stdout) print_log('initializing database') self.height = 0 self.last_hash = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' db_version = self.db_version # write root self.put_node('', {}) # check version if self.db_version != db_version: print_log("Your database '%s' is deprecated. Please create a new database"%self.dbpath) self.shared.stop() return # compute root hash d = self.get_node('') self.root_hash, v = self.get_node_hash('',d,None) print_log("UTXO tree root hash:", self.root_hash.encode('hex')) print_log("Coins in database:", v) # convert between bitcoin addresses and 20 bytes keys used for storage. def address_to_key(self, addr): return bc_address_to_hash_160(addr) def key_to_address(self, addr): return hash_160_to_bc_address(addr) def get_proof(self, addr): key = self.address_to_key(addr) i = self.db_utxo.iterator(start=key) k, _ = i.next() p = self.get_path(k) p.append(k) out = [] for item in p: v = self.db_utxo.get(item) out.append((item.encode('hex'), v.encode('hex'))) return out def get_balance(self, addr): key = self.address_to_key(addr) i = self.db_utxo.iterator(start=key) k, _ = i.next() if not k.startswith(key): return 0 p = self.get_parent(k) d = self.get_node(p) letter = k[len(p)] return d[letter][1] def listunspent(self, addr): key = self.address_to_key(addr) out = [] for k, v in self.db_utxo.iterator(start=key): if not k.startswith(key): break if len(k) == KEYLENGTH: txid = k[20:52].encode('hex') txpos = hex_to_int(k[52:56]) h = hex_to_int(v[8:12]) v = hex_to_int(v[0:8]) out.append({'tx_hash': txid, 'tx_pos':txpos, 'height': h, 'value':v}) out.sort(key=lambda x:x['height']) return out def get_history(self, addr): out = [] o = self.listunspent(addr) for item in o: out.append((item['tx_hash'], item['height'])) h = self.db_hist.get(addr) while h: item = h[0:80] h = h[80:] txi = item[0:32].encode('hex') hi = hex_to_int(item[36:40]) txo = item[40:72].encode('hex') ho = hex_to_int(item[76:80]) out.append((txi, hi)) out.append((txo, ho)) # sort out.sort(key=lambda x:x[1]) # uniqueness out = set(out) return map(lambda x: {'tx_hash':x[0], 'height':x[1]}, out) def get_address(self, txi): addr = self.db_addr.get(txi) return self.key_to_address(addr) if addr else None def get_undo_info(self, height): s = self.db_undo.get("undo_info_%d" % (height % 100)) if s is None: print_log("no undo info for ", height) return eval(s) def write_undo_info(self, height, bitcoind_height, undo_info): if height > bitcoind_height - 100 or self.test_reorgs: self.db_undo.put("undo_info_%d" % (height % 100), repr(undo_info)) def common_prefix(self, word1, word2): max_len = min(len(word1),len(word2)) for i in range(max_len): if word2[i] != word1[i]: index = i break else: index = max_len return word1[0:index] def put_node(self, key, d, batch=None): k = 0 serialized = '' for i in range(256): if chr(i) in d.keys(): k += 1< addr self.db_addr.delete(txi) # add to history s = self.db_hist.get(addr) if s is None: s = '' txo = (txid + int_to_hex(index,4) + int_to_hex(height,4)).decode('hex') s += txi + int_to_hex(in_height,4).decode('hex') + txo s = s[ -80*self.pruning_limit:] self.db_hist.put(addr, s) def revert_set_spent(self, addr, txi, undo): key = self.address_to_key(addr) leaf = key + txi # restore backlink self.db_addr.put(txi, key) v, height = undo.pop(leaf) self.add_address(leaf, v, height) # revert add to history s = self.db_hist.get(addr) # s might be empty if pruning limit was reached if not s: return assert s[-80:-44] == txi s = s[:-80] self.db_hist.put(addr, s) def import_transaction(self, txid, tx, block_height, touched_addr): undo = { 'prev_addr':[] } # contains the list of pruned items for each address in the tx; also, 'prev_addr' is a list of prev addresses prev_addr = [] for i, x in enumerate(tx.get('inputs')): txi = (x.get('prevout_hash') + int_to_hex(x.get('prevout_n'), 4)).decode('hex') addr = self.get_address(txi) if addr is not None: self.set_spent(addr, txi, txid, i, block_height, undo) touched_addr.add(addr) prev_addr.append(addr) undo['prev_addr'] = prev_addr # here I add only the outputs to history; maybe I want to add inputs too (that's in the other loop) for x in tx.get('outputs'): addr = x.get('address') if addr is None: continue self.add_to_history(addr, txid, x.get('index'), x.get('value'), block_height) touched_addr.add(addr) return undo def revert_transaction(self, txid, tx, block_height, touched_addr, undo): #print_log("revert tx", txid) for x in reversed(tx.get('outputs')): addr = x.get('address') if addr is None: continue self.revert_add_to_history(addr, txid, x.get('index'), x.get('value'), block_height) touched_addr.add(addr) prev_addr = undo.pop('prev_addr') for i, x in reversed(list(enumerate(tx.get('inputs')))): addr = prev_addr[i] if addr is not None: txi = (x.get('prevout_hash') + int_to_hex(x.get('prevout_n'), 4)).decode('hex') self.revert_set_spent(addr, txi, undo) touched_addr.add(addr) assert undo == {}