Browse Source

Use hash168 to distinguish address types in DB

master
Neil Booth 8 years ago
parent
commit
6644102441
  1. 46
      lib/coins.py
  2. 17
      lib/script.py
  3. 18
      lib/util.py
  4. 8
      query.py
  5. 76
      server/db.py

46
lib/coins.py

@ -16,7 +16,7 @@ class CoinError(Exception):
class Coin(object):
'''Base class of coin hierarchy'''
'''Base class of coin hierarchy.'''
# Not sure if these are coin-specific
HEADER_LEN = 80
@ -52,59 +52,67 @@ class Coin(object):
raise CoinError("version bytes unrecognised")
@classmethod
def address_to_hash160(cls, addr):
'''Returns a hash160 given an address'''
def address_to_hash168(cls, addr):
'''Return a 21-byte hash given an address.
This is the hash160 prefixed by the address version byte.
'''
result = Base58.decode_check(addr)
if len(result) != 21:
raise CoinError('invalid address: {}'.format(addr))
return result[1:]
return result
@classmethod
def P2PKH_address_from_hash160(cls, hash_bytes):
'''Returns a P2PKH address given a public key'''
'''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)
@classmethod
def P2PKH_address_from_pubkey(cls, pubkey):
'''Returns a coin address given a public key'''
'''Return a coin address given a public key.'''
return cls.P2PKH_address_from_hash160(hash160(pubkey))
@classmethod
def P2SH_address_from_hash160(cls, pubkey_bytes):
'''Returns a coin address given a public key'''
'''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)
@classmethod
def multisig_address(cls, m, pubkeys):
'''Returns the P2SH address for an M of N multisig transaction. Pass
the N pubkeys of which M are needed to sign it. If generating
an address for a wallet, it is the caller's responsibility to
sort them to ensure order does not matter for, e.g., wallet
recovery.'''
'''Return the P2SH address for an M of N multisig transaction.
Pass the N pubkeys of which M are needed to sign it. If
generating an address for a wallet, it is the caller's
responsibility to sort them to ensure order does not matter
for, e.g., wallet recovery.
'''
script = cls.pay_to_multisig_script(m, pubkeys)
payload = bytes([cls.P2SH_VERBYTE]) + hash160(pubkey_bytes)
return Base58.encode_check(payload)
@classmethod
def pay_to_multisig_script(cls, m, pubkeys):
'''Returns a P2SH multisig script for an M of N multisig
transaction.'''
'''Return a P2SH script for an M of N multisig transaction.'''
return ScriptPubKey.multisig_script(m, pubkeys)
@classmethod
def pay_to_pubkey_script(cls, pubkey):
'''Returns a pubkey script that pays to pubkey. The input is the
raw pubkey bytes (length 33 or 65).'''
'''Return a pubkey script that pays to a pubkey.
Pass the raw pubkey bytes (length 33 or 65).
'''
return ScriptPubKey.P2PK_script(pubkey)
@classmethod
def pay_to_address_script(cls, address):
'''Returns a pubkey script that pays to pubkey hash. Input is the
address (either P2PKH or P2SH) in base58 form.'''
'''Return a pubkey script that pays to a pubkey hash.
Pass the address (either P2PKH or P2SH) in base58 form.
'''
raw = Base58.decode_check(address)
# Require version byte plus hash160.
@ -121,7 +129,7 @@ class Coin(object):
@classmethod
def prvkey_WIF(privkey_bytes, compressed):
"The private key encoded in Wallet Import Format"
"Return the private key encoded in Wallet Import Format."
payload = bytearray([cls.WIF_BYTE]) + privkey_bytes
if compressed:
payload.append(0x01)

17
lib/script.py

@ -131,20 +131,20 @@ class ScriptPubKey(object):
TO_P2SH_OPS = [OpCodes.OP_HASH160, -1, OpCodes.OP_EQUAL]
TO_PUBKEY_OPS = [-1, OpCodes.OP_CHECKSIG]
def __init__(self, script, coin, kind, hash160, pubkey=None):
def __init__(self, script, coin, kind, hash168, pubkey=None):
self.script = script
self.coin = coin
self.kind = kind
self.hash160 = hash160
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.hash160)
return self.coin.P2SH_address_from_hash160(self.hash168[1:])
if self.hash160:
return self.coin.P2PKH_address_from_hash160(self.hash160)
return self.coin.P2PKH_address_from_hash160(self.hash168[1:])
return ''
@classmethod
@ -163,14 +163,17 @@ class ScriptPubKey(object):
ops, datas = Script.get_ops(script)
if Script.match_ops(ops, cls.TO_ADDRESS_OPS):
return cls(script, coin, cls.TO_ADDRESS, datas[2])
return cls(script, coin, cls.TO_ADDRESS,
bytes([coin.P2PKH_VERBYTE]) + datas[2])
if Script.match_ops(ops, cls.TO_P2SH_OPS):
return cls(script, coin, cls.TO_P2SH, datas[1])
return cls(script, coin, cls.TO_P2SH,
bytes([coin.P2SH_VERBYTE]) + datas[1])
if Script.match_ops(ops, cls.TO_PUBKEY_OPS):
pubkey = datas[0]
return cls(script, coin, cls.TO_PUBKEY, hash160(pubkey), pubkey)
return cls(script, coin, cls.TO_PUBKEY,
bytes([coin.P2PKH_VERBYTE]) + hash160(pubkey), pubkey)
raise ScriptError('unknown script pubkey pattern')

