From 3ab07c1fb64942dad17e11303feeea4c66b6c773 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sun, 6 Nov 2016 14:23:18 +0900 Subject: [PATCH] Speed up script parsing for ~3% faster throughput Also improves the coin abstraction --- lib/coins.py | 45 +++++++++++++++++++++------- lib/script.py | 62 +++++++++++++-------------------------- lib/util.py | 9 ++---- server/block_processor.py | 11 +++---- 4 files changed, 60 insertions(+), 67 deletions(-) diff --git a/lib/coins.py b/lib/coins.py index a87b652..f87d8c4 100644 --- a/lib/coins.py +++ b/lib/coins.py @@ -17,9 +17,9 @@ import struct import sys from lib.hash import Base58, hash160, double_sha256, hash_to_str -from lib.script import ScriptPubKey +from lib.script import ScriptPubKey, Script from lib.tx import Deserializer -from lib.util import subclasses +from lib.util import cachedproperty, subclasses class CoinError(Exception): @@ -47,6 +47,19 @@ class Coin(object): raise CoinError('unknown coin {} and network {} combination' .format(name, net)) + @cachedproperty + def hash168_handlers(cls): + return ScriptPubKey.PayToHandlers( + address = cls.P2PKH_hash168_from_hash160, + script_hash = cls.P2SH_hash168_from_hash160, + pubkey = cls.P2PKH_hash168_from_pubkey, + unknown = lambda x : None, + ) + + @classmethod + def hash168_from_script(cls, script): + return ScriptPubKey.pay_to(script, cls.hash168_handlers) + @staticmethod def lookup_xverbytes(verbytes): '''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.''' @@ -75,11 +88,18 @@ class Coin(object): return Base58.encode_check(hash168) @classmethod - def P2PKH_address_from_hash160(cls, hash_bytes): + def P2PKH_hash168_from_hash160(cls, hash160): + assert len(hash160) == 20 + return bytes([cls.P2PKH_VERBYTE]) + hash160 + + @classmethod + def P2PKH_hash168_from_pubkey(cls, pubkey): + return cls.P2PKH_hash168_from_hash160(hash160(pubkey)) + + @classmethod + def P2PKH_address_from_hash160(cls, hash160): '''Return a P2PKH address given a public key.''' - assert len(hash_bytes) == 20 - payload = bytes([cls.P2PKH_VERBYTE]) + hash_bytes - return Base58.encode_check(payload) + return Base58.encode_check(cls.P2PKH_hash168_from_hash160(hash160)) @classmethod def P2PKH_address_from_pubkey(cls, pubkey): @@ -87,11 +107,14 @@ class Coin(object): return cls.P2PKH_address_from_hash160(hash160(pubkey)) @classmethod - def P2SH_address_from_hash160(cls, pubkey_bytes): - '''Return a coin address given a public key.''' - assert len(hash_bytes) == 20 - payload = bytes([cls.P2SH_VERBYTE]) + hash_bytes - return Base58.encode_check(payload) + def P2SH_hash168_from_hash160(cls, hash160): + assert len(hash160) == 20 + return bytes([cls.P2SH_VERBYTE]) + hash160 + + @classmethod + def P2SH_address_from_hash160(cls, hash160): + '''Return a coin address given a hash160.''' + return Base58.encode_check(cls.P2SH_hash168_from_hash160(hash160)) @classmethod def multisig_address(cls, m, pubkeys): diff --git a/lib/script.py b/lib/script.py index 3894892..7037f80 100644 --- a/lib/script.py +++ b/lib/script.py @@ -8,8 +8,8 @@ '''Script-related classes and functions.''' -from binascii import hexlify import struct +from collections import namedtuple from lib.enum import Enumeration from lib.hash import hash160 @@ -133,60 +133,38 @@ class ScriptSig(object): class ScriptPubKey(object): - '''A script from a tx output that gives conditions necessary for - spending.''' + '''A class for handling a tx output script that gives conditions + necessary for spending. + ''' - TO_ADDRESS, TO_P2SH, TO_PUBKEY, TO_UNKNOWN = range(4) TO_ADDRESS_OPS = [OpCodes.OP_DUP, OpCodes.OP_HASH160, -1, OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG] TO_P2SH_OPS = [OpCodes.OP_HASH160, -1, OpCodes.OP_EQUAL] TO_PUBKEY_OPS = [-1, OpCodes.OP_CHECKSIG] - def __init__(self, script, coin, kind, hash168, pubkey=None): - self.script = script - self.coin = coin - self.kind = kind - self.hash168 = hash168 - if pubkey: - self.pubkey = pubkey - - @cachedproperty - def address(self): - if self.kind == ScriptPubKey.TO_P2SH: - return self.coin.P2SH_address_from_hash160(self.hash168[1:]) - if self.hash160: - return self.coin.P2PKH_address_from_hash160(self.hash168[1:]) - return '' + PayToHandlers = namedtuple('PayToHandlers', + 'address script_hash pubkey unknown') @classmethod - def from_script(cls, script, coin): - '''Returns an instance of this class. Uncrecognised scripts return - an object of kind TO_UNKNOWN.''' - try: - return cls.parse_script(script, coin) - except ScriptError: - return cls(script, coin, cls.TO_UNKNOWN, None) + def pay_to(cls, script, handlers): + '''Parse a script, invoke the appropriate handler and + return the result. - @classmethod - def parse_script(cls, script, coin): - '''Returns an instance of this class. Raises on unrecognised - scripts.''' + One of the following handlers is invoked: + handlers.address(hash160) + handlers.script_hash(hash160) + handlers.pubkey(pubkey) + handlers.unknown(None) + ''' ops, datas = Script.get_ops(script) if Script.match_ops(ops, cls.TO_ADDRESS_OPS): - return cls(script, coin, cls.TO_ADDRESS, - bytes([coin.P2PKH_VERBYTE]) + datas[2]) - + return handlers.address(datas[2]) if Script.match_ops(ops, cls.TO_P2SH_OPS): - return cls(script, coin, cls.TO_P2SH, - bytes([coin.P2SH_VERBYTE]) + datas[1]) - + return handlers.script_hash(datas[1]) if Script.match_ops(ops, cls.TO_PUBKEY_OPS): - pubkey = datas[0] - return cls(script, coin, cls.TO_PUBKEY, - bytes([coin.P2PKH_VERBYTE]) + hash160(pubkey), pubkey) - - raise ScriptError('unknown script pubkey pattern') + return handlers.pubkey(datas[0]) + return handlers.unknown(None) @classmethod def P2SH_script(cls, hash160): @@ -317,4 +295,4 @@ class Script(object): print(name) else: print('{} {} ({:d} bytes)' - .format(name, hexlify(data).decode('ascii'), len(data))) + .format(name, data.hex(), len(data))) diff --git a/lib/util.py b/lib/util.py index dd8187e..a537737 100644 --- a/lib/util.py +++ b/lib/util.py @@ -31,16 +31,11 @@ class cachedproperty(object): self.f = f def __get__(self, obj, type): - if obj is None: - return self + obj = obj or type value = self.f(obj) - obj.__dict__[self.f.__name__] = value + setattr(obj, self.f.__name__, value) return value - def __set__(self, obj, value): - raise AttributeError('cannot set {} on {}' - .format(self.f.__name__, obj)) - def deep_getsizeof(obj): """Find the memory footprint of a Python object. diff --git a/server/block_processor.py b/server/block_processor.py index 74c0f27..8177da2 100644 --- a/server/block_processor.py +++ b/server/block_processor.py @@ -20,7 +20,6 @@ from functools import partial from server.cache import FSCache, UTXOCache, NO_CACHE_ENTRY from server.daemon import DaemonError from lib.hash import hash_to_str -from lib.script import ScriptPubKey from lib.tx import Deserializer from lib.util import chunks, LoggedClass from server.storage import open_db @@ -205,12 +204,11 @@ class MemPool(LoggedClass): # The mempool is unordered, so process all outputs first so # that looking for inputs has full info. - parse_script = ScriptPubKey.from_script - coin = self.bp.coin + script_hash168 = self.bp.coin.hash168_from_script utxo_lookup = self.bp.utxo_cache.lookup def txout_pair(txout): - return (parse_script(txout.pk_script, coin).hash168, txout.value) + return (script_hash168(txout.pk_script), txout.value) for hex_hash, tx in new_txs.items(): txout_pairs = tuple(txout_pair(txout) for txout in tx.outputs) @@ -745,8 +743,7 @@ class BlockProcessor(LoggedClass): # Use local vars for speed in the loops history = self.history tx_num = self.tx_count - coin = self.coin - parse_script = ScriptPubKey.from_script + script_hash168 = self.coin.hash168_from_script pack = struct.pack for tx, tx_hash in zip(txs, tx_hashes): @@ -763,7 +760,7 @@ class BlockProcessor(LoggedClass): # Add the new UTXOs for idx, txout in enumerate(tx.outputs): # Get the hash168. Ignore scripts we can't grok. - hash168 = parse_script(txout.pk_script, coin).hash168 + hash168 = script_hash168(txout.pk_script) if hash168: hash168s.add(hash168) put_utxo(tx_hash + pack('