def summaries(self): # Return lists of (tx_hash, fee, has_unconfirmed_inputs) by hashX summaries = defaultdict(list) utxos = self.mempool_utxos() for tx_hash, tx in self.txs.items(): fee = 0 hashXs = set() has_ui = False for n, input in enumerate(tx.inputs): if is_gen_outpoint(input.prev_hash, input.prev_idx): continue has_ui = has_ui or (input.prev_hash in self.txs) prevout = (input.prev_hash, input.prev_idx) if prevout in utxos: hashX, value = utxos[prevout] else: hashX, value = self.db_utxos[prevout] hashXs.add(hashX) fee += value for output in tx.outputs: hashXs.add(coin.hashX_from_script(output.pk_script)) fee -= output.value summary = (tx_hash, fee, has_ui) for hashX in hashXs: summaries[hashX].append(summary) return summaries
async def test_notifications(): # Tests notifications over a cycle of: # 1) A first batch of txs come in # 2) A second batch of txs come in # 3) A block comes in confirming the first batch only api = API() api.initialize() mempool = MemPool(coin, api, refresh_secs=0.001, log_status_secs=0) event = Event() n = len(api.ordered_adds) // 2 raw_txs = api.raw_txs.copy() txs = api.txs.copy() first_hashes = api.ordered_adds[:n] first_touched = api.touched(first_hashes) second_hashes = api.ordered_adds[n:] second_touched = api.touched(second_hashes) async with TaskGroup() as group: # First batch enters the mempool api.raw_txs = {hash: raw_txs[hash] for hash in first_hashes} api.txs = {hash: txs[hash] for hash in first_hashes} first_utxos = api.mempool_utxos() first_spends = api.mempool_spends() await group.spawn(mempool.keep_synchronized, event) await event.wait() assert len(api.on_mempool_calls) == 1 touched, height = api.on_mempool_calls[0] assert height == api._height == api._cached_height assert touched == first_touched # Second batch enters the mempool api.raw_txs = raw_txs api.txs = txs await event.wait() assert len(api.on_mempool_calls) == 2 touched, height = api.on_mempool_calls[1] assert height == api._height == api._cached_height # Touched is incremental assert touched == second_touched # Block found; first half confirm new_height = 2 api._height = new_height api.db_utxos.update(first_utxos) for spend in first_spends: if is_gen_outpoint(*spend): continue del api.db_utxos[spend] api.raw_txs = {hash: raw_txs[hash] for hash in second_hashes} api.txs = {hash: txs[hash] for hash in second_hashes} await event.wait() assert len(api.on_mempool_calls) == 3 touched, height = api.on_mempool_calls[2] assert height == api._height == api._cached_height == new_height assert touched == first_touched await group.cancel_remaining()
def spends(self): # Return spends indexed by hashX spends = defaultdict(list) utxos = self.mempool_utxos() for tx_hash, tx in self.txs.items(): for n, input in enumerate(tx.inputs): prevout = (input.prev_hash, input.prev_idx) if is_gen_outpoint(input.prev_hash, input.prev_idx): continue if prevout in utxos: hashX, value = utxos.pop(prevout) else: hashX, value = self.db_utxos[prevout] spends[hashX].append(prevout) return spends
def advance_txs(self, txs): self.tx_hashes.append(b''.join(tx_hash for tx, tx_hash in txs)) # Use local vars for speed in the loops undo_info = [] tx_num = self.tx_count script_hashX = self.coin.hashX_from_script s_pack = pack put_utxo = self.utxo_cache.__setitem__ spend_utxo = self.spend_utxo undo_info_append = undo_info.append update_touched = self.touched.update hashXs_by_tx = [] append_hashXs = hashXs_by_tx.append for tx, tx_hash in txs: hashXs = [] append_hashX = hashXs.append tx_numb = s_pack('<I', tx_num) # Spend the inputs for txin in tx.inputs: if is_gen_outpoint(txin.prev_hash, txin.prev_idx): continue cache_value = spend_utxo(txin.prev_hash, txin.prev_idx) undo_info_append(cache_value) append_hashX(cache_value[:-12]) # Add the new UTXOs for idx, txout in enumerate(tx.outputs): # Get the hashX. Ignore unspendable outputs hashX = script_hashX(txout.pk_script) if hashX: append_hashX(hashX) put_utxo(tx_hash + s_pack('<H', idx), hashX + tx_numb + s_pack('<Q', txout.value)) append_hashXs(hashXs) update_touched(hashXs) tx_num += 1 self.db.history.add_unflushed(hashXs_by_tx, self.tx_count) self.tx_count = tx_num self.db.tx_counts.append(tx_num) return undo_info
def _accept_transactions(self, tx_map, utxo_map, touched): '''Accept transactions in tx_map to the mempool if all their inputs can be found in the existing mempool or a utxo_map from the DB. Returns an (unprocessed tx_map, unspent utxo_map) pair. ''' hashXs = self.hashXs txs = self.txs deferred = {} unspent = set(utxo_map) # Try to find all prevouts so we can accept the TX for hash, tx in tx_map.items(): in_pairs = [] try: for prevout in tx.prevouts: # Skip generation like prevouts if is_gen_outpoint(*prevout): continue utxo = utxo_map.get(prevout) if not utxo: prev_hash, prev_index = prevout # Raises KeyError if prev_hash is not in txs utxo = txs[prev_hash].out_pairs[prev_index] in_pairs.append(utxo) except KeyError: deferred[hash] = tx continue # Spend the prevouts unspent.difference_update(tx.prevouts) # Save the in_pairs, compute the fee and accept the TX tx.in_pairs = tuple(in_pairs) # Avoid negative fees if dealing with generation-like transactions # because some in_parts would be missing tx.fee = max(0, (sum(v for _, v in tx.in_pairs) - sum(v for _, v in tx.out_pairs))) txs[hash] = tx for hashX, value in itertools.chain(tx.in_pairs, tx.out_pairs): touched.add(hashX) hashXs[hashX].add(hash) return deferred, {prevout: utxo_map[prevout] for prevout in unspent}
def balance_deltas(self): # Return mempool balance deltas indexed by hashX deltas = defaultdict(int) utxos = self.mempool_utxos() for tx_hash, tx in self.txs.items(): for n, input in enumerate(tx.inputs): prevout = (input.prev_hash, input.prev_idx) if is_gen_outpoint(input.prev_hash, input.prev_idx): continue if prevout in utxos: utxos.pop(prevout) else: hashX, value = self.db_utxos[prevout] deltas[hashX] -= value for hashX, value in utxos.values(): deltas[hashX] += value return deltas
def touched(self, tx_hashes): touched = set() utxos = self.mempool_utxos() for tx_hash in tx_hashes: tx = self.txs[tx_hash] for n, input in enumerate(tx.inputs): if is_gen_outpoint(input.prev_hash, input.prev_idx): continue prevout = (input.prev_hash, input.prev_idx) if prevout in utxos: hashX, value = utxos[prevout] else: hashX, value = self.db_utxos[prevout] touched.add(hashX) for output in tx.outputs: touched.add(coin.hashX_from_script(output.pk_script)) return touched
async def _fetch_and_accept(self, hashes, all_hashes, touched): '''Fetch a list of mempool transactions.''' hex_hashes_iter = (hash_to_hex_str(hash) for hash in hashes) raw_txs = await self.api.raw_transactions(hex_hashes_iter) def deserialize_txs(): # This function is pure to_hashX = self.coin.hashX_from_script deserializer = self.coin.DESERIALIZER txs = {} for hash, raw_tx in zip(hashes, raw_txs): # The daemon may have evicted the tx from its # mempool or it may have gotten in a block if not raw_tx: continue tx, tx_size = deserializer(raw_tx).read_tx_and_vsize() # Convert the inputs and outputs into (hashX, value) pairs txin_pairs = tuple((txin.prev_hash, txin.prev_idx) for txin in tx.inputs) txout_pairs = tuple((to_hashX(txout.pk_script), txout.value) for txout in tx.outputs) txs[hash] = MemPoolTx(txin_pairs, None, txout_pairs, 0, tx_size) return txs # Thread this potentially slow operation so as not to block tx_map = await run_in_thread(deserialize_txs) # Determine all prevouts not in the mempool, and fetch the # UTXO information from the database. Failed prevout lookups # return None - concurrent database updates happen - which is # relied upon by _accept_transactions. Ignore prevouts that are # generation-like. prevouts = tuple(prevout for tx in tx_map.values() for prevout in tx.prevouts if (prevout[0] not in all_hashes and not is_gen_outpoint(*prevout))) utxos = await self.api.lookup_utxos(prevouts) utxo_map = {prevout: utxo for prevout, utxo in zip(prevouts, utxos)} return self._accept_transactions(tx_map, utxo_map, touched)
def backup_txs(self, txs): # Prevout values, in order down the block (coinbase first if present) # undo_info is in reverse block order undo_info = self.db.read_undo_info(self.height) if undo_info is None: raise ChainError( 'no undo information found for height {:,d}'.format( self.height)) n = len(undo_info) # Use local vars for speed in the loops s_pack = pack put_utxo = self.utxo_cache.__setitem__ spend_utxo = self.spend_utxo script_hashX = self.coin.hashX_from_script touched = self.touched undo_entry_len = 12 + HASHX_LEN for tx, tx_hash in reversed(txs): for idx, txout in enumerate(tx.outputs): # Spend the TX outputs. Be careful with unspendable # outputs - we didn't save those in the first place. hashX = script_hashX(txout.pk_script) if hashX: cache_value = spend_utxo(tx_hash, idx) touched.add(cache_value[:-12]) # Restore the inputs for txin in reversed(tx.inputs): if is_gen_outpoint(txin.prev_hash, txin.prev_idx): continue n -= undo_entry_len undo_item = undo_info[n:n + undo_entry_len] put_utxo(txin.prev_hash + s_pack('<H', txin.prev_idx), undo_item) touched.add(undo_item[:-12]) assert n == 0 self.tx_count -= len(txs)