Browse Source

Speed up script parsing for ~3% faster throughput

Also improves the coin abstraction
master
Neil Booth 8 years ago
parent
commit
3ab07c1fb6
  1. 45
      lib/coins.py
  2. 64
      lib/script.py
  3. 9
      lib/util.py
  4. 11
      server/block_processor.py

45
lib/coins.py

@ -17,9 +17,9 @@ import struct
import sys import sys
from lib.hash import Base58, hash160, double_sha256, hash_to_str 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.tx import Deserializer
from lib.util import subclasses from lib.util import cachedproperty, subclasses
class CoinError(Exception): class CoinError(Exception):
@ -47,6 +47,19 @@ class Coin(object):
raise CoinError('unknown coin {} and network {} combination' raise CoinError('unknown coin {} and network {} combination'
.format(name, net)) .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 @staticmethod
def lookup_xverbytes(verbytes): def lookup_xverbytes(verbytes):
'''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.''' '''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.'''
@ -75,11 +88,18 @@ class Coin(object):
return Base58.encode_check(hash168) return Base58.encode_check(hash168)
@classmethod @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.''' '''Return a P2PKH address given a public key.'''
assert len(hash_bytes) == 20 return Base58.encode_check(cls.P2PKH_hash168_from_hash160(hash160))
payload = bytes([cls.P2PKH_VERBYTE]) + hash_bytes
return Base58.encode_check(payload)
@classmethod @classmethod
def P2PKH_address_from_pubkey(cls, pubkey): def P2PKH_address_from_pubkey(cls, pubkey):
@ -87,11 +107,14 @@ class Coin(object):
return cls.P2PKH_address_from_hash160(hash160(pubkey)) return cls.P2PKH_address_from_hash160(hash160(pubkey))
@classmethod @classmethod
def P2SH_address_from_hash160(cls, pubkey_bytes): def P2SH_hash168_from_hash160(cls, hash160):
'''Return a coin address given a public key.''' assert len(hash160) == 20
assert len(hash_bytes) == 20 return bytes([cls.P2SH_VERBYTE]) + hash160
payload = bytes([cls.P2SH_VERBYTE]) + hash_bytes
return Base58.encode_check(payload) @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 @classmethod
def multisig_address(cls, m, pubkeys): def multisig_address(cls, m, pubkeys):

64
lib/script.py

@ -8,8 +8,8 @@
'''Script-related classes and functions.''' '''Script-related classes and functions.'''
from binascii import hexlify
import struct import struct
from collections import namedtuple
from lib.enum import Enumeration from lib.enum import Enumeration
from lib.hash import hash160 from lib.hash import hash160
@ -133,60 +133,38 @@ class ScriptSig(object):
class ScriptPubKey(object): class ScriptPubKey(object):
'''A script from a tx output that gives conditions necessary for '''A class for handling a tx output script that gives conditions
spending.''' necessary for spending.
'''
TO_ADDRESS, TO_P2SH, TO_PUBKEY, TO_UNKNOWN = range(4)
TO_ADDRESS_OPS = [OpCodes.OP_DUP, OpCodes.OP_HASH160, -1, TO_ADDRESS_OPS = [OpCodes.OP_DUP, OpCodes.OP_HASH160, -1,
OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG] OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG]
TO_P2SH_OPS = [OpCodes.OP_HASH160, -1, OpCodes.OP_EQUAL] TO_P2SH_OPS = [OpCodes.OP_HASH160, -1, OpCodes.OP_EQUAL]
TO_PUBKEY_OPS = [-1, OpCodes.OP_CHECKSIG] TO_PUBKEY_OPS = [-1, OpCodes.OP_CHECKSIG]
def __init__(self, script, coin, kind, hash168, pubkey=None): PayToHandlers = namedtuple('PayToHandlers',
self.script = script 'address script_hash pubkey unknown')
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 ''
@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)
@classmethod @classmethod
def parse_script(cls, script, coin): def pay_to(cls, script, handlers):
'''Returns an instance of this class. Raises on unrecognised '''Parse a script, invoke the appropriate handler and
scripts.''' return the result.
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) ops, datas = Script.get_ops(script)
if Script.match_ops(ops, cls.TO_ADDRESS_OPS): if Script.match_ops(ops, cls.TO_ADDRESS_OPS):
return cls(script, coin, cls.TO_ADDRESS, return handlers.address(datas[2])
bytes([coin.P2PKH_VERBYTE]) + datas[2])
if Script.match_ops(ops, cls.TO_P2SH_OPS): if Script.match_ops(ops, cls.TO_P2SH_OPS):
return cls(script, coin, cls.TO_P2SH, return handlers.script_hash(datas[1])
bytes([coin.P2SH_VERBYTE]) + datas[1])
if Script.match_ops(ops, cls.TO_PUBKEY_OPS): if Script.match_ops(ops, cls.TO_PUBKEY_OPS):
pubkey = datas[0] return handlers.pubkey(datas[0])
return cls(script, coin, cls.TO_PUBKEY, return handlers.unknown(None)
bytes([coin.P2PKH_VERBYTE]) + hash160(pubkey), pubkey)
raise ScriptError('unknown script pubkey pattern')
@classmethod @classmethod
def P2SH_script(cls, hash160): def P2SH_script(cls, hash160):
@ -317,4 +295,4 @@ class Script(object):
print(name) print(name)
else: else:
print('{} {} ({:d} bytes)' print('{} {} ({:d} bytes)'
.format(name, hexlify(data).decode('ascii'), len(data))) .format(name, data.hex(), len(data)))

