Merge branch 'develop'
This commit is contained in:
commit
3893b4f22f
46
lib/coins.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)
|
||||
|
||||
@ -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
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
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
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))
|
||||
|
||||
@ -75,13 +75,16 @@ class BlockCache(object):
|
||||
.format(self.cache_limit))
|
||||
|
||||
last_log = 0
|
||||
prior_height = self.db.height
|
||||
while await self.maybe_prefill():
|
||||
now = time.time()
|
||||
if now > last_log + 15:
|
||||
count = self.fetched_height - prior_height
|
||||
if now > last_log + 15 and count:
|
||||
last_log = now
|
||||
self.logger.info('prefilled blocks to height {:,d} '
|
||||
prior_height = self.fetched_height
|
||||
self.logger.info('prefilled {:,d} blocks to height {:,d} '
|
||||
'daemon height: {:,d}'
|
||||
.format(self.fetched_height,
|
||||
.format(count, self.fetched_height,
|
||||
self.daemon_height))
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user