18
lib/util.py

@ -5,24 +5,6 @@
import sys
class Log(object):
'''Logging base class'''
VERBOSE = True
def diagnostic_name(self):
return self.__class__.__name__
def log(self, *msgs):
if Log.VERBOSE:
print('[{}]: '.format(self.diagnostic_name()), *msgs,
file=sys.stdout, flush=True)
def log_error(self, *msg):
print('[{}]: ERROR: {}'.format(self.diagnostic_name()), *msgs,
file=sys.stderr, flush=True)
# Method decorator. To be used for calculations that will always
# deliver the same result. The method cannot take any arguments
# and should be accessed as an attribute.

8
query.py

@ -23,21 +23,21 @@ def main():
limit = 10
for addr in sys.argv[argc:]:
print('Address: ', addr)
hash160 = coin.address_to_hash160(addr)
hash168 = coin.address_to_hash168(addr)
n = None
for n, (tx_hash, height) in enumerate(db.get_history(hash160, limit)):
for n, (tx_hash, height) in enumerate(db.get_history(hash168, limit)):
print('History #{:d}: hash: {} height: {:d}'
.format(n + 1, bytes(reversed(tx_hash)).hex(), height))
if n is None:
print('No history')
n = None
for n, utxo in enumerate(db.get_utxos(hash160, limit)):
for n, utxo in enumerate(db.get_utxos(hash168, limit)):
print('UTXOs #{:d}: hash: {} pos: {:d} height: {:d} value: {:d}'
.format(n, bytes(reversed(utxo.tx_hash)).hex(),
utxo.tx_pos, utxo.height, utxo.value))
if n is None:
print('No UTXOs')
balance = db.get_balance(hash160)
balance = db.get_balance(hash168)
print('Balance: {} {}'.format(coin.decimal_value(balance),
coin.SHORTNAME))

76
server/db.py