9
lib/util.py

@ -31,16 +31,11 @@ class cachedproperty(object):
self.f = f self.f = f
def __get__(self, obj, type): def __get__(self, obj, type):
if obj is None: obj = obj or type
return self
value = self.f(obj) value = self.f(obj)
obj.__dict__[self.f.__name__] = value setattr(obj, self.f.__name__, value)
return value return value
def __set__(self, obj, value):
raise AttributeError('cannot set {} on {}'
.format(self.f.__name__, obj))
def deep_getsizeof(obj): def deep_getsizeof(obj):
"""Find the memory footprint of a Python object. """Find the memory footprint of a Python object.

11
server/block_processor.py

@ -20,7 +20,6 @@ from functools import partial
from server.cache import FSCache, UTXOCache, NO_CACHE_ENTRY from server.cache import FSCache, UTXOCache, NO_CACHE_ENTRY
from server.daemon import DaemonError from server.daemon import DaemonError
from lib.hash import hash_to_str from lib.hash import hash_to_str
from lib.script import ScriptPubKey
from lib.tx import Deserializer from lib.tx import Deserializer
from lib.util import chunks, LoggedClass from lib.util import chunks, LoggedClass
from server.storage import open_db from server.storage import open_db
@ -205,12 +204,11 @@ class MemPool(LoggedClass):
# The mempool is unordered, so process all outputs first so # The mempool is unordered, so process all outputs first so
# that looking for inputs has full info. # that looking for inputs has full info.
parse_script = ScriptPubKey.from_script script_hash168 = self.bp.coin.hash168_from_script
coin = self.bp.coin
utxo_lookup = self.bp.utxo_cache.lookup utxo_lookup = self.bp.utxo_cache.lookup
def txout_pair(txout): 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(): for hex_hash, tx in new_txs.items():
txout_pairs = tuple(txout_pair(txout) for txout in tx.outputs) 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 # Use local vars for speed in the loops
history = self.history history = self.history
tx_num = self.tx_count tx_num = self.tx_count
coin = self.coin script_hash168 = self.coin.hash168_from_script
parse_script = ScriptPubKey.from_script
pack = struct.pack pack = struct.pack
for tx, tx_hash in zip(txs, tx_hashes): for tx, tx_hash in zip(txs, tx_hashes):
@ -763,7 +760,7 @@ class BlockProcessor(LoggedClass):
# Add the new UTXOs # Add the new UTXOs
for idx, txout in enumerate(tx.outputs): for idx, txout in enumerate(tx.outputs):
# Get the hash168. Ignore scripts we can't grok. # 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: if hash168:
hash168s.add(hash168) hash168s.add(hash168)
put_utxo(tx_hash + pack('<H', idx), put_utxo(tx_hash + pack('<H', idx),

Loading…
Cancel
Save