# See the file "LICENSE" for information about the copyright # and warranty status of this software. import array import itertools import os import struct import time from binascii import hexlify, unhexlify from bisect import bisect_right from collections import defaultdict, namedtuple from functools import partial import logging import plyvel from lib.coins import Bitcoin from lib.script import ScriptPubKey ADDR_TX_HASH_LEN=6 UTXO_TX_HASH_LEN=4 HIST_ENTRY_LEN=256*4 # Admits 65536 * HIST_ENTRY_LEN/4 entries UTXO = namedtuple("UTXO", "tx_num tx_pos tx_hash height value") def to_4_bytes(value): return struct.pack(' HIST_ENTRY_LEN: # must be big-endian (idx, ) = struct.unpack('>H', key[-2:]) for n in range(HIST_ENTRY_LEN, len(v), HIST_ENTRY_LEN): idx += 1 key = prefix + struct.pack('>H', idx) if idx % 500 == 0: addr = self.coin.P2PKH_address_from_hash160(hash160) self.logger.info('address {} hist moving to idx {:d}' .format(addr, idx)) self.db.put(key, v[n:n + HIST_ENTRY_LEN]) self.history = defaultdict(list) def get_hash160(self, tx_hash, idx, delete=True): key = b'h' + tx_hash[:ADDR_TX_HASH_LEN] + struct.pack(' self.flush_size: self.flush() def process_tx(self, tx_hash, tx): hash160s = set() if not tx.is_coinbase: for txin in tx.inputs: hash160s.add(self.spend_utxo(txin.prevout)) for idx, txout in enumerate(tx.outputs): hash160s.add(self.put_utxo(tx_hash, idx, txout)) for hash160 in hash160s: self.history[hash160].append(self.tx_count) self.tx_count += 1 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.db_height: tx_hashes = self.tx_hashes[height - self.db_height] 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{:05d}'.format(file_num) with self.open_file(filename) as f: f.seek(offset) tx_hash = f.read(32) return tx_hash, height @staticmethod def resolve_limit(limit): if limit is None: return -1 assert isinstance(limit, int) and limit >= 0 return limit def get_history(self, hash160, limit=1000): '''Generator that returns an unpruned, sorted list of (tx_hash, height) tuples of 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' + hash160 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, hash160): '''Returns the confirmed balance of an address.''' return sum(utxo.value for utxo in self.get_utxos(hash160, limit=None)) def get_utxos(self, hash160, 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' + hash160 utxos = [] for k, v in self.db.iterator(prefix=prefix): (tx_pos, ) = unpack('