@ -256,56 +256,56 @@ class DB(object):
self.history.pop(None, None)
flush_id = struct.pack('>H', self.flush_count)
for hash160, hist in self.history.items():
key = b'H' + hash160 + flush_id
for hash168, hist in self.history.items():
key = b'H' + hash168 + flush_id
batch.put(key, array.array('I', hist).tobytes())
self.history = defaultdict(list)
def get_hash160(self, tx_hash, idx, delete=True):
def get_hash168(self, tx_hash, idx, delete=True):
key = b'h' + tx_hash[:ADDR_TX_HASH_LEN] + struct.pack('<H', idx)
data = self.get(key)
if data is None:
return None
if len(data) == 24:
if len(data) == 25:
if delete:
self.delete(key)
return data[:20]
return data[:21]
assert len(data) % 24 == 0
assert len(data) % 25 == 0
self.hcolls += 1
if self.hcolls % 1000 == 0:
self.logger.info('{} total hash160 compressed key collisions'
self.logger.info('{} total hash168 compressed key collisions'
.format(self.hcolls))
for n in range(0, len(data), 24):
(tx_num, ) = struct.unpack('<I', data[n+20:n+24])
for n in range(0, len(data), 25):
(tx_num, ) = struct.unpack('<I', data[n+21 : n+25])
my_hash, height = self.get_tx_hash(tx_num)
if my_hash == tx_hash:
if delete:
self.put(key, data[:n] + data[n + 24:])
return data[n:n+20]
else:
raise Exception('could not resolve hash160 collision')
self.put(key, data[:n] + data[n+25:])
return data[n : n+21]
raise Exception('could not resolve hash168 collision')
def spend_utxo(self, prevout):
hash160 = self.get_hash160(prevout.hash, prevout.n)
if hash160 is None:
hash168 = self.get_hash168(prevout.hash, prevout.n)
if hash168 is None:
# This indicates a successful spend of a non-standard script
# self.logger.info('ignoring spend of non-standard UTXO {}/{:d} '
# 'at height {:d}'
# .format(bytes(reversed(prevout.hash)).hex(),
# prevout.n, self.height))
return None
key = (b'u' + hash160 + prevout.hash[:UTXO_TX_HASH_LEN]
key = (b'u' + hash168 + prevout.hash[:UTXO_TX_HASH_LEN]
+ struct.pack('<H', prevout.n))
data = self.get(key)
if data is None:
# Uh-oh, this should not happen. It may be recoverable...
self.logger.error('found no UTXO for {} / {:d} key {}'
.format(bytes(reversed(prevout.hash)).hex(),
prevout.n, bytes(key).hex()))
return hash160
return hash168
if len(data) == 12:
(tx_num, ) = struct.unpack('<I', data[:4])
@ -324,35 +324,35 @@ class DB(object):
data = data[:n] + data[n + 12:]
self.put(key, data)
return hash160
return hash168
def put_utxo(self, tx_hash, idx, txout):
pk = ScriptPubKey.from_script(txout.pk_script, self.coin)
if not pk.hash160:
if not pk.hash168:
return None
pack = struct.pack
idxb = pack('<H', idx)
txcb = pack('<I', self.tx_count)
# First write the hash160 lookup
# First write the hash168 lookup
key = b'h' + tx_hash[:ADDR_TX_HASH_LEN] + idxb
# b'' avoids this annoyance: https://bugs.python.org/issue13298
value = b''.join([pk.hash160, txcb])
# b''.join avoids this: https://bugs.python.org/issue13298
value = b''.join((pk.hash168, txcb))
prior_value = self.get(key)
if prior_value: # Should almost never happen
value += prior_value
self.put(key, value)
# Next write the UTXO
key = b'u' + pk.hash160 + tx_hash[:UTXO_TX_HASH_LEN] + idxb
key = b'u' + pk.hash168 + tx_hash[:UTXO_TX_HASH_LEN] + idxb
value = txcb + pack('<Q', txout.value)
prior_value = self.get(key)
if prior_value: # Should almost never happen
value += prior_value
self.put(key, value)
return pk.hash160
return pk.hash168
def open_file(self, filename, truncate=False, create=False):
try:
@ -420,16 +420,16 @@ class DB(object):
self.flush()
def process_tx(self, tx_hash, tx):
hash160s = set()
hash168s = set()
if not tx.is_coinbase:
for txin in tx.inputs:
hash160s.add(self.spend_utxo(txin.prevout))
hash168s.add(self.spend_utxo(txin.prevout))
for idx, txout in enumerate(tx.outputs):
hash160s.add(self.put_utxo(tx_hash, idx, txout))
hash168s.add(self.put_utxo(tx_hash, idx, txout))
for hash160 in hash160s:
self.history[hash160].append(self.tx_count)
for hash168 in hash168s:
self.history[hash168].append(self.tx_count)
self.tx_count += 1
@ -458,7 +458,7 @@ class DB(object):
assert isinstance(limit, int) and limit >= 0
return limit
def get_history(self, hash160, limit=1000):
def get_history(self, hash168, 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
@ -466,7 +466,7 @@ class DB(object):
Set limit to None to get them all.
'''
limit = self.resolve_limit(limit)
prefix = b'H' + hash160
prefix = b'H' + hash168
for key, hist in self.db.iterator(prefix=prefix):
a = array.array('I')
a.frombytes(hist)
@ -476,18 +476,18 @@ class DB(object):
yield self.get_tx_hash(tx_num)
limit -= 1
def get_balance(self, hash160):
def get_balance(self, hash168):
'''Returns the confirmed balance of an address.'''
return sum(utxo.value for utxo in self.get_utxos(hash160, limit=None))
return sum(utxo.value for utxo in self.get_utxos(hash168, limit=None))
def get_utxos(self, hash160, limit=1000):
def get_utxos(self, hash168, 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
prefix = b'u' + hash168
utxos = []
for k, v in self.db.iterator(prefix=prefix):
(tx_pos, ) = unpack('<H', k[-2:])
@ -501,7 +501,7 @@ class DB(object):
yield UTXO(tx_num, tx_pos, tx_hash, height, value)
limit -= 1
def get_utxos_sorted(self, hash160):
def get_utxos_sorted(self, hash168):
'''Returns all the UTXOs for an address sorted by height and
position in the block.'''
return sorted(self.get_utxos(hash160, limit=None))
return sorted(self.get_utxos(hash168, limit=None))

Loading…
Cancel
Save