Neil Booth
8 years ago
4 changed files with 354 additions and 223 deletions
@ -0,0 +1,287 @@ |
|||||
|
# Copyright (c) 2016, Neil Booth |
||||
|
# |
||||
|
# All rights reserved. |
||||
|
# |
||||
|
# See the file "LICENCE" for information about the copyright |
||||
|
# and warranty status of this software. |
||||
|
|
||||
|
'''Mempool handling.''' |
||||
|
|
||||
|
import asyncio |
||||
|
import itertools |
||||
|
import time |
||||
|
from collections import defaultdict |
||||
|
from functools import partial |
||||
|
|
||||
|
from lib.hash import hash_to_str, hex_str_to_hash |
||||
|
from lib.tx import Deserializer |
||||
|
import lib.util as util |
||||
|
from server.daemon import DaemonError |
||||
|
|
||||
|
|
||||
|
class MemPool(util.LoggedClass): |
||||
|
'''Representation of the daemon's mempool. |
||||
|
|
||||
|
Updated regularly in caught-up state. Goal is to enable efficient |
||||
|
response to the value() and transactions() calls. |
||||
|
|
||||
|
To that end we maintain the following maps: |
||||
|
|
||||
|
tx_hash -> (txin_pairs, txout_pairs) |
||||
|
hash168 -> set of all tx hashes in which the hash168 appears |
||||
|
|
||||
|
A pair is a (hash168, value) tuple. tx hashes are hex strings. |
||||
|
''' |
||||
|
|
||||
|
def __init__(self, daemon, coin, db, touched, touched_event): |
||||
|
super().__init__() |
||||
|
self.daemon = daemon |
||||
|
self.coin = coin |
||||
|
self.db = db |
||||
|
self.touched = touched |
||||
|
self.touched_event = touched_event |
||||
|
self.stop = False |
||||
|
self.txs = {} |
||||
|
self.hash168s = defaultdict(set) # None can be a key |
||||
|
|
||||
|
async def main_loop(self, caught_up): |
||||
|
'''Asynchronously maintain mempool status with daemon. |
||||
|
|
||||
|
Waits until the caught up event is signalled.''' |
||||
|
await caught_up.wait() |
||||
|
self.logger.info('beginning processing of daemon mempool. ' |
||||
|
'This can take some time...') |
||||
|
try: |
||||
|
await self.fetch_and_process() |
||||
|
except asyncio.CancelledError: |
||||
|
# This aids clean shutdowns |
||||
|
self.stop = True |
||||
|
|
||||
|
async def fetch_and_process(self): |
||||
|
'''The inner loop unprotected by try / except.''' |
||||
|
unfetched = set() |
||||
|
unprocessed = {} |
||||
|
log_every = 150 |
||||
|
log_secs = 0 |
||||
|
fetch_size = 400 |
||||
|
process_some = self.async_process_some(unfetched, fetch_size // 2) |
||||
|
next_refresh = 0 |
||||
|
# The list of mempool hashes is fetched no more frequently |
||||
|
# than this number of seconds |
||||
|
refresh_secs = 5 |
||||
|
|
||||
|
while True: |
||||
|
try: |
||||
|
now = time.time() |
||||
|
if now >= next_refresh: |
||||
|
await self.new_hashes(unprocessed, unfetched) |
||||
|
next_refresh = now + refresh_secs |
||||
|
log_secs -= refresh_secs |
||||
|
|
||||
|
# Fetch some txs if unfetched ones remain |
||||
|
if unfetched: |
||||
|
count = min(len(unfetched), fetch_size) |
||||
|
hex_hashes = [unfetched.pop() for n in range(count)] |
||||
|
unprocessed.update(await self.fetch_raw_txs(hex_hashes)) |
||||
|
|
||||
|
# Process some txs if unprocessed ones remain |
||||
|
if unprocessed: |
||||
|
await process_some(unprocessed) |
||||
|
|
||||
|
if self.touched: |
||||
|
self.touched_event.set() |
||||
|
|
||||
|
if log_secs <= 0 and not unprocessed: |
||||
|
log_secs = log_every |
||||
|
self.logger.info('{:,d} txs touching {:,d} addresses' |
||||
|
.format(len(self.txs), |
||||
|
len(self.hash168s))) |
||||
|
await asyncio.sleep(1) |
||||
|
except DaemonError as e: |
||||
|
self.logger.info('ignoring daemon error: {}'.format(e)) |
||||
|
|
||||
|
async def new_hashes(self, unprocessed, unfetched): |
||||
|
'''Get the current list of hashes in the daemon's mempool. |
||||
|
|
||||
|
Remove ones that have disappeared from self.txs and unprocessed. |
||||
|
''' |
||||
|
txs = self.txs |
||||
|
hash168s = self.hash168s |
||||
|
touched = self.touched |
||||
|
|
||||
|
hashes = set(await self.daemon.mempool_hashes()) |
||||
|
new = hashes.difference(txs) |
||||
|
gone = set(txs).difference(hashes) |
||||
|
for hex_hash in gone: |
||||
|
unprocessed.pop(hex_hash, None) |
||||
|
item = txs.pop(hex_hash) |
||||
|
if item: |
||||
|
txin_pairs, txout_pairs = item |
||||
|
tx_hash168s = set(hash168 for hash168, value in txin_pairs) |
||||
|
tx_hash168s.update(hash168 for hash168, value in txout_pairs) |
||||
|
for hash168 in tx_hash168s: |
||||
|
hash168s[hash168].remove(hex_hash) |
||||
|
if not hash168s[hash168]: |
||||
|
del hash168s[hash168] |
||||
|
touched.update(tx_hash168s) |
||||
|
|
||||
|
unfetched.update(new) |
||||
|
for hex_hash in new: |
||||
|
txs[hex_hash] = None |
||||
|
|
||||
|
def async_process_some(self, unfetched, limit): |
||||
|
loop = asyncio.get_event_loop() |
||||
|
pending = [] |
||||
|
txs = self.txs |
||||
|
|
||||
|
async def process(unprocessed): |
||||
|
nonlocal pending |
||||
|
|
||||
|
raw_txs = {} |
||||
|
while unprocessed and len(raw_txs) < limit: |
||||
|
hex_hash, raw_tx = unprocessed.popitem() |
||||
|
raw_txs[hex_hash] = raw_tx |
||||
|
|
||||
|
if unprocessed: |
||||
|
deferred = [] |
||||
|
else: |
||||
|
deferred = pending |
||||
|
pending = [] |
||||
|
|
||||
|
process_raw_txs = partial(self.process_raw_txs, raw_txs, deferred) |
||||
|
result, deferred = ( |
||||
|
await loop.run_in_executor(None, process_raw_txs)) |
||||
|
|
||||
|
pending.extend(deferred) |
||||
|
hash168s = self.hash168s |
||||
|
touched = self.touched |
||||
|
for hex_hash, in_out_pairs in result.items(): |
||||
|
if hex_hash in txs: |
||||
|
txs[hex_hash] = in_out_pairs |
||||
|
for hash168, value in itertools.chain(*in_out_pairs): |
||||
|
touched.add(hash168) |
||||
|
hash168s[hash168].add(hex_hash) |
||||
|
|
||||
|
to_do = len(unfetched) + len(unprocessed) |
||||
|
if to_do: |
||||
|
percent = (len(txs) - to_do) * 100 // len(txs) |
||||
|
self.logger.info('catchup {:d}% complete'.format(percent)) |
||||
|
|
||||
|
return process |
||||
|
|
||||
|
async def fetch_raw_txs(self, hex_hashes): |
||||
|
'''Fetch a list of mempool transactions.''' |
||||
|
raw_txs = await self.daemon.getrawtransactions(hex_hashes) |
||||
|
|
||||
|
# Skip hashes the daemon has dropped. Either they were |
||||
|
# evicted or they got in a block. |
||||
|
return {hh:raw for hh, raw in zip(hex_hashes, raw_txs) if raw} |
||||
|
|
||||
|
def process_raw_txs(self, raw_tx_map, pending): |
||||
|
'''Process the dictionary of raw transactions and return a dictionary |
||||
|
of updates to apply to self.txs. |
||||
|
|
||||
|
This runs in the executor so should not update any member |
||||
|
variables it doesn't own. Atomic reads of self.txs that do |
||||
|
not depend on the result remaining the same are fine. |
||||
|
''' |
||||
|
script_hash168 = self.coin.hash168_from_script() |
||||
|
db_utxo_lookup = self.db.db_utxo_lookup |
||||
|
txs = self.txs |
||||
|
|
||||
|
# Deserialize each tx and put it in our priority queue |
||||
|
for tx_hash, raw_tx in raw_tx_map.items(): |
||||
|
if not tx_hash in txs: |
||||
|
continue |
||||
|
tx = Deserializer(raw_tx).read_tx() |
||||
|
|
||||
|
# Convert the tx outputs into (hash168, value) pairs |
||||
|
txout_pairs = [(script_hash168(txout.pk_script), txout.value) |
||||
|
for txout in tx.outputs] |
||||
|
|
||||
|
# Convert the tx inputs to ([prev_hex_hash, prev_idx) pairs |
||||
|
txin_pairs = [(hash_to_str(txin.prev_hash), txin.prev_idx) |
||||
|
for txin in tx.inputs] |
||||
|
|
||||
|
pending.append((tx_hash, txin_pairs, txout_pairs)) |
||||
|
|
||||
|
# Now process what we can |
||||
|
result = {} |
||||
|
deferred = [] |
||||
|
|
||||
|
for item in pending: |
||||
|
if self.stop: |
||||
|
break |
||||
|
|
||||
|
tx_hash, old_txin_pairs, txout_pairs = item |
||||
|
if tx_hash not in txs: |
||||
|
continue |
||||
|
|
||||
|
mempool_missing = False |
||||
|
txin_pairs = [] |
||||
|
|
||||
|
try: |
||||
|
for prev_hex_hash, prev_idx in old_txin_pairs: |
||||
|
tx_info = txs.get(prev_hex_hash, 0) |
||||
|
if tx_info is None: |
||||
|
tx_info = result.get(prev_hex_hash) |
||||
|
if not tx_info: |
||||
|
mempool_missing = True |
||||
|
continue |
||||
|
if tx_info: |
||||
|
txin_pairs.append(tx_info[1][prev_idx]) |
||||
|
elif not mempool_missing: |
||||
|
prev_hash = hex_str_to_hash(prev_hex_hash) |
||||
|
txin_pairs.append(db_utxo_lookup(prev_hash, prev_idx)) |
||||
|
except self.db.MissingUTXOError: |
||||
|
# This typically happens just after the daemon has |
||||
|
# accepted a new block and the new mempool has deps on |
||||
|
# new txs in that block. |
||||
|
continue |
||||
|
|
||||
|
if mempool_missing: |
||||
|
deferred.append(item) |
||||
|
else: |
||||
|
result[tx_hash] = (txin_pairs, txout_pairs) |
||||
|
|
||||
|
return result, deferred |
||||
|
|
||||
|
async def transactions(self, hash168): |
||||
|
'''Generate (hex_hash, tx_fee, unconfirmed) tuples for mempool |
||||
|
entries for the hash168. |
||||
|
|
||||
|
unconfirmed is True if any txin is unconfirmed. |
||||
|
''' |
||||
|
# hash168s is a defaultdict |
||||
|
if not hash168 in self.hash168s: |
||||
|
return [] |
||||
|
|
||||
|
hex_hashes = self.hash168s[hash168] |
||||
|
raw_txs = self.bp.daemon.getrawtransactions(hex_hashes) |
||||
|
result = [] |
||||
|
for hex_hash, raw_tx in zip(hex_hashes, raw_txs): |
||||
|
item = self.txs.get(hex_hash) |
||||
|
if not item or not raw_tx: |
||||
|
continue |
||||
|
tx = Deserializer(raw_tx).read_tx() |
||||
|
txin_pairs, txout_pairs = item |
||||
|
tx_fee = (sum(v for hash168, v in txin_pairs) |
||||
|
- sum(v for hash168, v in txout_pairs)) |
||||
|
unconfirmed = any(txin.prev_hash not in self.txs |
||||
|
for txin in tx.inputs) |
||||
|
result.append((hex_hash, tx_fee, unconfirmed)) |
||||
|
return result |
||||
|
|
||||
|
def value(self, hash168): |
||||
|
'''Return the unconfirmed amount in the mempool for hash168. |
||||
|
|
||||
|
Can be positive or negative. |
||||
|
''' |
||||
|
value = 0 |
||||
|
# hash168s is a defaultdict |
||||
|
if hash168 in self.hash168s: |
||||
|
for hex_hash in self.hash168s[hash168]: |
||||
|
txin_pairs, txout_pairs = self.txs[hex_hash] |
||||
|
value -= sum(v for h168, v in txin_pairs if h168 == hash168) |
||||
|
value += sum(v for h168, v in txout_pairs if h168 == hash168) |
||||
|
return value |
Loading…
Reference in new issue