From 43231b3cf3d1128a6dfede03aad0aff09afadf1d Mon Sep 17 00:00:00 2001 From: buck54321 Date: Tue, 10 Sep 2019 14:18:16 -0500 Subject: [PATCH 01/12] add full script support. add stakepool client Adds support for all script types other that alt-sig types. Implementation is a mirror of parts of txscript and dcrutil modules. Also adds basic stake pool client. Adds new http module in util, used in both dcrdata and stakepool. --- app.py | 1 + crypto/crypto.py | 269 ++++++++++-- pydecred/dcrdata.py | 45 +- pydecred/txscript.py | 964 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 1164 insertions(+), 115 deletions(-) diff --git a/app.py b/app.py index a87bf77f..94e31268 100644 --- a/app.py +++ b/app.py @@ -13,6 +13,7 @@ from tinydecred.pydecred import constants as DCR from tinydecred.pydecred.dcrdata import DcrdataBlockchain from tinydecred.wallet import Wallet +from tinydecred.crypto import crypto from tinydecred.ui import screens, ui, qutilities as Q # the directory of the tinydecred package diff --git a/crypto/crypto.py b/crypto/crypto.py index 258b8780..f385d09b 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -18,6 +18,7 @@ KEY_SIZE = 32 HASH_SIZE = 32 BLAKE256_SIZE = 32 +RIPEMD160_SIZE = 20 SERIALIZED_KEY_LENGTH = 4 + 1 + 4 + 4 + 32 + 33 # 78 bytes HARDENED_KEY_START = 2**31 MAX_COIN_TYPE = HARDENED_KEY_START - 1 @@ -47,6 +48,14 @@ # signature over the secp256k1 elliptic curve. STSchnorrSecp256k1 = 2 +# PKFUncompressed indicates the pay-to-pubkey address format is an +# uncompressed public key. +PKFUncompressed = 0 + +# PKFCompressed indicates the pay-to-pubkey address format is a +# compressed public key. +PKFCompressed = 1 + class ParameterRangeError(Exception): pass class ZeroBytesError(Exception): @@ -54,15 +63,25 @@ class ZeroBytesError(Exception): class PasswordError(Exception): pass -class Address: +def encodeAddress(netID, k): + b = ByteArray(netID) + b += k + b += checksum(b.b) + return b58encode(b.bytes()).decode() + +class AddressPubKeyHash: """ - Address represents an address, which is a pubkey hash and its base-58 - encoding. + AddressPubKeyHash represents an address based on a pubkey hash. """ - def __init__(self, netID=None, pkHash=None, net=None): + def __init__(self, netID=None, pkHash=None, sigType=STEcdsaSecp256k1): + if len(pkHash) != 20: + raise Exception("AddressPubKeyHash expected 20 bytes, got %d" % len(pkHash)) + # For now, just reject anything except secp256k1 + if sigType != STEcdsaSecp256k1: + raise Exception("unsupported signature type %v", self.sigType) + self.sigType = sigType self.netID = netID self.pkHash = pkHash - self.net = net def string(self): """ A base-58 encoding of the pubkey hash. @@ -70,23 +89,107 @@ def string(self): Returns: str: The encoded address. """ - b = ByteArray(self.netID) - b += self.pkHash - b += checksum(b.b) - x = b.int() - - answer = "" + return encodeAddress(self.netID, self.pkHash) + def address(self): + return self.string() + def scriptAddress(self): + return self.pkHash + def hash160(self): + return self.pkHash + +class AddressSecpPubKey: + """ + AddressSecpPubKey represents and address, which is a pubkey hash and it's + base-58 encoding. Argument pubkey should be a ByteArray corresponding the + the serializedCompressed public key (33 bytes). + """ + def __init__(self, serializedPubkey, net): + pubkey = Curve.parsePubKey(serializedPubkey) + # Set the format of the pubkey. This probably should be returned + # from dcrec, but do it here to avoid API churn. We already know the + # pubkey is valid since it parsed above, so it's safe to simply examine + # the leading byte to get the format. + fmt = serializedPubkey[0] + if fmt in (0x02, 0x03): + pkFormat = PKFCompressed + elif fmt == 0x04: + pkFormat = PKFUncompressed + else: + raise Exception("unknown pubkey format %d", fmt) + self.pubkeyFormat = pkFormat + self.netID = self.pubkeyID = net.PubKeyAddrID + self.pubkeyHashID = net.PubKeyHashAddrID + self.pubkey = pubkey + def serialize(self): + """ + serialize returns the serialization of the public key according to the + format associated with the address. + """ + fmt = self.pubkeyFormat + if fmt == PKFUncompressed: + return self.pubkey.serializeUncompressed() + elif fmt == PKFCompressed: + return self.pubkey.serializeCompressed() + raise Exception("unknown pubkey format") + def string(self): + """ + A base-58 encoding of the pubkey. - while x > 0: - m = x%RADIX - x = x//RADIX - answer += ALPHABET[m] + Returns: + str: The encoded address. + """ + encoded = ByteArray(self.pubkeyID) + buf = ByteArray(STEcdsaSecp256k1, length=1) + compressed = self.pubkey.serializeCompressed() + # set the y-bit if needed + if compressed[0] == 0x03: + buf[0] |= (1 << 7) + buf += compressed[1:] + encoded += buf + encoded += checksum(encoded.b) + return b58encode(encoded.bytes()).decode() + def address(self): + """ + Address returns the string encoding of the public key as a + pay-to-pubkey-hash. Note that the public key format (uncompressed, + compressed, etc) will change the resulting address. This is expected since + pay-to-pubkey-hash is a hash of the serialized public key which obviously + differs with the format. At the time of this writing, most Decred addresses + are pay-to-pubkey-hash constructed from the compressed public key. + """ + return encodeAddress(self.pubkeyHashID, hash160(self.serialize().bytes())) + def scriptAddress(self): + return self.serialize() + def hash160(self): + return hash160(self.serialize().bytes()) - while len(answer) < len(b)*136//100: - answer += ALPHABET[0] +class AddressScriptHash(object): + """ + AddressScriptHash is an Address for a pay-to-script-hash (P2SH) transaction. + """ + def __init__(self, netID, scriptHash): + self.netID = netID + self.scriptHash = scriptHash + @staticmethod + def fromScript(netID, script): + return AddressScriptHash(netID, hash160(script.b)) + def string(self): + """ + A base-58 encoding of the pubkey hash. - # reverse - return answer[::-1] + Returns: + str: The encoded address. + """ + return encodeAddress(self.netID, self.scriptHash) + def address(self): + """ + Address returns the string encoding of a pay-to-script-hash address. + """ + return self.string() + def scriptAddress(self): + return self.scriptHash + def hash160(self): + return self.scriptHash def hmacDigest(key, msg, digestmod=hashlib.sha512): """ @@ -111,7 +214,7 @@ def hash160(b): b (byte-like): The bytes to hash. Returns: - ByteArray: A 20-byte hash. + byte-like: A 20-byte hash. """ h = hashlib.new("ripemd160") h.update(blake_hash(b)) @@ -241,9 +344,38 @@ def b58CheckDecode(s): payload = decoded[2 : len(decoded)-4] return payload, version +def newAddressPubKey(decoded, net): + """ + NewAddressPubKey returns a new Address. decoded must be 33 bytes. This + constructor takes the decoded pubkey such as would be decoded from a base58 + string. The first byte indicates the signature suite. For compressed + secp256k1 pubkeys, use AddressSecpPubKey directly. + """ + if len(decoded) == 33: + # First byte is the signature suite and ybit. + suite = decoded[0] + suite &= ~(1 << 7) + ybit = not (decoded[0]&(1<<7) == 0) + toAppend = 0x02 + if ybit: + toAppend = 0x03 + + if suite == STEcdsaSecp256k1: + b = ByteArray(toAppend) + decoded[1:] + return AddressSecpPubKey(b, net) + elif suite == STEd25519: + # return NewAddressEdwardsPubKey(decoded, net) + raise Exception("Edwards signatures not implemented") + elif suite == STSchnorrSecp256k1: + # return NewAddressSecSchnorrPubKey(append([]byte{toAppend}, decoded[1:]...), net) + raise Exception("Schnorr signatures not implemented") + else: + raise Exception("unknown address type %d" % suite) + raise Exception("unable to decode pubkey of length %d" % len(decoded)) + def newAddressPubKeyHash(pkHash, net, algo): """ - newAddressPubKeyHash returns a new Address. + newAddressPubKeyHash returns a new AddressPubkeyHash. Args: pkHash (ByteArray): The hash160 of the public key. @@ -256,12 +388,43 @@ def newAddressPubKeyHash(pkHash, net, algo): if algo == STEcdsaSecp256k1: netID = net.PubKeyHashAddrID elif algo == STEd25519: - netID = net.PKHEdwardsAddrID + # netID = net.PKHEdwardsAddrID + raise Exception("Edwards not implemented") elif algo == STSchnorrSecp256k1: - netID = net.PKHSchnorrAddrID + # netID = net.PKHSchnorrAddrID + raise Exception("Schnorr not implemented") else: raise Exception("unknown ECDSA algorithm") - return Address(netID, pkHash, net) + return AddressPubKeyHash(netID, pkHash) + +def newAddressScriptHash(script, net): + """ + newAddressScriptHash returns a new AddressScriptHash from a redeem script. + + Args: + script (ByteArray): the redeem script + net (obj): the network parameters + + Returns: + AddressScriptHash: An address object. + """ + return newAddressScriptHashFromHash(hash160(script.b), net) + +def newAddressScriptHashFromHash(scriptHash, net): + """ + newAddressScriptHashFromHash returns a new AddressScriptHash from an already + hash160'd script. + + Args: + pkHash (ByteArray): The hash160 of the public key. + net (obj): The network parameters. + + Returns: + AddressScriptHash: An address object. + """ + if len(scriptHash) != RIPEMD160_SIZE: + raise Exception("incorrect script hash length") + return AddressScriptHash(net.ScriptHashAddrID, scriptHash) class ExtendedKey: """ @@ -831,6 +994,16 @@ def rekey(password, kp): raise PasswordError("rekey digest check failed") return sk + +testAddrMagics = { + "pubKeyID": (0x1386).to_bytes(2, byteorder="big"), # starts with Dk + "pkhEcdsaID": (0x073f).to_bytes(2, byteorder="big"), # starts with Ds + "pkhEd25519ID": (0x071f).to_bytes(2, byteorder="big"), # starts with De + "pkhSchnorrID": (0x0701).to_bytes(2, byteorder="big"), # starts with DS + "scriptHashID": (0x071a).to_bytes(2, byteorder="big"), # starts with Dc + "privKeyID": (0x22de).to_bytes(2, byteorder="big"), # starts with Pm +} + class TestCrypto(unittest.TestCase): def test_encryption(self): ''' @@ -841,19 +1014,35 @@ def test_encryption(self): b = SecretKey.rekey("abc".encode(), a.params()) aUnenc = b.decrypt(aEnc.bytes()) self.assertTrue(a, aUnenc) - def test_curve(self): - ''' - Test curves. Unimplemented. - ''' - pass - def test_priv_keys(self): - ''' - Test private key parsing. - ''' - key = ByteArray("eaf02ca348c524e6392655ba4d29603cd1a7347d9d65cfe93ce1ebffdca22694") - pk = privKeyFromBytes(key) - - inHash = ByteArray("00010203040506070809") - # sig = txscript.signRFC6979(pk.key, inHash) - - # self.assertTrue(txscript.verifySig(pk.pub, inHash, sig.r, sig.s)) + def test_addr_pubkey(self): + from tinydecred.pydecred import mainnet + hexKeys = [ + "033b26959b2e1b0d88a050b111eeebcf776a38447f7ae5806b53c9b46e07c267ad", + "0389ced3eaee84d5f0d0e166f6cd15f1bf6f429d1d13709393b418a6fb22d8be53", + "02a14a0023d7d8cbc5d39fa60f7e4dc4d5bf18a7031f52875fbca6bf837f68713f", + "03c3e3d7cde1c453a6283f5802a73d1cb3827cb4b007f58e3a52a36ce189934b6a", + "0254e17b230e782e591a9910794fdbf9943d500a47f2bf8446e1238f84e809bffc", + ] + b58Keys = [ + "DkRKjw7LmGCSzBwaUtjQLfb75Zcx9hH8yGNs3qPSwVzZuUKs7iu2e", + "DkRLLaJWkmH75iZGtQYE6FEf16zxeHr6TCAF59tGxhds4MFc2HqUS", + "DkM3hdWuKSSTm7Vq8WZx5f294vcZbPkAQYBDswkjmF1CFuWCRYxTr", + "DkRLn9vzsjK4ZYgDKy7JVYHKGvpZU5CYGK9H8zF2VCWbpTyVsEf4P", + "DkM37ymaat9j6oTFii1MZVpXrc4aRLEMHhTZrvrz8QY6BZ2HX843L", + ] + for hexKey, b58Key in zip(hexKeys, b58Keys): + pubkey = ByteArray(hexKey) + addr = AddressSecpPubKey(pubkey, mainnet) + self.assertEqual(addr.string(), b58Key) + def test_addr_pubkey_hash(self): + from tinydecred.pydecred import mainnet + pairs = [ + ("e201ee2f37bcc0ba0e93f82322e48333a92b9355", "DsmZvWuokf5NzFwFfJk5cALZZBZivjkhMSQ"), + ("5643d59202de158b509544d40b32e85bfaf6243e", "DsYq2s8mwpM6vXLbjb8unhNmBXFofPzcrrv"), + ("c5fa0d15266e055eaf8ec7c4d7a679885266ef0d", "Dsj1iA5PBCU6Nmpe6jqucwfHK17WmSKd3uG"), + ("73612f7b7b1ed32ff44dded7a2cf87c206fabf8a", "DsbUyd4DueVNyvfh542kZDXNEGKByUAi1RV"), + ("a616bc09179e31e6d9e3abfcb16ac2d2baf45141", "Dsg76ttvZmTFchZ5mWRnAUg6UGfCyrq86ch"), + ] + for pubkeyHash, addrStr in pairs: + addr = AddressPubKeyHash(mainnet.PubKeyHashAddrID, ByteArray(pubkeyHash)) + self.assertEqual(addr.string(), addrStr) diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index c0998d27..0fbe248a 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -2,10 +2,8 @@ Copyright (c) 2019, Brian Stafford See LICENSE for details -pyDcrDdta DcrdataClient.endpointList() for available enpoints. """ -import urllib.request as urlrequest from urllib.parse import urlparse, urlencode import time @@ -19,7 +17,7 @@ import os import websocket from tempfile import TemporaryDirectory -from tinydecred.util import tinyjson, helpers, database +from tinydecred.util import tinyjson, helpers, database, http from tinydecred.crypto import opcode, crypto from tinydecred.crypto.bytearray import ByteArray from tinydecred.api import InsufficientFundsError @@ -30,8 +28,11 @@ log = helpers.getLogger("DCRDATA") # , logLvl=0) VERSION = "0.0.1" -HEADERS = {"User-Agent": "PyDcrData/%s" % VERSION} - +GET_HEADERS = {"User-Agent": "PyDcrData/%s" % VERSION} +POST_HEADERS = { + "User-Agent": "tinydecred/%s" % VERSION, + "Content-Type":"application/json; charset=utf-8", +} # Many of these constants were pulled from the dcrd, and are left as mixed case # to maintain reference. @@ -81,34 +82,6 @@ formatTraceback = helpers.formatTraceback -def getUri(uri): - return performRequest(uri) - -def postData(uri, data): - return performRequest(uri, data) - -def performRequest(uri, post=None): - try: - headers = HEADERS - if post: - encoded = tinyjson.dump(post).encode("utf-8") - req = urlrequest.Request(uri, data=encoded) - req.add_header("User-Agent", "PyDcrData/%s" % VERSION) - req.add_header("Content-Type", "application/json; charset=utf-8") - else: - req = urlrequest.Request(uri, headers=headers, method="GET") - raw = urlrequest.urlopen(req).read().decode() - try: - return tinyjson.load(raw) - except tinyjson.JSONDecodeError: - # A couple of paths return simple strings or integers. block/best/hash or block/best/height for instance. - return raw - except Exception as e: - raise DcrDataException("JSONError", "Failed to decode server response from path %s: %s : %s" % (uri, raw, formatTraceback(e))) - except Exception as e: - raise DcrDataException("RequestError", "Error encountered in requesting path %s: %s" % (uri, formatTraceback(e))) - - class DcrdataPath(object): """ DcrdataPath represents some point along a URL. It may just be a node that @@ -154,10 +127,10 @@ def __getattr__(self, key): raise DcrDataException("SubpathError", "No subpath %s found in datapath" % (key,)) def __call__(self, *args, **kwargs): - return getUri(self.getCallsignPath(*args, **kwargs)) + return http.get(self.getCallsignPath(*args, **kwargs), headers=GET_HEADERS) def post(self, data): - return postData(self.getCallsignPath(), data) + return http.post(self.getCallsignPath(), data, headers=POST_HEADERS) def getSocketURIs(uri): uri = urlparse(uri) @@ -191,7 +164,7 @@ def __init__(self, baseURI, customPaths=None, emitter=None): self.listEntries = [] customPaths = customPaths if customPaths else [] # /list returns a json list of enpoints with parameters in template format, base/A/{param}/B - endpoints = getUri(self.baseApi + "/list") + endpoints = http.get(self.baseApi + "/list", headers=GET_HEADERS) endpoints += customPaths def getParam(part): diff --git a/pydecred/txscript.py b/pydecred/txscript.py index cd013019..1c864c06 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -128,7 +128,7 @@ def __init__(self, version, script): self.version = version self.offset = 0 self.op = None - self.data = None + self.d = None self.err = None def next(self): """ @@ -159,7 +159,7 @@ def next(self): # OP_0, and OP_[1-16] represent the data themselves. self.offset += 1 self.op = op - self.data = None + self.d = ByteArray(b'') return True elif op.length > 1: # Data pushes of specific lengths -- OP_DATA_[1-75]. @@ -171,7 +171,7 @@ def next(self): # Move the offset forward and set the opcode and data accordingly. self.offset += op.length self.op = op - self.data = script[1:op.length] + self.d = script[1:op.length] return True elif op.length < 0: # Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. @@ -202,7 +202,7 @@ def next(self): # Move the offset forward and set the opcode and data accordingly. self.offset += 1 - op.length + dataLen self.op = op - self.data = script[:dataLen] + self.d = script[:dataLen] return False # The only remaining case is an opcode with length zero which is @@ -226,6 +226,25 @@ def opcode(self): if self.op is None: return None return self.op.value + def data(self): + """ + Data returns the data associated with the most recently successfully parsed + opcode. + + Returns: + ByteArray: The data + """ + return self.d + def byteIndex(self): + """ + ByteIndex returns the current offset into the full script that will be + parsed next and therefore also implies everything before it has already + been parsed. + + Returns: + int: the current offset + """ + return self.offset def checkScriptParses(scriptVersion, script): """ @@ -353,7 +372,7 @@ def isPubKeyHashScript(script): def extractPubKeyHash(script): """ extractPubKeyHash extracts the public key hash from the passed script if it - is a standard pay-to-pubkey-hash script. It will return nil otherwise. + is a standard pay-to-pubkey-hash script. It will return None otherwise. """ # A pay-to-pubkey-hash script is of the form: # OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG @@ -367,43 +386,321 @@ def extractPubKeyHash(script): return script[3:23] return None -def payToAddrScript(netID, pkHash, chain): +def extractScriptHash(pkScript): + """ + extractScriptHash extracts the script hash from the passed script if it is a + standard pay-to-script-hash script. It will return nil otherwise. + + NOTE: This function is only valid for version 0 opcodes. Since the function + does not accept a script version, the results are undefined for other script + versions. + """ + # A pay-to-script-hash script is of the form: + # OP_HASH160 <20-byte scripthash> OP_EQUAL + if (len(pkScript) == 23 and + pkScript[0] == opcode.OP_HASH160 and + pkScript[1] == opcode.OP_DATA_20 and + pkScript[22] == opcode.OP_EQUAL): + + return pkScript[2:22] + return None + +def isScriptHashScript(pkScript): + """ + isScriptHashScript returns whether or not the passed script is a standard + pay-to-script-hash script. + """ + return extractScriptHash(pkScript) != None + +def extractPubKey(script): + """ + extractPubKey extracts either compressed or uncompressed public key from the + passed script if it is a either a standard pay-to-compressed-secp256k1-pubkey + or pay-to-uncompressed-secp256k1-pubkey script, respectively. It will return + nil otherwise. + """ + pubkey = extractCompressedPubKey(script) + if pubkey: + return pubkey + return extractUncompressedPubKey(script) + +def extractCompressedPubKey(script): + """ + extractCompressedPubKey extracts a compressed public key from the passed + script if it is a standard pay-to-compressed-secp256k1-pubkey script. It + will return nil otherwise. + """ + # pay-to-compressed-pubkey script is of the form: + # OP_DATA_33 <33-byte compresed pubkey> OP_CHECKSIG + + # All compressed secp256k1 public keys must start with 0x02 or 0x03. + if (len(script) == 35 and + script[34] == opcode.OP_CHECKSIG and + script[0] == opcode.OP_DATA_33 and + (script[1] == 0x02 or script[1] == 0x03)): + return script[1:34] + return None + +def extractUncompressedPubKey(script): + """ + extractUncompressedPubKey extracts an uncompressed public key from the + passed script if it is a standard pay-to-uncompressed-secp256k1-pubkey + script. It will return nil otherwise. + """ + # A pay-to-compressed-pubkey script is of the form: + # OP_DATA_65 <65-byte uncompressed pubkey> OP_CHECKSIG + + # All non-hybrid uncompressed secp256k1 public keys must start with 0x04. + if (len(script) == 67 and + script[66] == opcode.OP_CHECKSIG and + script[0] == opcode.OP_DATA_65 and + script[1] == 0x04): + + return script[1:66] + return None + +def isPubKeyScript(script): + """ + isPubKeyScript returns whether or not the passed script is either a standard + pay-to-compressed-secp256k1-pubkey or pay-to-uncompressed-secp256k1-pubkey + script. + """ + return extractPubKey(script) != None + + +def isStakeScriptHash(script, stakeOpcode): + """ + isStakeScriptHash returns whether or not the passed public key script is a + standard pay-to-script-hash script tagged with the provided stake opcode. + """ + return extractStakeScriptHash(script, stakeOpcode) != None + +def extractStakeScriptHash(script, stakeOpcode): + """ + extractStakeScriptHash extracts a script hash from the passed public key + script if it is a standard pay-to-script-hash script tagged with the provided + stake opcode. It will return None otherwise. + """ + if (len(script) == 24 and + script[0] == stakeOpcode and + script[1] == opcode.OP_HASH160 and + script[2] == opcode.OP_DATA_20 and + script[23] == opcode.OP_EQUAL): + return script[3:23] + return None + +def extractStakePubKeyHash(script, stakeOpcode): + """ + extractStakePubKeyHash extracts the public key hash from the passed script if + it is a standard stake-tagged pay-to-pubkey-hash script with the provided + stake opcode. It will return nil otherwise. + """ + # A stake-tagged pay-to-pubkey-hash is of the form: + # + + # The script can't possibly be a stake-tagged pay-to-pubkey-hash if it + # doesn't start with the given stake opcode. Fail fast to avoid more work + # below. + if len(script) < 1 or script[0] != stakeOpcode: + return None + return extractPubKeyHash(script[1:]) + +class multiSigDetails(object): + """ + multiSigDetails houses details extracted from a standard multisig script. + """ + def __init__(self, pubkeys, numPubKeys, requiredSigs, valid): + self.requiredSigs = requiredSigs + self.numPubKeys = numPubKeys + self.pubKeys = pubkeys + self.valid = valid + +def invalidMSDetails(): + return multiSigDetails([], 0, [], False) + +def extractMultisigScriptDetails(scriptVersion, script, extractPubKeys): + """ + extractMultisigScriptDetails attempts to extract details from the passed + script if it is a standard multisig script. The returned details struct will + have the valid flag set to false otherwise. + + The extract pubkeys flag indicates whether or not the pubkeys themselves + should also be extracted and is provided because extracting them results in + an allocation that the caller might wish to avoid. The pubKeys member of + the returned details struct will be nil when the flag is false. + + NOTE: This function is only valid for version 0 scripts. The returned + details struct will always be empty and have the valid flag set to false for + other script versions. + """ + # The only currently supported script version is 0. + if scriptVersion != 0: + return invalidMSDetails() + + # A multi-signature script is of the form: + # NUM_SIGS PUBKEY PUBKEY PUBKEY ... NUM_PUBKEYS OP_CHECKMULTISIG + + # The script can't possibly be a multisig script if it doesn't end with + # OP_CHECKMULTISIG or have at least two small integer pushes preceding it. + # Fail fast to avoid more work below. + if len(script) < 3 or script[len(script)-1] != opcode.OP_CHECKMULTISIG: + return invalidMSDetails() + # The first opcode must be a small integer specifying the number of + # signatures required. + tokenizer = ScriptTokenizer(scriptVersion, script) + if not tokenizer.next() or not isSmallInt(tokenizer.opcode()): + return invalidMSDetails() + requiredSigs = asSmallInt(tokenizer.opcode()) + # The next series of opcodes must either push public keys or be a small + # integer specifying the number of public keys. + numPubkeys = 0 + pubkeys = [] + while tokenizer.next(): + data = tokenizer.data() + if not isStrictPubKeyEncoding(data): + break + numPubkeys += 1 + if extractPubKeys: + pubkeys.append(data) + if tokenizer.done(): + return invalidMSDetails() + # The next opcode must be a small integer specifying the number of public + # keys required. + op = tokenizer.opcode() + if not isSmallInt(op) or asSmallInt(op) != numPubkeys: + return invalidMSDetails() + + # There must only be a single opcode left unparsed which will be + # OP_CHECKMULTISIG per the check above. + if len(tokenizer.script)-tokenizer.byteIndex() != 1: + return invalidMSDetails() + return multiSigDetails(pubkeys, numPubkeys, requiredSigs, True) + +# asSmallInt returns the passed opcode, which must be true according to +# isSmallInt(), as an integer. +def asSmallInt(op): + if op == opcode.OP_0: + return 0 + return int(op - (opcode.OP_1 - 1)) + +def isSmallInt(op): + """ + isSmallInt returns whether or not the opcode is considered a small integer, + which is an OP_0, or OP_1 through OP_16. + + NOTE: This function is only valid for version 0 opcodes. Since the function + does not accept a script version, the results are undefined for other script + versions. + """ + return op == opcode.OP_0 or (op >= opcode.OP_1 and op <= opcode.OP_16) + +def isStrictPubKeyEncoding(pubKey): + """ + isStrictPubKeyEncoding returns whether or not the passed public key adheres + to the strict encoding requirements. + """ + if len(pubKey) == 33 and (pubKey[0] == 0x02 or pubKey[0] == 0x03): + # Compressed + return True + if len(pubKey) == 65 and pubKey[0] == 0x04: + # Uncompressed + return True + return False + +def payToAddrScript(addr): + """ + PayToAddrScript creates a new script to pay a transaction output to a the + specified address. + """ + if isinstance(addr, crypto.AddressPubKeyHash): + if addr.sigType == crypto.STEcdsaSecp256k1: + return payToPubKeyHashScript(addr.scriptAddress()) + elif addr.sigType == crypto.STEd25519: + # return payToPubKeyHashEdwardsScript(addr.ScriptAddress()) + raise Exception("Edwards signatures not implemented") + elif addr.sigType == crypto.STSchnorrSecp256k1: + # return payToPubKeyHashSchnorrScript(addr.ScriptAddress()) + raise Exception("Schnorr signatures not implemented") + raise Exception("unknown signature type %d" % addr.sigType) + + elif isinstance(addr, crypto.AddressScriptHash): + return payToScriptHashScript(addr.scriptAddress()) + + elif isinstance(addr, crypto.AddressSecpPubKey): + return payToPubKeyScript(addr.scriptAddress()) + + elif isinstance(addr, crypto.AddressEdwardsPubKey): + # return payToEdwardsPubKeyScript(addr.ScriptAddress()) + raise Exception("Edwards signatures not implemented") + + elif isinstance(addr, crypto.AddressSecSchnorrPubKey): + # return payToSchnorrPubKeyScript(addr.ScriptAddress()) + raise Exception("Schnorr signatures not implemented") + + raise Exception("unable to generate payment script for unsupported address type %s" % type(addr)) + +def payToPubKeyHashScript(pkHash): """ payToAddrScript creates a new script to pay a transaction output to a the specified address. """ - if netID == chain.PubKeyHashAddrID: - script = ByteArray(b'') - script += opcode.OP_DUP - script += opcode.OP_HASH160 - script += addData(pkHash) - script += opcode.OP_EQUALVERIFY - script += opcode.OP_CHECKSIG - return script - raise Exception("unimplemented signature type") + if len(pkHash) != 20: + raise Exception("cannot create script with pubkey hash length %d. expected length 20" % len(pkHash)) + script = ByteArray(b'') + script += opcode.OP_DUP + script += opcode.OP_HASH160 + script += addData(pkHash) + script += opcode.OP_EQUALVERIFY + script += opcode.OP_CHECKSIG + return script -def decodeAddress(addr, chain): +def payToScriptHashScript(scriptHash): """ - decodeAddress decodes the string encoding of an address and returns - the Address if addr is a valid encoding for a known address type + payToScriptHashScript creates a new script to pay a transaction output to a + script hash. It is expected that the input is a valid hash. """ - addrLen = len(addr) - if addrLen == 66 or addrLen == 130: - # Secp256k1 pubkey as a string, handle differently. - # return newAddressSecpPubKey(ByteArray(addr), chain) - raise Exception("decode from secp256k1 pubkey string unimplemented") + script = ByteArray('') + script += opcode.OP_HASH160 + script += addData(scriptHash) + script += opcode.OP_EQUAL + return script + +def payToPubKeyScript(serializedPubKey): + """ + payToPubkeyScript creates a new script to pay a transaction output to a + public key. It is expected that the input is a valid pubkey. + """ + script = ByteArray('') + script += addData(serializedPubKey) + script += opcode.OP_CHECKSIG + return script +def decodeAddress(addr, net): + """ + DecodeAddress decodes the string encoding of an address and returns the + Address if it is a valid encoding for a known address type and is for the + provided network. + """ + # Switch on decoded length to determine the type. decoded, netID = crypto.b58CheckDecode(addr) - # regular tx nedID is PubKeyHashAddrID - if netID == chain.PubKeyHashAddrID: - return netID, decoded #newAddressPubKeyHash(decoded, chain, crypto.STEcdsaSecp256k1) - else: - raise Exception("unsupported address type") + if netID == net.PubKeyAddrID: + return crypto.newAddressPubKey(decoded, net) + elif netID == net.PubKeyHashAddrID: + return crypto.newAddressPubKeyHash(decoded, net, crypto.STEcdsaSecp256k1) + elif netID == net.PKHEdwardsAddrID: + # return NewAddressPubKeyHash(decoded, net, STEd25519) + raise Exception("Edwards signatures not implemented") + elif netID == net.PKHSchnorrAddrID: + # return NewAddressPubKeyHash(decoded, net, STSchnorrSecp256k1) + raise Exception("Schnorr signatures not implemented") + elif netID == net.ScriptHashAddrID: + return crypto.newAddressScriptHashFromHash(decoded, net) + raise Exception("unknown network ID %s" % netID) def makePayToAddrScript(addrStr, chain): - netID, pkHash = decodeAddress(addrStr, chain) - return payToAddrScript(netID, pkHash, chain) + addr = decodeAddress(addrStr, chain) + return payToAddrScript(addr) def int2octets(v, rolen): """ https://tools.ietf.org/html/rfc6979#section-2.3.3""" @@ -920,10 +1217,17 @@ def pubKeyHashToAddrs(pkHash, params): """ pubKeyHashToAddrs is a convenience function to attempt to convert the passed hash to a pay-to-pubkey-hash address housed within an address - slice. It is used to consolidate common code. + list. It is used to consolidate common code. """ - addrs = [crypto.newAddressPubKeyHash(pkHash, params, crypto.STEcdsaSecp256k1)] - return addrs + return [crypto.newAddressPubKeyHash(pkHash, params, crypto.STEcdsaSecp256k1)] + +def scriptHashToAddrs(scriptHash, params): + """ + scriptHashToAddrs is a convenience function to attempt to convert the passed + hash to a pay-to-script-hash address housed within an address list. It is + used to consolidate common code. + """ + return [crypto.newAddressScriptHashFromHash(scriptHash, params)] def extractPkScriptAddrs(version, pkScript, chainParams): """ @@ -941,10 +1245,70 @@ def extractPkScriptAddrs(version, pkScript, chainParams): # Check for pay-to-pubkey-hash script. pkHash = extractPubKeyHash(pkScript) - if pkHash != None: + if pkHash: return PubKeyHashTy, pubKeyHashToAddrs(pkHash, chainParams), 1 + + # Check for pay-to-script-hash. + scriptHash = extractScriptHash(pkScript) + if scriptHash: + return ScriptHashTy, scriptHashToAddrs(scriptHash, chainParams), 1 + + # Check for pay-to-pubkey script. + data = extractPubKey(pkScript) + if data: + addrs = [] + pk = Curve.parsePubKey(data) + addrs = [crypto.AddressSecpPubKey(pk.serializeCompressed(), chainParams)] + return PubKeyTy, addrs, 1 + + # Check for multi-signature script. + details = extractMultisigScriptDetails(version, pkScript, True) + if details.valid: + # Convert the public keys while skipping any that are invalid. + addrs = [] + for encodedPK in details.pubKeys: + pk = Curve.parsePubKey(encodedPK) + addrs.append(crypto.AddressSecpPubKey(pk.serializeCompressed(), chainParams)) + return MultiSigTy, addrs, details.requiredSigs + + # Check for stake submission script. Only stake-submission-tagged + # pay-to-pubkey-hash and pay-to-script-hash are allowed. + pkHash = extractStakePubKeyHash(pkScript, opcode.OP_SSTX) + if pkHash: + return StakeSubmissionTy, pubKeyHashToAddrs(hash, chainParams), 1 + scriptHash = extractStakeScriptHash(pkScript, opcode.OP_SSTX) + if scriptHash: + return StakeSubmissionTy, scriptHashToAddrs(hash, chainParams), 1 + + # Check for stake generation script. Only stake-generation-tagged + # pay-to-pubkey-hash and pay-to-script-hash are allowed. + pkHash = extractStakePubKeyHash(pkScript, opcode.OP_SSGEN) + if pkHash: + return StakeGenTy, pubKeyHashToAddrs(pkHash, chainParams), 1 + scriptHash = extractStakeScriptHash(pkScript, opcode.OP_SSGEN) + if scriptHash: + return StakeGenTy, scriptHashToAddrs(scriptHash, chainParams), 1 + + # Check for stake revocation script. Only stake-revocation-tagged + # pay-to-pubkey-hash and pay-to-script-hash are allowed. + pkHash = extractStakePubKeyHash(pkScript, opcode.OP_SSRTX) + if pkHash: + return StakeRevocationTy, pubKeyHashToAddrs(pkHash, chainParams), 1 + scriptHash = extractStakeScriptHash(pkScript, opcode.OP_SSRTX) + if scriptHash: + return StakeRevocationTy, scriptHashToAddrs(scriptHash, chainParams), 1 + + # Check for stake change script. Only stake-change-tagged + # pay-to-pubkey-hash and pay-to-script-hash are allowed. + pkHash = extractStakePubKeyHash(pkScript, opcode.OP_SSTXCHANGE) + if pkHash: + return StakeSubChangeTy, pubKeyHashToAddrs(pkHash, chainParams), 1 + scriptHash = extractStakeScriptHash(pkScript, opcode.OP_SSTXCHANGE) + if scriptHash: + return StakeSubChangeTy, scriptHashToAddrs(scriptHash, chainParams), 1 + # EVERYTHING AFTER TIHS IS UN-IMPLEMENTED - raise Exception("Not a pay-to-pubkey-hash script") + raise Exception("unsupported script") def sign(privKey, chainParams, tx, idx, subScript, hashType, sigType): scriptClass, addresses, nrequired = extractPkScriptAddrs(DefaultScriptVersion, subScript, chainParams) @@ -1281,7 +1645,6 @@ def test_script_tokenizer(self): opcodeNum = 0 while tokenizer.next(): # Ensure Next never returns true when there is an error set. - # print("--test_expected: %s" % repr(test_expected)) self.assertIs(tokenizer.err, None, msg="%s: Next returned true when tokenizer has err: %r" % (test_name, tokenizer.err)) # Ensure the test data expects a token to be parsed. @@ -1329,8 +1692,8 @@ def test_sign_tx(self): ) signatureSuites = ( crypto.STEcdsaSecp256k1, - # dcrec.STEd25519, - # dcrec.STSchnorrSecp256k1, + # crypto.STEd25519, + # crypto.STSchnorrSecp256k1, ) testValueIn = 12345 @@ -1436,4 +1799,527 @@ def test_sign_tx(self): sigScript = signTxOutput(privKey, testingParams, tx, idx, pkScript, hashType, None, suite) self.assertEqual(sigScript, ByteArray(sigStr), msg="%d:%d:%d" % (hashType, idx, suite)) - return \ No newline at end of file + return + def test_addresses(self): + from tinydecred.pydecred import mainnet, testnet + from base58 import b58decode + class test: + def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAddress=None, f=None, net=None): + self.name = name + self.addr = addr + self.saddr = saddr + self.encoded = encoded + self.valid = valid + self.scriptAddress = scriptAddress + self.f = f + self.net = net + + addrPKH = crypto.newAddressPubKeyHash + addrSH = crypto.newAddressScriptHash + addrSHH = crypto.newAddressScriptHashFromHash + addrPK = crypto.AddressSecpPubKey + + tests = [] + # Positive P2PKH tests. + tests.append(test( + name = "mainnet p2pkh", + addr = "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", + encoded = "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", + valid = True, + scriptAddress = ByteArray("2789d58cfa0957d206f025c2af056fc8a77cebb0"), + f = lambda: addrPKH( + ByteArray("2789d58cfa0957d206f025c2af056fc8a77cebb0"), + mainnet, + crypto.STEcdsaSecp256k1, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pkh 2", + addr = "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9", + encoded = "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9", + valid = True, + scriptAddress = ByteArray("229ebac30efd6a69eec9c1a48e048b7c975c25f2"), + f = lambda: addrPKH( + ByteArray("229ebac30efd6a69eec9c1a48e048b7c975c25f2"), + mainnet, + crypto.STEcdsaSecp256k1, + ), + net = mainnet, + )) + tests.append(test( + name = "testnet p2pkh", + addr = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h", + encoded = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h", + valid = True, + scriptAddress = ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), + f = lambda: addrPKH( + ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), + testnet, + crypto.STEcdsaSecp256k1 + ), + net = testnet, + )) + + # Negative P2PKH tests. + tests.append(test( + name = "p2pkh wrong hash length", + addr = "", + valid = False, + f = lambda: addrPKH( + ByteArray("000ef030107fd26e0b6bf40512bca2ceb1dd80adaa"), + mainnet, + crypto.STEcdsaSecp256k1, + ), + )) + tests.append(test( + name = "p2pkh bad checksum", + addr = "TsmWaPM77WSyA3aiQ2Q1KnwGDVWvEkhip23", + valid = False, + net = testnet, + )) + + # Positive P2SH tests. + tests.append(test( + # Taken from transactions: + # output: 3c9018e8d5615c306d72397f8f5eef44308c98fb576a88e030c25456b4f3a7ac + # input: 837dea37ddc8b1e3ce646f1a656e79bbd8cc7f558ac56a169626d649ebe2a3ba. + name = "mainnet p2sh", + addr = "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", + encoded = "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", + valid = True, + scriptAddress = ByteArray("f0b4e85100aee1a996f22915eb3c3f764d53779a"), + f = lambda: addrSH( + ByteArray("512103aa43f0a6c15730d886cc1f0342046d20175483d90d7ccb657f90c489111d794c51ae"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + # Taken from transactions: + # output: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d + # input: (not yet redeemed at time test was written) + name = "mainnet p2sh 2", + addr = "DcqgK4N4Ccucu2Sq4VDAdu4wH4LASLhzLVp", + encoded = "DcqgK4N4Ccucu2Sq4VDAdu4wH4LASLhzLVp", + valid = True, + scriptAddress = ByteArray("c7da5095683436f4435fc4e7163dcafda1a2d007"), + f = lambda: addrSHH( + ByteArray("c7da5095683436f4435fc4e7163dcafda1a2d007"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + # Taken from bitcoind base58_keys_valid. + name = "testnet p2sh", + addr = "TccWLgcquqvwrfBocq5mcK5kBiyw8MvyvCi", + encoded = "TccWLgcquqvwrfBocq5mcK5kBiyw8MvyvCi", + valid = True, + scriptAddress = ByteArray("36c1ca10a8a6a4b5d4204ac970853979903aa284"), + f = lambda: addrSHH( + ByteArray("36c1ca10a8a6a4b5d4204ac970853979903aa284"), + testnet, + ), + net = testnet, + )) + + # Negative P2SH tests. + tests.append(test( + name = "p2sh wrong hash length", + addr = "", + valid = False, + f = lambda: addrSHH( + ByteArray("00f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), + mainnet, + ), + net = mainnet, + )) + + # Positive P2PK tests. + tests.append(test( + name = "mainnet p2pk compressed (0x02)", + addr = "DsT4FDqBKYG1Xr8aGrT1rKP3kiv6TZ5K5th", + encoded = "DsT4FDqBKYG1Xr8aGrT1rKP3kiv6TZ5K5th", + valid = True, + scriptAddress = ByteArray("028f53838b7639563f27c94845549a41e5146bcd52e7fef0ea6da143a02b0fe2ed"), + f = lambda: addrPK( + ByteArray("028f53838b7639563f27c94845549a41e5146bcd52e7fef0ea6da143a02b0fe2ed"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pk compressed (0x03)", + addr = "DsfiE2y23CGwKNxSGjbfPGeEW4xw1tamZdc", + encoded = "DsfiE2y23CGwKNxSGjbfPGeEW4xw1tamZdc", + valid = True, + scriptAddress = ByteArray("03e925aafc1edd44e7c7f1ea4fb7d265dc672f204c3d0c81930389c10b81fb75de"), + f = lambda: addrPK( + ByteArray("03e925aafc1edd44e7c7f1ea4fb7d265dc672f204c3d0c81930389c10b81fb75de"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pk uncompressed (0x04)", + addr = "DkM3EyZ546GghVSkvzb6J47PvGDyntqiDtFgipQhNj78Xm2mUYRpf", + encoded = "DsfFjaADsV8c5oHWx85ZqfxCZy74K8RFuhK", + valid = True, + saddr = "0264c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0", + scriptAddress = ByteArray("0464c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), + f = lambda: addrPK( + ByteArray("0464c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "testnet p2pk compressed (0x02)", + addr = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", + encoded = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", + valid = True, + scriptAddress = ByteArray("026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e"), + f = lambda: addrPK( + ByteArray("026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e"), + testnet, + ), + net = testnet, + )) + tests.append(test( + name = "testnet p2pk compressed (0x03)", + addr = "TsWZ1EzypJfMwBKAEDYKuyHRGctqGAxMje2", + encoded = "TsWZ1EzypJfMwBKAEDYKuyHRGctqGAxMje2", + valid = True, + scriptAddress = ByteArray("030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af5"), + f = lambda: addrPK( + ByteArray("030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af5"), + testnet, + ), + net = testnet, + )) + tests.append(test( + name = "testnet p2pk uncompressed (0x04)", + addr = "TkKmMiY5iDh4U3KkSopYgkU1AzhAcQZiSoVhYhFymZHGMi9LM9Fdt", + encoded = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", + valid = True, + saddr = "026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e", + scriptAddress = ByteArray("046a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), + f = lambda: addrPK( + ByteArray("046a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), + testnet, + ), + net = testnet, + )) + + # Negative P2PK tests. + tests.append(test( + name = "mainnet p2pk hybrid (0x06)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("0664c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pk hybrid (0x07)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("07348d8aeb4253ca52456fe5da94ab1263bfee16bb8192497f666389ca964f84798375129d7958843b14258b905dc94faed324dd8a9d67ffac8cc0a85be84bac5d"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "testnet p2pk hybrid (0x06)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("066a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), + testnet, + ), + net = testnet, + )) + tests.append(test( + name = "testnet p2pk hybrid (0x07)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("07edd40747de905a9becb14987a1a26c1adbd617c45e1583c142a635bfda9493dfa1c6d36735974965fe7b861e7f6fcc087dc7fe47380fa8bde0d9c322d53c0e89"), + testnet, + ), + net = testnet, + )) + + for test in tests: + # Decode addr and compare error against valid. + err = None + try: + decoded = decodeAddress(test.addr, test.net) + except Exception as e: + err = e + self.assertEqual(err == None, test.valid, "%s error: %s" % (test.name, err)) + + if err == None: + # Ensure the stringer returns the same address as theoriginal. + self.assertEqual(test.addr, decoded.string(), test.name) + + # Encode again and compare against the original. + encoded = decoded.address() + self.assertEqual(test.encoded, encoded) + + # Perform type-specific calculations. + if isinstance(decoded, crypto.AddressPubKeyHash): + d = ByteArray(b58decode(encoded)) + saddr = d[2 : 2+crypto.RIPEMD160_SIZE] + + elif isinstance(decoded, crypto.AddressScriptHash): + d = ByteArray(b58decode(encoded)) + saddr = d[2 : 2+crypto.RIPEMD160_SIZE] + + elif isinstance(decoded, crypto.AddressSecpPubKey): + # Ignore the error here since the script + # address is checked below. + try: + saddr = ByteArray(decoded.string()) + except Exception: + saddr = test.saddr + + elif isinstance(decoded, crypto.AddressEdwardsPubKey): + # Ignore the error here since the script + # address is checked below. + # saddr = ByteArray(decoded.String()) + self.fail("Edwards sigs unsupported") + + elif isinstance(decoded, crypto.AddressSecSchnorrPubKey): + # Ignore the error here since the script + # address is checked below. + # saddr = ByteArray(decoded.String()) + self.fail("Schnorr sigs unsupported") + + # Check script address, as well as the Hash160 method for P2PKH and + # P2SH addresses. + self.assertEqual(saddr, decoded.scriptAddress(), test.name) + + if isinstance(decoded, crypto.AddressPubKeyHash): + self.assertEqual(decoded.pkHash, saddr) + + if isinstance(decoded, crypto.AddressScriptHash): + self.assertEqual(decoded.hash160(), saddr) + + if not test.valid: + # If address is invalid, but a creation function exists, + # verify that it returns a nil addr and non-nil error. + if test.f != None: + try: + test.f() + self.fail("%s: address is invalid but creating new address succeeded" % test.name) + except Exception: + pass + continue + + # Valid test, compare address created with f against expected result. + try: + addr = test.f() + except Exception as e: + self.fail("%s: address is valid but creating new address failed with error %s", test.name, e) + self.assertEqual(addr.scriptAddress(), test.scriptAddress, test.name) + + def test_extract_script_addrs(self): + from tinydecred.pydecred import mainnet + scriptVersion = 0 + tests = [] + def pkAddr(b): + addr = crypto.AddressSecpPubKey(b, mainnet) + # force the format to compressed, as per golang tests. + addr.pubkeyFormat = crypto.PKFCompressed + return addr + + class test: + def __init__(self, name="", script=b'', addrs=None, reqSigs=-1, scriptClass=-1, exception=None): + self.name = name + self.script = script + self.addrs = addrs if addrs else [] + self.reqSigs = reqSigs + self.scriptClass = scriptClass + self.exception = exception + tests.append(test( + name = "standard p2pk with compressed pubkey (0x02)", + script = ByteArray("2102192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4ac"), + addrs = [pkAddr(ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"))], + reqSigs = 1, + scriptClass = PubKeyTy, + )) + tests.append(test( + name = "standard p2pk with uncompressed pubkey (0x04)", + script = ByteArray("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" + "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), + addrs = [ + pkAddr(ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482eca" + "d7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3")), + ], + reqSigs = 1, + scriptClass = PubKeyTy, + )) + tests.append(test( + name = "standard p2pk with compressed pubkey (0x03)", + script = ByteArray("2103b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65ac"), + addrs = [pkAddr(ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"))], + reqSigs = 1, + scriptClass = PubKeyTy, + )) + tests.append(test( + name = "2nd standard p2pk with uncompressed pubkey (0x04)", + script = ByteArray("4104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782" + "eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac"), + addrs = [ + pkAddr(ByteArray("04b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2" + "c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b")), + ], + reqSigs = 1, + scriptClass = PubKeyTy, + )) + tests.append(test( + name = "standard p2pkh", + script = ByteArray("76a914ad06dd6ddee55cbca9a9e3713bd7587509a3056488ac"), + addrs = [crypto.newAddressPubKeyHash(ByteArray("ad06dd6ddee55cbca9a9e3713bd7587509a30564"), mainnet, crypto.STEcdsaSecp256k1)], + reqSigs = 1, + scriptClass = PubKeyHashTy, + )) + tests.append(test( + name = "standard p2sh", + script = ByteArray("a91463bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb87"), + addrs = [crypto.newAddressScriptHashFromHash(ByteArray("63bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb"), mainnet)], + reqSigs = 1, + scriptClass = ScriptHashTy, + )) + # from real tx 60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1, vout 0 + tests.append(test( + name = "standard 1 of 2 multisig", + script = ByteArray("514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a47" + "3e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4410461cbdcc5409fb4b4d42b51d3338" + "1354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af52ae"), + addrs = [ + pkAddr(ByteArray("04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a" + "1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4")), + pkAddr(ByteArray("0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34b" + "fa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af")), + ], + reqSigs = 1, + scriptClass = MultiSigTy, + )) + # from real tx d646f82bd5fbdb94a36872ce460f97662b80c3050ad3209bef9d1e398ea277ab, vin 1 + tests.append(test( + name = "standard 2 of 3 multisig", + script = ByteArray("524104cb9c3c222c5f7a7d3b9bd152f363a0b6d54c9eb312c4d4f9af1e8551b6c421a6a4ab0e2" + "9105f24de20ff463c1c91fcf3bf662cdde4783d4799f787cb7c08869b4104ccc588420deeebea22a7e900cc8" + "b68620d2212c374604e3487ca08f1ff3ae12bdc639514d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e" + "71e610b036aa24104ab47ad1939edcb3db65f7fedea62bbf781c5410d3f22a7a3a56ffefb2238af8627363bd" + "f2ed97c1f89784a1aecdb43384f11d2acc64443c7fc299cef0400421a53ae"), + addrs = [ + pkAddr(ByteArray("04cb9c3c222c5f7a7d3b9bd152f363a0b6d54c9eb312c4d4f9af" + "1e8551b6c421a6a4ab0e29105f24de20ff463c1c91fcf3bf662cdde4783d4799f787cb7c08869b")), + pkAddr(ByteArray("04ccc588420deeebea22a7e900cc8b68620d2212c374604e3487" + "ca08f1ff3ae12bdc639514d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e71e610b036aa2")), + pkAddr(ByteArray("04ab47ad1939edcb3db65f7fedea62bbf781c5410d3f22a7a3a5" + "6ffefb2238af8627363bdf2ed97c1f89784a1aecdb43384f11d2acc64443c7fc299cef0400421a")), + ], + reqSigs = 2, + scriptClass = MultiSigTy, + )) + + # The below are nonstandard script due to things such as + # invalid pubkeys, failure to parse, and not being of a + # standard form. + + tests.append(test( + name = "p2pk with uncompressed pk missing OP_CHECKSIG", + script = ByteArray("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" + "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + addrs = [], + exception = "unsupported script", + )) + tests.append(test( + name = "valid signature from a sigscript - no addresses", + script = ByteArray("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd41022" + "0181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), + addrs = [], + exception = "unsupported script", + )) + # Note the technically the pubkey is the second item on the + # stack, but since the address extraction intentionally only + # works with standard PkScripts, this should not return any + # addresses. + tests.append(test( + name = "valid sigscript to redeem p2pk - no addresses", + script = ByteArray("493046022100ddc69738bf2336318e4e041a5a77f305da87428ab1606f023260017854350ddc0" + "22100817af09d2eec36862d16009852b7e3a0f6dd76598290b7834e1453660367e07a014104cd4240c198e12" + "523b6f9cb9f5bed06de1ba37e96a1bbd13745fcf9d11c25b1dff9a519675d198804ba9962d3eca2d5937d58e5a75a71042d40388a4d307f887d"), + addrs = [], + reqSigs = 0, + exception = "unsupported script", + )) + # adapted from btc: + # tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 0 + # invalid public keys + tests.append(test( + name = "1 of 3 multisig with invalid pubkeys", + script = ByteArray("5141042200007353455857696b696c65616b73204361626c6567617465204261636b75700a0a6" + "361626c65676174652d3230313031323034313831312e377a0a0a446f41046e6c6f61642074686520666f6c6" + "c6f77696e67207472616e73616374696f6e732077697468205361746f736869204e616b616d6f746f2773206" + "46f776e6c6f61410420746f6f6c2077686963680a63616e20626520666f756e6420696e207472616e7361637" + "4696f6e2036633533636439383731313965663739376435616463636453ae"), + addrs = [], + exception = "isn't on secp256k1 curve", + )) + # adapted from btc: + # tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 44 + # invalid public keys + tests.append(test( + name = "1 of 3 multisig with invalid pubkeys 2", + script = ByteArray("514104633365633235396337346461636536666430383862343463656638630a6336366263313" + "9393663386239346133383131623336353631386665316539623162354104636163636539393361333938386" + "134363966636336643664616266640a323636336366613963663463303363363039633539336333653931666" + "56465373032392102323364643432643235363339643338613663663530616234636434340a00000053ae"), + addrs = [], + exception = "isn't on secp256k1 curve", + )) + tests.append(test( + name = "empty script", + script = ByteArray(b''), + addrs = [], + reqSigs = 0, + exception = "unsupported script", + )) + tests.append(test( + name = "script that does not parse", + script = ByteArray([opcode.OP_DATA_45]), + addrs = [], + reqSigs = 0, + exception = "unsupported script", + )) + + def checkAddrs(a, b, name): + if len(a) != len(b): + t.fail("extracted address length mismatch. expected %d, got %d" % (len(a), len(b))) + for av, bv in zip(a, b): + if av.scriptAddress() != bv.scriptAddress(): + self.fail("scriptAddress mismatch. expected %s, got %s (%s)" % + (av.scriptAddress().hex(), bv.scriptAddress().hex(), name)) + + for i, t in enumerate(tests): + try: + scriptClass, addrs, reqSigs = extractPkScriptAddrs(scriptVersion, t.script, mainnet) + except Exception as e: + if t.exception and t.exception in str(e): + continue + self.fail("extractPkScriptAddrs #%d (%s): %s" % (i, t.name, e)) + + self.assertEqual(scriptClass, t.scriptClass, t.name) + + self.assertEqual(reqSigs, t.reqSigs, t.name) + + checkAddrs(t.addrs, addrs, t.name) From 2af704bf84202f4ba31f91fad8588e92da7838da Mon Sep 17 00:00:00 2001 From: buck54321 Date: Thu, 12 Sep 2019 05:57:31 -0500 Subject: [PATCH 02/12] add vsp and ticket purchase support. add more txscript tests. Also moves pydecred tests to dedicated test file so they aren't evaluated on import. --- api.py | 6 +- crypto/crypto.py | 10 +- crypto/secp256k1/curve.py | 5 +- pydecred/calc.py | 210 +++- pydecred/dcrdata.py | 548 ++++----- pydecred/mainnet.py | 4 +- pydecred/simnet.py | 1 + pydecred/stakepool.py | 167 +++ pydecred/test-data/sighash.json | 144 +++ pydecred/testnet.py | 4 +- pydecred/tests.py | 1977 +++++++++++++++++++++++++++++++ pydecred/txscript.py | 1865 +++++++++++++++-------------- pydecred/wire/msgtx.py | 8 +- util/http.py | 41 + wallet.py | 6 +- 15 files changed, 3777 insertions(+), 1219 deletions(-) create mode 100644 pydecred/stakepool.py create mode 100644 pydecred/test-data/sighash.json create mode 100644 pydecred/tests.py create mode 100644 util/http.py diff --git a/api.py b/api.py index fa963b11..cfdefba6 100644 --- a/api.py +++ b/api.py @@ -364,11 +364,11 @@ def priv(self, addr): PrivateKey: Private key. """ raise Unimplemented("KeySource not implemented") - def change(self): + def internal(self): """ - Get a new change address. + Get a new internal address. Returns: str: A new base-58 encoded change address. """ - raise Unimplemented("change not implemented") + raise Unimplemented("internal not implemented") diff --git a/crypto/crypto.py b/crypto/crypto.py index f385d09b..543e31af 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -93,9 +93,9 @@ def string(self): def address(self): return self.string() def scriptAddress(self): - return self.pkHash + return self.pkHash.copy() def hash160(self): - return self.pkHash + return self.pkHash.copy() class AddressSecpPubKey: """ @@ -187,9 +187,9 @@ def address(self): """ return self.string() def scriptAddress(self): - return self.scriptHash + return self.scriptHash.copy() def hash160(self): - return self.scriptHash + return self.scriptHash.copy() def hmacDigest(key, msg, digestmod=hashlib.sha512): """ @@ -341,7 +341,7 @@ def b58CheckDecode(s): cksum =decoded[len(decoded)-4:] if checksum(decoded[:len(decoded)-4]) != cksum: raise Exception("checksum error") - payload = decoded[2 : len(decoded)-4] + payload = ByteArray(decoded[2 : len(decoded)-4]) return payload, version def newAddressPubKey(decoded, net): diff --git a/crypto/secp256k1/curve.py b/crypto/secp256k1/curve.py index c933304d..33d827ff 100644 --- a/crypto/secp256k1/curve.py +++ b/crypto/secp256k1/curve.py @@ -132,13 +132,14 @@ def randFieldElement(): # c elliptic.Curve, rand io.Reader) (k *big.Int, err err k = k + 1 return k -def generateKey(): # (*PrivateKey, error) { +def generateKey(): """ generateKey generates a public and private key pair. """ k = randFieldElement() x, y = curve.scalarBaseMult(k) - return PrivateKey(curve, k, x, y) + b = ByteArray(k, length=32) + return PrivateKey(curve, b, x, y) class KoblitzCurve: def __init__(self, P, N, B, Gx, Gy, BitSize, H, q, byteSize, lamda, beta, a1, b1, a2, b2): diff --git a/pydecred/calc.py b/pydecred/calc.py index 0df2efc9..b35216bc 100644 --- a/pydecred/calc.py +++ b/pydecred/calc.py @@ -5,11 +5,18 @@ Some network math. """ import math +import bisect from tinydecred.util import helpers from tinydecred.pydecred import constants as C, mainnet NETWORK = mainnet -MODEL_DEVICE = helpers.makeDevice(**C.MODEL_DEVICE) +MODEL_DEVICE = { + "model": "INNOSILICON D9 Miner", + "price": 1699, + "release": "2018-04-18", + "hashrate": 2.1e12, + "power": 900 +} def makeDevice(model=None, price=None, hashrate=None, power=None, release=None, source=None): """ @@ -328,4 +335,203 @@ def minimizeAy(*args, grains=100, **kwargs): if A.attackCost < lowest: lowest = A.attackCost result = A - return result \ No newline at end of file + return result + +class SubsidyCache(object): + """ + SubsidyCache provides efficient access to consensus-critical subsidy + calculations for blocks and votes, including the max potential subsidy for + given block heights, the proportional proof-of-work subsidy, the proportional + proof of stake per-vote subsidy, and the proportional treasury subsidy. + + It makes using of caching to avoid repeated calculations. + """ + # The following fields are protected by the mtx mutex. + # + # cache houses the cached subsidies keyed by reduction interval. + # + # cachedIntervals contains an ordered list of all cached intervals. It is + # used to efficiently track sparsely cached intervals with O(log N) + # discovery of a prior cached interval. + def __init__(self, params): + self.cache = {0: params.BaseSubsidy} + self.cachedIntervals = [0] + + # params stores the subsidy parameters to use during subsidy calculation. + self.params = params + + # These fields house values calculated from the parameters in order to + # avoid repeated calculation. + # + # minVotesRequired is the minimum number of votes required for a block to + # be consider valid by consensus. + # + # totalProportions is the sum of the PoW, PoS, and Treasury proportions. + self.minVotesRequired = (params.TicketsPerBlock // 2) + 1 + self.totalProportions = (params.WorkRewardProportion + + params.StakeRewardProportion + params.BlockTaxProportion) + + def calcBlockSubsidy(self, height): + """ + calcBlockSubsidy returns the max potential subsidy for a block at the + provided height. This value is reduced over time based on the height and + then split proportionally between PoW, PoS, and the Treasury. + """ + # Negative block heights are invalid and produce no subsidy. + # Block 0 is the genesis block and produces no subsidy. + # Block 1 subsidy is special as it is used for initial token distribution. + if height <= 0: + return 0 + elif height == 1: + return self.params.BlockOneSubsidy + + # Calculate the reduction interval associated with the requested height and + # attempt to look it up in cache. When it's not in the cache, look up the + # latest cached interval and subsidy while the mutex is still held for use + # below. + reqInterval = height // self.params.SubsidyReductionInterval + if reqInterval in self.cache: + return self.cache[reqInterval] + + lastCachedInterval = self.cachedIntervals[len(self.cachedIntervals)-1] + lastCachedSubsidy = self.cache[lastCachedInterval] + + # When the requested interval is after the latest cached interval, avoid + # additional work by either determining if the subsidy is already exhausted + # at that interval or using the interval as a starting point to calculate + # and store the subsidy for the requested interval. + # + # Otherwise, the requested interval is prior to the final cached interval, + # so use a binary search to find the latest cached interval prior to the + # requested one and use it as a starting point to calculate and store the + # subsidy for the requested interval. + if reqInterval > lastCachedInterval: + # Return zero for all intervals after the subsidy reaches zero. This + # enforces an upper bound on the number of entries in the cache. + if lastCachedSubsidy == 0: + return 0 + else: + cachedIdx = bisect.bisect_left(self.cachedIntervals, reqInterval) + lastCachedInterval = self.cachedIntervals[cachedIdx-1] + lastCachedSubsidy = self.cache[lastCachedInterval] + + # Finally, calculate the subsidy by applying the appropriate number of + # reductions per the starting and requested interval. + reductionMultiplier = self.params.MulSubsidy + reductionDivisor = self.params.DivSubsidy + subsidy = lastCachedSubsidy + neededIntervals = reqInterval - lastCachedInterval + for i in range(neededIntervals): + subsidy *= reductionMultiplier + subsidy = subsidy // reductionDivisor + + # Stop once no further reduction is possible. This ensures a bounded + # computation for large requested intervals and that all future + # requests for intervals at or after the final reduction interval + # return 0 without recalculating. + if subsidy == 0: + reqInterval = lastCachedInterval + i + 1 + break + + # Update the cache for the requested interval or the interval in which the + # subsidy became zero when applicable. The cached intervals are stored in + # a map for O(1) lookup and also tracked via a sorted array to support the + # binary searches for efficient sparse interval query support. + self.cache[reqInterval] = subsidy + + bisect.insort_left(self.cachedIntervals, reqInterval) + return subsidy + + def calcWorkSubsidy(self, height, voters): + # The first block has special subsidy rules. + if height == 1: + return self.params.BlockOneSubsidy + + # The subsidy is zero if there are not enough voters once voting begins. A + # block without enough voters will fail to validate anyway. + stakeValidationHeight = self.params.StakeValidationHeight + if height >= stakeValidationHeight and voters < self.minVotesRequired: + return 0 + + # Calculate the full block subsidy and reduce it according to the PoW + # proportion. + subsidy = self.calcBlockSubsidy(height) + subsidy *= self.params.WorkRewardProportion + subsidy = subsidy // self.totalProportions + + # Ignore any potential subsidy reductions due to the number of votes prior + # to the point voting begins. + if height < stakeValidationHeight: + return subsidy + + # Adjust for the number of voters. + return (voters * subsidy) // self.params.TicketsPerBlock + + def calcStakeVoteSubsidy(self, height): + """ + CalcStakeVoteSubsidy returns the subsidy for a single stake vote for a block. + It is calculated as a proportion of the total subsidy and max potential + number of votes per block. + + Unlike the Proof-of-Work and Treasury subsidies, the subsidy that votes + receive is not reduced when a block contains less than the maximum number of + votes. Consequently, this does not accept the number of votes. However, it + is important to note that blocks that do not receive the minimum required + number of votes for a block to be valid by consensus won't actually produce + any vote subsidy either since they are invalid. + + This function is safe for concurrent access. + """ + # Votes have no subsidy prior to the point voting begins. The minus one + # accounts for the fact that vote subsidy are, unfortunately, based on the + # height that is being voted on as opposed to the block in which they are + # included. + if height < self.params.StakeValidationHeight-1: + return 0 + + # Calculate the full block subsidy and reduce it according to the stake + # proportion. Then divide it by the number of votes per block to arrive + # at the amount per vote. + subsidy = self.calcBlockSubsidy(height) + proportions = self.totalProportions + subsidy *= self.params.StakeRewardProportion + subsidy = subsidy // (proportions * self.params.TicketsPerBlock) + + return subsidy + + def calcTreasurySubsidy(self, height, voters): + """ + calcTreasurySubsidy returns the subsidy required to go to the treasury for + a block. It is calculated as a proportion of the total subsidy and further + reduced proportionally depending on the number of votes once the height at + which voting begins has been reached. + + Note that passing a number of voters fewer than the minimum required for a + block to be valid by consensus along with a height greater than or equal to + the height at which voting begins will return zero. + + This function is safe for concurrent access. + """ + # The first two blocks have special subsidy rules. + if height <= 1: + return 0 + + # The subsidy is zero if there are not enough voters once voting begins. A + # block without enough voters will fail to validate anyway. + stakeValidationHeight = self.params.StakeValidationHeight + if height >= stakeValidationHeight and voters < self.minVotesRequired: + return 0 + + # Calculate the full block subsidy and reduce it according to the treasury + # proportion. + subsidy = self.calcBlockSubsidy(height) + subsidy *= self.params.BlockTaxProportion + subsidy = subsidy // self.totalProportions + + # Ignore any potential subsidy reductions due to the number of votes prior + # to the point voting begins. + if height < stakeValidationHeight: + return subsidy + + # Adjust for the number of voters. + return (voters * subsidy) // self.params.TicketsPerBlock diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 0fbe248a..8799364b 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -8,20 +8,17 @@ import time import calendar -import unittest import threading import ssl import sys import select import atexit -import os import websocket -from tempfile import TemporaryDirectory from tinydecred.util import tinyjson, helpers, database, http -from tinydecred.crypto import opcode, crypto +from tinydecred.crypto import crypto from tinydecred.crypto.bytearray import ByteArray from tinydecred.api import InsufficientFundsError -from tinydecred.pydecred import txscript, simnet +from tinydecred.pydecred import txscript, calc from tinydecred.pydecred.wire import msgtx, wire, msgblock from tinydecred.util.database import KeyValueDatabase @@ -33,52 +30,6 @@ "User-Agent": "tinydecred/%s" % VERSION, "Content-Type":"application/json; charset=utf-8", } -# Many of these constants were pulled from the dcrd, and are left as mixed case -# to maintain reference. - -# DefaultRelayFeePerKb is the default minimum relay fee policy for a mempool. -DefaultRelayFeePerKb = 1e4 - -# AtomsPerCent is the number of atomic units in one coin cent. -AtomsPerCent = 1e6 - -# AtomsPerCoin is the number of atomic units in one coin. -AtomsPerCoin = 1e8 - -# MaxAmount is the maximum transaction amount allowed in atoms. -# Decred - Changeme for release -MaxAmount = 21e6 * AtomsPerCoin - -opNonstake = opcode.OP_NOP10 - -# RedeemP2PKHSigScriptSize is the worst case (largest) serialize size -# of a transaction input script that redeems a compressed P2PKH output. -# It is calculated as: -# -# - OP_DATA_73 -# - 72 bytes DER signature + 1 byte sighash -# - OP_DATA_33 -# - 33 bytes serialized compressed pubkey -RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 - -# generatedTxVersion is the version of the transaction being generated. -# It is defined as a constant here rather than using the wire.TxVersion -# constant since a change in the transaction version will potentially -# require changes to the generated transaction. Thus, using the wire -# constant for the generated transaction version could allow creation -# of invalid transactions for the updated version. -generatedTxVersion = 1 - -# P2PKHPkScriptSize is the size of a transaction output script that -# pays to a compressed pubkey hash. It is calculated as: - -# - OP_DUP -# - OP_HASH160 -# - OP_DATA_20 -# - 20 bytes pubkey hash -# - OP_EQUALVERIFY -# - OP_CHECKSIG -P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 formatTraceback = helpers.formatTraceback @@ -390,9 +341,8 @@ def __init__(self, name, message): class UTXO(object): """ - The UTXO is the only class fully implemented by the wallet API. BlockChains - must know how to create and parse UTXO objects and fill fields as required - by the Wallet. + The UTXO is part of the wallet API. BlockChains create and parse UTXO + objects and fill fields as required by the Wallet. """ def __init__(self, address, txid, vout, ts=None, scriptPubKey=None, height=-1, amount=0, satoshis=0, maturity=None): @@ -405,6 +355,7 @@ def __init__(self, address, txid, vout, ts=None, scriptPubKey=None, self.amount = amount self.satoshis = satoshis self.maturity = maturity + def __tojson__(self): return { "address": self.address, @@ -449,7 +400,7 @@ def makeKey(txid, vout): tinyjson.register(UTXO) -def makeOutputs(pairs, chain): #pairs map[string]dcrutil.Amount, chainParams *chaincfg.Params) ([]*wire.TxOut, error) { +def makeOutputs(pairs, chain): """ makeOutputs creates a slice of transaction outputs from a pair of address strings to amounts. This is used to create the outputs to include in newly @@ -491,13 +442,13 @@ def checkOutput(output, fee): """ if output.value < 0: raise Exception("transaction output amount is negative") - if output.value > MaxAmount: + if output.value > txscript.MaxAmount: raise Exception("transaction output amount exceeds maximum value") if output.value == 0: raise Exception("zero-value output") # need to implement these - # if IsDustOutput(output, fee): - # raise Exception("policy violation: transaction output is dust") + if txscript.isDustOutput(output, fee): + raise Exception("policy violation: transaction output is dust") def hashFromHex(s): """ @@ -520,180 +471,6 @@ def hexFromHash(h): """ return reversed(h).hex() -def getP2PKHOpCode(pkScript): - """ - getP2PKHOpCode returns opNonstake for non-stake transactions, or - the stake op code tag for stake transactions. - - Args: - pkScript (ByteArray): The pubkey script. - - Returns: - int: The opcode tag for the script types parsed from the script. - """ - scriptClass = txscript.getScriptClass(txscript.DefaultScriptVersion, pkScript) - if scriptClass == txscript.NonStandardTy: - raise Exception("unknown script class") - if scriptClass == txscript.StakeSubmissionTy: - return opcode.OP_SSTX - elif scriptClass == txscript.StakeGenTy: - return opcode.OP_SSGEN - elif scriptClass == txscript.StakeRevocationTy: - return opcode.OP_SSRTX - elif scriptClass == txscript.StakeSubChangeTy: - return opcode.OP_SSTXCHANGE - # this should always be the case for now. - return opNonstake - -def spendScriptSize(pkScript): - # Unspent credits are currently expected to be either P2PKH or - # P2PK, P2PKH/P2SH nested in a revocation/stakechange/vote output. - scriptClass = txscript.getScriptClass(txscript.DefaultScriptVersion, pkScript) - - if scriptClass == txscript.PubKeyHashTy: - return RedeemP2PKHSigScriptSize - raise Exception("unimplemented") - -def estimateInputSize(scriptSize): - """ - estimateInputSize returns the worst case serialize size estimate for a tx input - - 32 bytes previous tx - - 4 bytes output index - - 1 byte tree - - 8 bytes amount - - 4 bytes block height - - 4 bytes block index - - the compact int representation of the script size - - the supplied script size - - 4 bytes sequence - - Args: - scriptSize int: Byte-length of the script. - - Returns: - int: Estimated size of the byte-encoded transaction input. - """ - return 32 + 4 + 1 + 8 + 4 + 4 + wire.varIntSerializeSize(scriptSize) + scriptSize + 4 - -def estimateOutputSize(scriptSize): - """ - estimateOutputSize returns the worst case serialize size estimate for a tx output - - 8 bytes amount - - 2 bytes version - - the compact int representation of the script size - - the supplied script size - - Args: - scriptSize int: Byte-length of the script. - - Returns: - int: Estimated size of the byte-encoded transaction output. - """ - return 8 + 2 + wire.varIntSerializeSize(scriptSize) + scriptSize - -def sumOutputSerializeSizes(outputs): # outputs []*wire.TxOut) (serializeSize int) { - """ - sumOutputSerializeSizes sums up the serialized size of the supplied outputs. - - Args: - outputs list(TxOut): Transaction outputs. - - Returns: - int: Estimated size of the byte-encoded transaction outputs. - """ - serializeSize = 0 - for txOut in outputs: - serializeSize += txOut.serializeSize() - return serializeSize - -def estimateSerializeSize(scriptSizes, txOuts, changeScriptSize): - """ - estimateSerializeSize returns a worst case serialize size estimate for a - signed transaction that spends a number of outputs and contains each - transaction output from txOuts. The estimated size is incremented for an - additional change output if changeScriptSize is greater than 0. Passing 0 - does not add a change output. - - Args: - scriptSizes list(int): Pubkey script sizes - txOuts list(TxOut): Transaction outputs. - changeScriptSize int: Size of the change script. - - Returns: - int: Estimated size of the byte-encoded transaction outputs. - """ - # Generate and sum up the estimated sizes of the inputs. - txInsSize = 0 - for size in scriptSizes: - txInsSize += estimateInputSize(size) - - inputCount = len(scriptSizes) - outputCount = len(txOuts) - changeSize = 0 - if changeScriptSize > 0: - changeSize = estimateOutputSize(changeScriptSize) - outputCount += 1 - # 12 additional bytes are for version, locktime and expiry. - return (12 + (2 * wire.varIntSerializeSize(inputCount)) + - wire.varIntSerializeSize(outputCount) + - txInsSize + - sumOutputSerializeSizes(txOuts) + - changeSize) - -def calcMinRequiredTxRelayFee(relayFeePerKb, txSerializeSize): - """ - calcMinRequiredTxRelayFee returns the minimum transaction fee required for a - transaction with the passed serialized size to be accepted into the memory - pool and relayed. - - Args: - relayFeePerKb (float): The fee per kilobyte. - txSerializeSize int: (Size) of the byte-encoded transaction. - - Returns: - int: Fee in atoms. - """ - # Calculate the minimum fee for a transaction to be allowed into the - # mempool and relayed by scaling the base fee (which is the minimum - # free transaction relay fee). minTxRelayFee is in Atom/KB, so - # multiply by serializedSize (which is in bytes) and divide by 1000 to - # get minimum Atoms. - fee = relayFeePerKb * txSerializeSize / 1000 - - if fee == 0 and relayFeePerKb > 0: - fee = relayFeePerKb - - if fee < 0 or fee > MaxAmount: # dcrutil.MaxAmount: - fee = MaxAmount - return round(fee) - - -def isDustAmount(amount, scriptSize, relayFeePerKb): #amount dcrutil.Amount, scriptSize int, relayFeePerKb dcrutil.Amount) bool { - """ - isDustAmount determines whether a transaction output value and script length would - cause the output to be considered dust. Transactions with dust outputs are - not standard and are rejected by mempools with default policies. - - Args: - amount (int): Atoms. - scriptSize (int): Byte-size of the script. - relayFeePerKb (float): Fees paid per kilobyte. - - Returns: - bool: True if the amount is considered dust. - """ - # Calculate the total (estimated) cost to the network. This is - # calculated using the serialize size of the output plus the serial - # size of a transaction input which redeems it. The output is assumed - # to be compressed P2PKH as this is the most common script type. Use - # the average size of a compressed P2PKH redeem input (165) rather than - # the largest possible (txsizes.RedeemP2PKHInputSize). - totalSize = 8 + 2 + wire.varIntSerializeSize(scriptSize) + scriptSize + 165 - - # Dust is defined as an output value where the total cost to the network - # (output size + input size) is greater than 1/3 of the relay fee. - return amount*1000/(3*totalSize) < relayFeePerKb - class DcrdataBlockchain(object): """ DcrdataBlockchain implements the Blockchain API from tinydecred.api. @@ -719,6 +496,7 @@ def __init__(self, dbPath, params, datapath, skipConnect=False): self.headerDB = self.db.getBucket("header") self.txBlockMap = self.db.getBucket("blocklink") self.tip = None + self.subsidyCache = calc.SubsidyCache(params) if not skipConnect: self.connect() @@ -937,6 +715,8 @@ def bestBlock(self): bestBlock will produce a decoded block as a Python dict. """ return self.dcrdata.block.best() + def stakeDiff(self): + return self.dcrdata.stake.diff() def updateTip(self): """ Update the tip block. If the wallet is subscribed to block updates, @@ -955,7 +735,7 @@ def relayFee(self): Returns: int: Atoms per kB of encoded transaction. """ - return DefaultRelayFeePerKb + return txscript.DefaultRelayFeePerKb def saveBlockHeader(self, header): """ Save the block header to the database. @@ -1083,17 +863,17 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf scripts = [] scriptSizes = [] - changeAddress = keysource.change() + changeAddress = keysource.internal() changeScript = self.changeScript(changeAddress) changeScriptVersion = txscript.DefaultScriptVersion - changeScriptSize = P2PKHPkScriptSize + changeScriptSize = txscript.P2PKHPkScriptSize relayFeePerKb = feeRate * 1e3 if feeRate else self.relayFee() for txout in outputs: checkOutput(txout, relayFeePerKb) - signedSize = estimateSerializeSize([RedeemP2PKHSigScriptSize], outputs, changeScriptSize) - targetFee = calcMinRequiredTxRelayFee(relayFeePerKb, signedSize) + signedSize = txscript.estimateSerializeSize([txscript.RedeemP2PKHSigScriptSize], outputs, changeScriptSize) + targetFee = txscript.calcMinRequiredTxRelayFee(relayFeePerKb, signedSize) targetAmount = sum(txo.value for txo in outputs) while True: @@ -1105,8 +885,8 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf # header = self.blockHeaderByHeight(utxo["height"]) txout = tx.txOut[utxo.vout] - opCodeClass = getP2PKHOpCode(txout.pkScript) - tree = wire.TxTreeRegular if opCodeClass == opNonstake else wire.TxTreeStake + opCodeClass = txscript.getP2PKHOpCode(txout.pkScript) + tree = wire.TxTreeRegular if opCodeClass == txscript.opNonstake else wire.TxTreeStake op = msgtx.OutPoint( txHash=tx.hash(), idx=utxo.vout, @@ -1117,10 +897,10 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf total += txout.value inputs.append(txIn) scripts.append(txout.pkScript) - scriptSizes.append(spendScriptSize(txout.pkScript)) + scriptSizes.append(txscript.spendScriptSize(txout.pkScript)) - signedSize = estimateSerializeSize(scriptSizes, outputs, changeScriptSize) - requiredFee = calcMinRequiredTxRelayFee(relayFeePerKb, signedSize) + signedSize = txscript.estimateSerializeSize(scriptSizes, outputs, changeScriptSize) + requiredFee = txscript.calcMinRequiredTxRelayFee(relayFeePerKb, signedSize) remainingAmount = total - targetAmount if remainingAmount < requiredFee: targetFee = requiredFee @@ -1128,7 +908,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf newTx = msgtx.MsgTx( serType = wire.TxSerializeFull, - version = generatedTxVersion, + version = txscript.generatedTxVersion, txIn = inputs, txOut = outputs, lockTime = 0, @@ -1140,7 +920,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf newUTXOs = [] changeVout = -1 changeAmount = round(total - targetAmount - requiredFee) - if changeAmount != 0 and not isDustAmount(changeAmount, changeScriptSize, relayFeePerKb): + if changeAmount != 0 and not txscript.isDustAmount(changeAmount, changeScriptSize, relayFeePerKb): if len(changeScript) > txscript.MaxScriptElementSize: raise Exception("script size exceed maximum bytes pushable to the stack") change = msgtx.TxOut( @@ -1151,7 +931,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf changeVout = len(newTx.txOut) newTx.txOut.append(change) else: - signedSize = estimateSerializeSize(scriptSizes, newTx.txOut, 0) + signedSize = txscript.estimateSerializeSize(scriptSizes, newTx.txOut, 0) # dcrwallet conditionally randomizes the change position here if len(newTx.txIn) != len(scripts): @@ -1171,7 +951,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf address = changeAddress, txid = newTx.txid(), vout = changeVout, - ts = time.time(), + ts = int(time.time()), scriptPubKey = changeScript, amount = changeAmount*1e-8, satoshis = changeAmount, @@ -1179,46 +959,238 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf return newTx, utxos, newUTXOs -class TestDcrdata(unittest.TestCase): - def test_post(self): - dcrdata = DcrdataClient("http://localhost:7777", customPaths={ - "/tx/send", - "/insight/api/addr/{address}/utxo", - "insight/api/tx/send" - }) - - tx = "01000000010f6d7f5d37408065b3646360a4c40d03a6e2cfbeb285cd800e0eba6e324a0d900200000000ffffffff0200e1f5050000000000001976a9149905a4df9d118e0e495d2bb2548f1f72bc1f305888ac1ec12df60600000000001976a91449c533219ff4eb65603ab31d827c9a22b72b429488ac00000000000000000100ac23fc0600000000000000ffffffff6b483045022100b602bfb324a24801d914ec2f6a48ee27d65e2cde3fa1e71877fda23d7bae4a1f02201210c789dc33fe156bd086c3779af1953e937b24b0bba4a8adb9532b4eda53c00121035fc391f92ba86e8d5b893d832ced31e6a9cc7a9c1cddc19a29fa53dc1fa2ff9f" - r = dcrdata.insight.api.tx.send.post({ - "rawtx": tx, - }) - print(repr(r)) - def test_get(self): - dcrdata = DcrdataClient("http://localhost:7777", customPaths={ - "/tx/send", - "/insight/api/addr/{address}/utxo", - "insight/api/tx/send" - }) - # print(dcrdata.endpointGuide()) - - tx = dcrdata.tx.hex("796a0288a5560400cce55e87b8ccd95ba256a2c509a08f1be8d3198f873f5a2d") - def test_websocket(self): - """ - "newblock": SigNewBlock, - "mempool": SigMempoolUpdate, - "ping": SigPingAndUserCount, - "newtxs": SigNewTxs, - "address": SigAddressTx, - "blockchainSync": SigSyncStatus, - """ - client = DcrdataClient("http://localhost:7777") - def emitter(o): - print("msg: %s" % repr(o)) - client.subscribeAddresses("SsUYTr1PBd2JMbaUfiRqxUoRcYHj1a1DKY9", emitter=emitter) - time.sleep(60*1) # 1 minute - def test_get_block_header(self): - with TemporaryDirectory() as tempDir: - db = database.KeyValueDatabase(os.path.join(tempDir, "db.db")) - blockchain = DcrdataBlockchain(db, simnet, "http://localhost:7777") - blockchain.connect() - blockchain.blockHeader("00000e0cae637353e73ad85fc0073ebb7ed00a0668b068b376a6aef2812e1bf3") + def purchaseTickets(self, keysource, utxosource, req): + """ + purchaseTickets indicates to the wallet that a ticket should be purchased + using all currently available funds. The ticket address parameter in the + request can be nil in which case the ticket address associated with the + wallet instance will be used. Also, when the spend limit in the request is + greater than or equal to 0, tickets that cost more than that limit will + return an error that not enough funds are available. + """ + # Ensure the minimum number of required confirmations is positive. + self.updateTip() + if req.minConf < 0: + raise Exception("negative minconf") + + # Need a positive or zero expiry that is higher than the next block to + # generate. + if req.expiry < 0: + raise Exception("negative expiry") + + # Perform a sanity check on expiry. + tipHeight = self.tip["height"] + if req.expiry <= tipHeight+1 and req.expiry > 0: + raise Exception("expiry height must be above next block height") + + # Fetch a new address for creating a split transaction. Then, + # make a split transaction that contains exact outputs for use + # in ticket generation. Cache its hash to use below when + # generating a ticket. The account balance is checked first + # in case there is not enough money to generate the split + # even without fees. + # TODO This can still sometimes fail if the split amount + # required plus fees for the split is larger than the + # balance we have, wasting an address. In the future, + # address this better and prevent address burning. + + # Calculate the current ticket price. + ticketPrice = int(self.stakeDiff()["next"]*1e8) + + # Ensure the ticket price does not exceed the spend limit if set. + if req.spendLimit > 0 and ticketPrice > req.spendLimit: + raise Exception("ticket price %f above spend limit %f" % (ticketPrice, req.spendLimit)) + + # Check the pool address from the request. If none exists in the + # request, try to get the global pool address. Then do the same for pool + # fees, but check sanity too. + if not req.poolAddress: + raise Exception("no pool address specified. solo voting not supported") + + stakeSubmissionPkScriptSize = 0 + + # The stake submission pkScript is tagged by an OP_SSTX. + if isinstance(req.votingAddress, crypto.AddressScriptHash): + stakeSubmissionPkScriptSize = txscript.P2SHPkScriptSize + 1 + elif isinstance(req.votingAddress, crypto.AddressPubKeyHash) and req.votingAddress.sigType == crypto.STEcdsaSecp256k1: + stakeSubmissionPkScriptSize = txscript.P2PKHPkScriptSize + 1 + else: + raise Exception("unsupported pool address type %s" % req.votingAddress.__class__.__name__) + + ticketFeeIncrement = req.ticketFee + if ticketFeeIncrement == 0: + ticketFeeIncrement = self.relayFee() + + # Make sure that we have enough funds. Calculate different + # ticket required amounts depending on whether or not a + # pool output is needed. If the ticket fee increment is + # unset in the request, use the global ticket fee increment. + + # A pool ticket has: + # - two inputs redeeming a P2PKH for the worst case size + # - a P2PKH or P2SH stake submission output + # - two ticket commitment outputs + # - two OP_SSTXCHANGE tagged P2PKH or P2SH change outputs + # + # NB: The wallet currently only supports P2PKH change addresses. + # The network supports both P2PKH and P2SH change addresses however. + inSizes = [txscript.RedeemP2PKHSigScriptSize, + txscript.RedeemP2PKHSigScriptSize] + outSizes = [stakeSubmissionPkScriptSize, + txscript.TicketCommitmentScriptSize, txscript.TicketCommitmentScriptSize, + txscript.P2PKHPkScriptSize + 1, txscript.P2PKHPkScriptSize + 1] + estSize = txscript.estimateSerializeSizeFromScriptSizes(inSizes, outSizes, 0) + + ticketFee = txscript.calcMinRequiredTxRelayFee(ticketFeeIncrement, estSize) + neededPerTicket = ticketFee + ticketPrice + + # If we need to calculate the amount for a pool fee percentage, + # do so now. + poolFeeAmt = txscript.stakePoolTicketFee(ticketPrice, ticketFee, + tipHeight, req.poolFees, self.subsidyCache, self.params) + + # Fetch the single use split address to break tickets into, to + # immediately be consumed as tickets. + splitTxAddr = keysource.internal() + + # TODO: Don't reuse addresses + # TODO: Consider wrapping. see dcrwallet implementation. + splitPkScript = txscript.makePayToAddrScript(splitTxAddr, self.params) + + # Create the split transaction by using txToOutputs. This varies + # based upon whether or not the user is using a stake pool or not. + # For the default stake pool implementation, the user pays out the + # first ticket commitment of a smaller amount to the pool, while + # paying themselves with the larger ticket commitment. + splitOuts = [] + for i in range(req.count): + # No pool used. + # Stake pool used. + userAmt = neededPerTicket - poolFeeAmt + poolAmt = poolFeeAmt + + # Pool amount. + splitOuts.append(msgtx.TxOut( + value = poolAmt, + pkScript = splitPkScript, + )) + + # User amount. + splitOuts.append(msgtx.TxOut( + value = userAmt, + pkScript = splitPkScript, + )) + + txFeeIncrement = req.txFee + if txFeeIncrement == 0: + txFeeIncrement = self.relayFee() + + # Send the split transaction. + # sendOutputs takes the fee rate in atoms/byte + splitTx, splitSpent, internalOutputs = self.sendOutputs(splitOuts, keysource, utxosource, int(txFeeIncrement/1000)) + + # // After tickets are created and published, watch for future + # // relevant transactions + # var watchOutPoints []wire.OutPoint + # defer func() { + # err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { + # return w.watchFutureAddresses(ctx, tx) + # }) + # if err != nil { + # log.Errorf("Failed to watch for future addresses after ticket "+ + # "purchases: %v", err) + # } + # if len(watchOutPoints) > 0 { + # err := n.LoadTxFilter(ctx, false, nil, watchOutPoints) + # if err != nil { + # log.Errorf("Failed to watch outpoints: %v", err) + # } + # } + # }() + + # Generate the tickets individually. + ticketHashes = [] + + for i in range(req.count): + # Generate the extended outpoints that we need to use for ticket + # inputs. There are two inputs for pool tickets corresponding to the + # fees and the user subsidy, while user-handled tickets have only one + # input. + poolIdx = i * 2 + poolTxOut = splitTx.txOut[poolIdx] + userIdx = i*2 + 1 + txOut = splitTx.txOut[userIdx] + + eopPool = txscript.ExtendedOutPoint( + op = msgtx.OutPoint( + txHash = splitTx.hash(), + idx = poolIdx, + tree = wire.TxTreeRegular, + ), + amt = poolTxOut.value, + pkScript = poolTxOut.pkScript, + ) + eop = txscript.ExtendedOutPoint( + op = msgtx.OutPoint( + txHash = splitTx.hash(), + idx = userIdx, + tree = wire.TxTreeRegular, + ), + amt = txOut.value, + pkScript = txOut.pkScript, + ) + + # If the user hasn't specified a voting address + # to delegate voting to, just use an address from + # this wallet. Check the passed address from the + # request first, then check the ticket address + # stored from the configuation. Finally, generate + # an address. + if not req.votingAddress: + raise Exception("voting address not set in purchaseTickets request") + + addrSubsidy = txscript.decodeAddress(keysource.internal(), self.params) + + # Generate the ticket msgTx and sign it. + ticket = txscript.makeTicket(self.params, eopPool, eop, req.votingAddress, + addrSubsidy, ticketPrice, req.poolAddress) + forSigning = [] + eopPoolCredit = txscript.Credit( + op = eopPool.op, + blockMeta = None, + amount = eopPool.amt, + pkScript = eopPool.pkScript, + received = int(time.time()), + fromCoinBase = False, + ) + forSigning.append(eopPoolCredit) + eopCredit = txscript.Credit( + op = eop.op, + blockMeta = None, + amount = eop.amt, + pkScript = eop.pkScript, + received = int(time.time()), + fromCoinBase = False, + ) + forSigning.append(eopCredit) + + # Set the expiry. + ticket.expiry = req.expiry + + txscript.signP2PKHMsgTx(ticket, forSigning, keysource, self.params) + + # dcrwallet actually runs the pk scripts through the script + # Engine as another validation step. Engine not implemented in + # Python yet. + # validateMsgTx(op, ticket, creditScripts(forSigning)) + + # For now, don't allow any high fees (> 1000x default). Could later + # be a property of the account. + if txscript.paysHighFees(eop.amt, ticket): + raise Exception("high fees detected") + self.broadcast(ticket.txHex()) + ticketHash = ticket.hash() + ticketHashes.append(ticketHash) + log.info("published ticket purchase %s" % ticketHash) + return (splitTx, ticket), splitSpent, internalOutputs diff --git a/pydecred/mainnet.py b/pydecred/mainnet.py index 04fadff4..284875ea 100644 --- a/pydecred/mainnet.py +++ b/pydecred/mainnet.py @@ -131,4 +131,6 @@ GENESIS_STAMP = 1454954400 STAKE_SPLIT = 0.3 POW_SPLIT = 0.6 -TREASURY_SPLIT = 0.1 \ No newline at end of file +TREASURY_SPLIT = 0.1 + +BlockOneSubsidy = int(1680000 * 1e8) \ No newline at end of file diff --git a/pydecred/simnet.py b/pydecred/simnet.py index ae568718..fb44bee0 100644 --- a/pydecred/simnet.py +++ b/pydecred/simnet.py @@ -143,3 +143,4 @@ OrganizationPkScript = (0xa914cbb08d6ca783b533b2c7d24a51fbca92d937bf9987).to_bytes(23, byteorder="big") OrganizationPkScriptVersion = 0 # BlockOneLedger = BlockOneLedgerSimNet, +BlockOneSubsidy = int(300000 * 1e8) \ No newline at end of file diff --git a/pydecred/stakepool.py b/pydecred/stakepool.py new file mode 100644 index 00000000..d141940b --- /dev/null +++ b/pydecred/stakepool.py @@ -0,0 +1,167 @@ +""" +Copyright (c) 2019, Brian Stafford +See LICENSE for details + +DcrdataClient.endpointList() for available enpoints. +""" +import unittest +from tinydecred.util import http, tinyjson +from tinydecred.pydecred import txscript +from tinydecred.crypto import crypto, opcode +from tinydecred.crypto.bytearray import ByteArray + +def resultIsSuccess(res): + return res and isinstance(res, object) and "status" in res and res["status"] == "success" + +class PurchaseInfo(object): + def __init__(self, pi): + get = lambda k, default=None: pi[k] if k in pi else default + self.poolAddress = get("PoolAddress") + self.poolFees = get("PoolFees") + self.script = get("Script") + self.ticketAddress = get("TicketAddress") + self.voteBits = get("VoteBits") + self.voteBitsVersion = get("VoteBitsVersion") + def __tojson__(self): + # using upper-camelcase to match keys in api response + return { + "PoolAddress": self.poolAddress, + "PoolFees": self.poolFees, + "Script": self.script, + "TicketAddress": self.ticketAddress, + "VoteBits": self.voteBits, + "VoteBitsVersion": self.voteBitsVersion, + } + @staticmethod + def __fromjson__(obj): + return PurchaseInfo(obj) + +tinyjson.register(PurchaseInfo) + +class PoolStats(object): + def __init__(self, stats): + get = lambda k, default=None: stats[k] if k in stats else default + self.allMempoolTix = get("AllMempoolTix") + self.apiVersionsSupported = get("APIVersionsSupported") + self.blockHeight = get("BlockHeight") + self.difficulty = get("Difficulty") + self.expired = get("Expired") + self.immature = get("Immature") + self.live = get("Live") + self.missed = get("Missed") + self.ownMempoolTix = get("OwnMempoolTix") + self.poolSize = get("PoolSize") + self.proportionLive = get("ProportionLive") + self.proportionMissed = get("ProportionMissed") + self.revoked = get("Revoked") + self.totalSubsidy = get("TotalSubsidy") + self.voted = get("Voted") + self.network = get("Network") + self.poolEmail = get("PoolEmail") + self.poolFees = get("PoolFees") + self.poolStatus = get("PoolStatus") + self.userCount = get("UserCount") + self.userCountActive = get("UserCountActive") + self.version = get("Version") + def __tojson__(self): + return { + "AllMempoolTix": self.allMempoolTix, + "APIVersionsSupported": self.apiVersionsSupported, + "BlockHeight": self.blockHeight, + "Difficulty": self.difficulty, + "Expired": self.expired, + "Immature": self.immature, + "Live": self.live, + "Missed": self.missed, + "OwnMempoolTix": self.ownMempoolTix, + "PoolSize": self.poolSize, + "ProportionLive": self.proportionLive, + "ProportionMissed": self.proportionMissed, + "Revoked": self.revoked, + "TotalSubsidy": self.totalSubsidy, + "Voted": self.voted, + "Network": self.network, + "PoolEmail": self.poolEmail, + "PoolFees": self.poolFees, + "PoolStatus": self.poolStatus, + "UserCount": self.userCount, + "UserCountActive": self.userCountActive, + "Version": self.version, + } + @staticmethod + def __fromjson__(obj): + return PoolStats(obj) + +tinyjson.register(PoolStats) + +class StakePool(object): + def __init__(self, url, apiKey): + self.url = url + self.signingAddress = None + self.apiKey = apiKey + self.lastConnection = 0 + self.purchaseInfo = None + self.stats = None + def __tojson__(self): + return { + "url": self.url, + "apiKey": self.apiKey, + "purchaseInfo": self.purchaseInfo, + "stats": self.stats, + } + @staticmethod + def __fromjson__(obj): + sp = StakePool(obj["url"], obj["apiKey"]) + sp.purchaseInfo = obj["purchaseInfo"] + sp.stats = obj["stats"] + return sp + def apiPath(self, command): + return "%s/api/v2/%s" % (self.url, command) + def headers(self): + return {"Authorization": "Bearer %s" % self.apiKey} + def setAddress(self, address): + data = { "UserPubKeyAddr": address } + res = http.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) + if resultIsSuccess(res): + self.signingAddress = address + else: + raise Exception("unexpected response from 'address': %s" % repr(res)) + def getPurchaseInfo(self, net): + res = http.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) + if resultIsSuccess(res): + pi = PurchaseInfo(res["data"]) + # check the script hash + redeemScript = ByteArray(pi.script) + scriptAddr = crypto.AddressScriptHash.fromScript(net.ScriptHashAddrID, redeemScript) + if scriptAddr.string() != pi.ticketAddress: + raise Exception("ticket address mismatch. %s != %s" % (pi.ticketAddress, scriptAddr.string())) + # extract addresses + scriptType, addrs, numSigs = txscript.extractPkScriptAddrs(0, redeemScript, net) + if numSigs != 1: + raise Exception("expected 2 required signatures, found 2") + found = False + signAddr = txscript.decodeAddress(self.signingAddress, net) + for addr in addrs: + if addr.string() == signAddr.string(): + found = True + break + if not found: + raise Exception("signing pubkey not found in redeem script") + self.purchaseInfo = pi + return self.purchaseInfo + raise Exception("unexpected response from 'getpurchaseinfo': %r" % (res, )) + def getStats(self): + res = http.get(self.apiPath("stats"), headers=self.headers()) + if resultIsSuccess(res): + self.stats = PoolStats(res["data"]) + return self.stats + raise Exception("unexpected response from 'stats': %s" % repr(res)) + def setVoteBits(self, voteBits): + data = { "VoteBits": voteBits } + res = http.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) + if resultIsSuccess(res): + return True + raise Exception("unexpected response from 'voting': %s" % repr(res)) + +tinyjson.register(StakePool) + diff --git a/pydecred/test-data/sighash.json b/pydecred/test-data/sighash.json new file mode 100644 index 00000000..cda387ae --- /dev/null +++ b/pydecred/test-data/sighash.json @@ -0,0 +1,144 @@ +[ +["Format is: [raw transaction, script, input index, hash type, signature hash (result), expected error, comment (optional)]"], +["NOTE: The hex representing the signature hash bytes is not reversed like block and transaction hashes"], + +["Specialized transactions (coinbase and stake)"], +["01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff03fa1a981200000000000017a914f5916158e3e2c4551c1796708db8367207ed13bb8700000000000000000000266a2402000000000000000000000000000000000000000000000000000000ffa310d9a6a9588edea1906f0000000000001976a9148ffe7a49ecf0f4858e7a52155302177398d2296988ac000000000000000001d8bc28820000000000000000ffffffff0800002f646372642f", "", 0, 1, "66951eea08e08888ad33f128e2b562e56c59be2e6b9ad71231e3cea63468cf29", "OK", "block 2 coinbase, SigHashAll"], +["0100000001e68bcb9222c7f6336e865c81d7fd3e4b3244cd83998ac9767efcb355b3cd295eb90a000000ffffffff0300c2eb0b0000000000001aba76a9146c5b4353449a7948652321c71f4d0409ae2dd5f688ac00000000000000000000206a1ebaeaaa590c5c125aad8ebb998a9b26f7436aa42a400d380c000000000058a03d6f880600000000001abd76a9148d1cf0fc4d7918421306fd7d4a4e1b9770a9aa8588ac000000000000000001e04aa7940600000001000000000000006a47304402203a5e5724969d35452cf97bb3ae0c72834093797464c04096608402336d0ade8302204975f5b35b54fca6af02631094cd46251b7af57435641825ec88950fab8005790121028eae6dde0fab8c118c3b6f81e95a4369c0f8834006bef08b5f0c1afb242b6bba", "76a9144013ba74220111fc67dcab19d26eaf036785348b88ac", 0, 1, "2c54ef7020bb2f4f18c2dcd8447d18a51c536259c05201db14a9ce8ea3202ff3", "OK", "block 257 ticket purchase, SigHashAll"], +["0100000001e68bcb9222c7f6336e865c81d7fd3e4b3244cd83998ac9767efcb355b3cd295eb90a000000ffffffff0300c2eb0b0000000000001aba76a9146c5b4353449a7948652321c71f4d0409ae2dd5f688ac00000000000000000000206a1ebaeaaa590c5c125aad8ebb998a9b26f7436aa42a400d380c000000000058a03d6f880600000000001abd76a9148d1cf0fc4d7918421306fd7d4a4e1b9770a9aa8588ac000000000000000001e04aa7940600000001000000000000006a47304402203a5e5724969d35452cf97bb3ae0c72834093797464c04096608402336d0ade8302204975f5b35b54fca6af02631094cd46251b7af57435641825ec88950fab8005790121028eae6dde0fab8c118c3b6f81e95a4369c0f8834006bef08b5f0c1afb242b6bba", "76a9144013ba74220111fc67dcab19d26eaf036785348b88ac", 0, 129, "9e886e18ffce5ef004e471a7247912d5aa0520129a8aa5286e9f989dcc52369b", "OK", "block 257 ticket purchase, SigHashAll|SigHashAnyOneCanPay"], +["01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffffc89d678aa5d6604d5cec00069a9b982f1f97cb1a7a745de81499b5bca44705a10000000001ffffffff0300000000000000000000266a24d5a50f5d288e7c755c2d59eb4eeeab333a4f2176b0fbb66ba313000000000000ff0f000000000000000000000000046a0201002fd213170000000000001abb76a914d81c2a2b41089cee62c2cbd85a47acf5f5f0353d88ac0000000000000000022f10280b0000000000000000ffffffff02000000c2eb0b000000009b020000010000006b483045022100fe0d708f6db2ae1ce072f6a7791a6ccf8d0149c69fd9565a8cfa6731ef1fc0c202202a84edd000db79a72ea312885f9b19968207c69b4fb5c9174477674c2652a4c7012103d17c71464bcb71d016b974a31703813faf3bdf3b69794694eea9bb321e646c7a", "ba76a9140f91dcf59d7a03649112ba0a2a3a3beca66a473888ac", 1, 1, "fa82c089367438879299258f26b4cfd8cedee3ebbb3f9c4a3e14aad530613899", "OK", "block 4096 vote, SigHashAll"], +["01000000012543e79920db8c927a8725523f27080bebb268c56a5565ca2477aa5b5d0081a50000000001ffffffff012051790b0000000000001abc76a9143724d32970f43ee17ad61cd03a99290d342c1e0788ac00000000000000000100c2eb0b000000003e030000000000006b483045022100ae870b1fc2a25b619cc08d0de24bbb96029238577c16e4c0d5c8a39e096b16ef022034a3131d2405348d272dbcef178be6c7d6dfce5a615c246893bcce7514d61bd60121036a716aee61663e4fab8a696c417aa644f00472384466682e36a22816363b1c3a", "ba76a91408c2624baa0eb512b0279818dedb8c43460dbb6888ac", 0, 1, "820db771e3b4f8532b43a5b3769ba3ee8a5934264d4847061ee1e88075896a05", "OK", "block 4107 revocation, SigHashAll"], + +["Regular transaction with more inputs than outputs. This ensures SigHashSingle error behavior is specifically tested."], +["Includes all combinations of defined flags."], +["From block 354, tx hash d8a785e965c4eb7ea955a751ec9f6a069bd2ea25be6828d19d2355d4b68936e9"], +["3 inputs, 2 outputs, all signing input 0 with various hash types"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 0, 1, "569f23573cd279d9fea347ed16d86984b271b0b4b4270cc7122201683fcd7708", "OK", "sign input 0, SigHashAll"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 0, 129, "edee9800fb6e09f3a6f91f65c6b465587142b42398c4420f1d17fbcf0878cf54", "OK", "sign input 0, SigHashAll|SigHashAnyOneCanPay"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 0, 2, "67ce8c98109995d7b303063330ba91f332f35d32a3242f85f798460c2540fc56", "OK", "sign input 0, SigHashNone"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 0, 130, "b51eb5253694ad53b330affef46de7236f5c815636f27d12af883c46876ca263", "OK", "sign input 0, SigHashNone|SigHashAnyOneCanPay"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 0, 3, "a1f4f2ced71352153ffee5dd570da5d609ecd5ce04e1db808c238554d758fb13", "OK", "sign input 0, SigHashSingle"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 0, 131, "e3955506137fddb6826f1fe6f0c7c291bc71fa5644fd1330be2a3aa546d3ef22", "OK", "sign input 0, SigHashSingle|SigHashAnyOneCanPay"], + +["Same transaction as above, but signing input 1."], +["Includes all combinations of defined flags."], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91446b7b31c6b5da4643cd7453eae3beba375fa9f4a88ac", 1, 1, "d902d1a552314cbe5308f10facda22d0ec0001cc62f7d27c2a069e05b5cbf976", "OK", "sign input 1, SigHashAll"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91446b7b31c6b5da4643cd7453eae3beba375fa9f4a88ac", 1, 129, "1ec4461f3663fcda50aa2d3b1b8ac5a3fe9f2c61f4f79b816551c30389a670ac", "OK", "sign input 1, SigHashAll|SigHashAnyOneCanPay"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91446b7b31c6b5da4643cd7453eae3beba375fa9f4a88ac", 1, 2, "b73be8afc6c800ef8763eb6d43f1a90c1c5556fe07d5b26ca4444b5dcc7a9fc5", "OK", "sign input 1, SigHashNone"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91446b7b31c6b5da4643cd7453eae3beba375fa9f4a88ac", 1, 130, "c86067bf9b3656fbb98f45a90990dffd4b3740227daf45a277e984e9fb1aac44", "OK", "sign input 1, SigHashNone|SigHashAnyOneCanPay"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91446b7b31c6b5da4643cd7453eae3beba375fa9f4a88ac", 1, 3, "c5f5a1b4144ac8e9898047ae6791ae304f46f274fd68e5c5e072ac16298e2a88", "OK", "sign input 1, SigHashSingle"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91446b7b31c6b5da4643cd7453eae3beba375fa9f4a88ac", 1, 131, "09d9cb4ccb085b695be4be003399d5ea30c7804b711713bcf37ffd75f6142333", "OK", "sign input 1, SigHashSingle|SigHashAnyOneCanPay"], + +["Same transaction, but signing input 2. Should fail SigHashSingle since third input is larger than num outputs."], +["Includes all combinations of defined flags."], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 2, 1, "311b66f62fa3a2a4292185cbc903377749e12a03e75c7a685c220ca66e21b096", "OK", "sign input 2, SigHashAll"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 2, 81, "66992fa43b7429689fc9da5d078f71e523f140bda016f98724a510e76bf9d580", "OK", "sign input 2, SigHashAll|SigHashAnyOneCanPay"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 2, 2, "3266e325a7125c61871d5ca99a819b64c367ee431ea8323ec145c80b37413f51", "OK", "sign input 2, SigHashNone"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 2, 130, "11298ac3f6d72c38fdf397c1037fec2c7750cd9db467b76dac9fb977f0030f10", "OK", "sign input 2, SigHashNone|SigHashAnyOneCanPay"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 2, 3, "", "SIGHASH_SINGLE_IDX", "sign input 2, SigHashSingle"], +["010000000304aacce7ca34e1f59e55d957f4d27aa6f54c5dd4046665840797ffe88b27320a0100000000ffffffff0785b51df7d46512ebd63c4dd17f391360c9d6fc5c8846a0684184a601c30c790100000000ffffffff0998d992230ab4b6ab112923bf8fd4db6bd977292ec52e722d27e389e229d1e10000000000ffffffff02e05d6a2f0000000000001976a9142fc06df75ec010d3ff25c3de77713fca4e731d4088ace09cede90500000000001976a914c2a65fb57cd570a53ff6cc721d854d5d7549f23f88ac00000000000000000300e40b540200000051010000040000006a47304402203162d5cea243874539bb6e35c9515342fcfa3fc7b8fa77ca9a17cef541c8957302204e00f31091c8f982eff563b805d1909679741c02c851919a709fce40dcd452ad012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb0012c2e8010000003f010000010000006a4730440220557f6069906bc945c9139f4d2d222abc30e521a20845513897d9ddcee3cb819002205edbda2708bb8df15c3a6f6b28144247544044e320448ff4ac766630bd6532aa012103d7502318c3205e4df6d0b2e9afa4c721526421914783fb33ce2aec9d40f0b4490050d6dc010000000d010000020000006b48304502210099f5cb0ca36e68f7f815e17538706b374e24ec9e61795984f767f230ee08dea802204c908c38e647e5d551dba5054adfd0430dde19ca94d83b68a795678d5246a90d012103ee327661befce7e68046a18aab5d2a566b0425069ad6b7b1951a737d40abd9cb", "76a91478807bd86b22a9f23bb4e026705c3e52824d7f3e88ac", 2, 131, "", "SIGHASH_SINGLE_IDX", "sign input 2, SigHashSingle|SigHashAnyOneCanPay"], + +["Fuzz testing. Selection of transactions with random (including undefined) hash types."], +["0100000002a744bebe5a6d3953c49a46b968006933057c79216ffb08ab477290a944feb6d90200000000ffffffffb09728aa82b1c809f89d0b994d9a2381ab6349feeb5e0a3e946dd129095091c90000000000ffffffff02a5a5ee940000000000001976a914fd579ebb6af01f5ef9e2ab74e722666449a47ce288acc9985f1a0000000000001976a914051a109bfb52dcc8cd3e2366f023ad474c5960a788ac00000000000000000232b8495b000000000e260000000000006a47304402200ee964dc1ef9decdc28be50a40114cab569441752198007f1e8780b36493241d02207591ed69ef78dbcf08a666deaad407ef910039e2c5c788b7b645f0b9a1ac010c012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8269c691b540000000003270000010000006a4730440220795855d38ce14f7f7ed19f0f10e1cc63d0857de0d893503296024cc20457a951022026c8b20c521807f8d435d4cdd64bb7ad19c97636f7dcb5f253d256bff3d84ba4012103f93375fd8a4dc43fb536ff072652b0139a262b20694e1d26d258a46c5d80ed58","76a91425e317b8f980b165021fad9a75c8fbb7733f60d088ac",1,61,"bc6d0c65d5f161371eb5c5b7ae7507c3c37c07af6ef4cba4aa8335fa6186a801","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000004911a9706890ec2b373f7b96ec5f0c1f2a2df427bbfb6bdb39fba3931e874a5bd0000000000ffffffff2ff8be74375a349185adc6070c65a63b1dcc9a09f106bd257d6164e8f35b6cde0000000000ffffffff8b54f2c3093d542c3994880336d66e82f1ac487bcd01200c14eaf155e48bae710000000000ffffffff32989de7a964068ee91c18f030be96865b980e6418e463e0b27f61180aa76bcd0000000000ffffffff022e2c84050000000000001976a914c1f18e4fb7cef269a64dc033b0a9200872cd2d3688acf71b1a000000000000001976a914a13acd567b8ce5c2ba2b182a8106c75d6880be7088ac000000000000000004a76434020000000021260000050000006b483045022100d6d73a863712535df51e6cf0a4b20f3883617901205f032f3532a833355702360220316dd87105377b3d760b5c65dfec1078082cbf55b665d76744e701acb73249d10121030293800f7d519ca54d3db4f0901875035e98c02ba8069aae7fcf3d3a07778cb0a413a60100000000d1260000020000006a473044022078c42abee7a04120758f7a952aa884a51fe206771536bffc359f61cb360d3e6402206f49914b1b4d1a57cbeff44b4b8688eeb9783e178fb1e5e25793405529f747d10121028682ce8b24cb8d5e26fa43a83e0693b302cba9c18e51ceedc3c36d4d8b4b2c40643e140100000000ee250000020000006a47304402202dd2decc7498fa808704beb2635cfb65a9d4983a526e102f397d0b101266abee02207b570e15a6339ad589f8be2aef4e96404682bbed584024d1bf80e2f93277ed1101210346eec815224299e81330a776cbd92978b0edfc3430a6800360e0b974307167e8d674c600000000000f270000050000006b483045022100f93be6018ef53509c2b098c92e4870e5c0a1ac9f6e5614e4c92aac88dce0935602202a1a4954418a55876eb2ac4a2556f227c96fba3cda00bfae9bc507232218a0bd012102818601ba90f30aaa3c1d2e8e187e61906307e40eec62edc00326a4f3b17868db","76a9149101c5cc6a9bff7baa5f556dde4ef513060d3f7288ac",3,59,"aa12c5e60acb4a595e7f4bdb2d75263cc9b82fdf8c350248cf1ed956e4ddf7dd","OK", "4 inputs, 2 outputs, sign input 3"], +["010000000211f1f5b21af23988af1948bd92307fb159c891706af6fb4f1f624c3db5339f3a0100000000ffffffff276d1adb947ce6b6777c666e74da78339715085a7a2d83d2f13a1761eda84cb80100000000ffffffff029e1465050000000000001976a9148958fae2118d4b5d7a3884c6c2132ef7276accd788ac49ba08000000000000001976a914816de5bab0a2991b35c7ddbae5d1afc7955136d388ac00000000000000000295bb5505000000000f270000040000006b483045022100eff76ee2821fb4d1f86dc1168249916647e4eb89c2595ae82fc1574697c891400220557b67adf577d78c5f6bb494f1ec389582cb6144f49db9bcb404b128f28e5f5c012102e49ca60c09291ebd52bd677ddbe169098bf6d7f89dff9fbdb9dc6a1a6cae7734b2f62e0000000000c3260000030000006b483045022100b5a594c8ff9997aab0fe5eee2421bc85b2d6a6154636ebe9a32a38660da6477c02204833362350d890c51f8a36076e61d498c8c8b3ba0c652591d364ae591735780201210265c1ef7cd75d39596f8134b3a1f4213d252f27f2508e43b2a27ceba27ee23cb7","76a91442d98cd9b143b7b0cb7a8d8bcc7bdb73cc8fd76588ac",0,96,"ebc27bbd7d5dc54a875a5ab6fd28a67da51cb2a662d0e0834ac4f6e453339f89","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000003c502e9a9befa8c772d6d84a385b3730e1cba4efd56342552158357d97df0dfd70000000000ffffffffc1b3068c2c2c46d3154dee9d5e309d405cb6efc432d35f1c0337b6d91a52a0d20000000000ffffffffc2c182d2e63a2fb5bedb1466528a8aef771d0a530ee20251319d93d03138edcb0100000000ffffffff024c2ca47a0000000000001976a91402dd4bfed233b365f0788d8749f305281c89d6f588ac4cb6c9010000000000001976a914f344ca402459f41eed08e4d7fd516dee818b49ad88ac000000000000000003c68c7c3b0000000010270000020000006a473044022033519c2ccb7570eca3af64a279b40c1369f2661a1d6b1a2000cc6d8e918fe05402205678f4f27fc0e68df9e28cd5f09b0ec639faf7f2becc6ed8a1b7b9c7ed4aec9f0121031057a0e0f995095b0c74d61912fd28f0599277c75ec542ecf3dd6aa3e5d34c6b69a0a8260000000001270000060000006a47304402206db99cfecc4730a8475567fd9233dcc444bda821fa4124e4c7764663f8bf49d7022016a527641194129f9cb9b4ca7c94406122828a3eb130170176e15e1f37d457da0121020df00cae47ea314bf1f34b9a8070cef4ef525c5cfd447a8520ac6b5b57bf9df7c9985f1a0000000012270000020000006a47304402203d5462c3d0b03975a9ded6c57298c099979d638a5ab48c52702277c10b17d8a902202763c07244c1a93240bbe15e8c4dcdf806da7ae660704255fbd0f77dfc9b7e7901210331758466e37e2797735423c5654f72a06ea128b7ac655802be20097130aeb3a5","76a914051a109bfb52dcc8cd3e2366f023ad474c5960a788ac",2,66,"bcb782d5220937f4d86c34add78eec0ca8132f0b30b042ccd0b3912a8772d826","OK", "3 inputs, 2 outputs, sign input 2"], +["010000000212765d1989098b0223803d69cb114f9fb212ce2ddfa01d6204a9d1bd2869ba7f0200000001ffffffff136c8284cc334d103b510ffd462c6fe2d26af0d4445541daf7878bddd29a28f60200000001ffffffff028017b42c0000000000001976a91457830e4ffe8d4a7f78c5385e9c27cc3d8a1cf22a88ac5cb3ee000000000000001976a914ddb150ba79508a61701c1ab6f95febc38009a0fe88ac0000000000000000020e8bf7160000000027200000030000006b483045022100a7f2e152c77c7421dfb1f661871d5fe879a7112d3e43ec39cc3879749043ea81022024285969accbbd7d1a47d088d11c1dd39d51777593632fe56f3c93a35fd8735a012102e1ed47c034a4f690b1c5f1e9d0e8e875ad655481e43b337f367934d5ac9237540e8bf7160000000002230000010000006a4730440220651fce55c63d9e4a4d789b904913572c795c2e3ac7274909aee8973340777fca022066077e0633ff315c8f2433f093cdb549c5963faea800419f5afab2a4b87da14a0121023821ed9fdfeb5e9bfd9e501273cc956caa1a2f127fcb3e3e4d4bf82ec7ae5dda","bb76a914b5333b4781bb7bd91942ab35c502e1038e34670288ac",1,77,"93daad357fd86b97b6d7fbc57d906f5f58f50fc0a679bb7af2c75fd8a383094f","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000022792325d17753bdcf0b9f4e4b7d0fc1dfb2f4faa4527b98493f829921639ec8b0100000000ffffffff66f71ec8cb187403633f37c602a9ef9a15bee6a08e33a348110e7f2b452d10c30000000000ffffffff029a04b5ab0000000000001976a914028a7ace756b8a4b5ea8347145a8119e18df604088ac486941230000000000001976a914111e6ae99e4d5bf14480f884964684138cc9597c88ac0000000000000000023b095a690000000013270000010000006b48304502210090c1eb5df3acf4ef1a7bd56252833cf000951e1178a9dc0901407beb3a9934aa0220355953de4e05fa39841bf0eea2b11cd94c8f6acfc67da6ea55662c6d617ab14a012102dee6629eaa428bb17e46d4de4d6441a48bf59a2028fc6229f777ff025cf1791c0748b3650000000013270000030000006a473044022034154a513d24f116619fbedcccefd5953fbe225912680245a02ebec026f2db6402202374f52e2b8890fd9955ea9db5207ba065ba8d8811a1066a1c2f4ac7ec37fcfe0121030bfeb923307b6cd6e03fc398d2d94e339d2c6906fdc8631be103582014d1a40e","76a9148e744b4d29544b06c15b8005b60071f7b844e18c88ac",0,48,"856154f4af2b65f43fb573ed8660491ac47c8fbc2c838b7f8fdfe35e040ef952","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000338a4e0160ba69d9c9ab7504d6895f0a2672ab8b172d30c32bc6363bae9f9d62c0200000000ffffffff63fd620072ccc3f0b2790880df6cc22ef834081968e5e5764eefbe57d677fb050200000000ffffffff452113807e0528d4e7a3f622811f6c1f4e0b480c320d993791005f1f32acc8ca0000000000ffffffff0285b48b410000000000001976a9149c7d0120174543785cfaf1f6a7341b58ccae4bd488ac829b15f00000000000001976a9146da90994317bc8703b6a4fb1cdb850236a52783188ac0000000000000000032f360a700000000025260000000000006b483045022100dc709faf834077414e20ff0aa575566e929664f8e787d72b07688a230a5d79b7022056fb3fbaef4810ceb54944e2f3db58e775c82c7f62af576f8a4b11bb4225e428012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8269007a76f000000001e260000000000006a473044022016953fcf2c1433fc9431766f41dc5e480426086c4e6d19bf8f160291830aa06e02203dc7a634bb616472a913e9506646486615e87cb6521e1bdf0d7ecd1ab828edc7012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826a8f50652000000001f270000010000006a4730440220230b2870083c835d66fb8dd84f1bad6ab54f44ffd4ba9431119f156d8f04e36802206b42e8ea181638a86a33801059136df7d8de9e2bbe39a312699d771b01c9c4cd0121026a2f542882b68310d403102070cf9b26d5959c1c0f76f1be1c095ae790882adb","76a9140dbfdb20bc3a5eff7afbfb7804c8acb093c57a6788ac",2,248,"4f2f6f0a8db1cc044118c05da040e553866e4797cec7810ee98153cea8449e9c","OK", "3 inputs, 2 outputs, sign input 2"], +["0100000002b29f0d66cbb191e4ae4f8309a4271819e83195ed40d91f765622cbc74d0a58730000000000ffffffff990889858897442aad192639b02ce1d92c5d1b3544658281105ba77ac32ce8750000000000ffffffff02424baa160000000000001976a91426597059a1df90f3f8afaebd73983e8bcf5d962988ac91e4723c0000000000001976a9147ee4da3f65fb223d61a383ed488b495eeff2dab388ac000000000000000002e048dc2e0000000014270000070000006b483045022100c4117210d2bd7c783b3fa59606a270d71fb1ce2c3d346162820b6bedc022eea802200536de6dc6638e95d2043bcdff09eada1cba9ea81c0c2b843cf32c0a7c7d91bf012102298bf8919658b1c5785c301538ad7afa6141dea28dc437c7db6c26d5fa90a8ff53ca5724000000001f270000020000006a47304402206185c1cca1f5228437340136b1a8ff978557c1bacb349756dd194e001c0d69b302204936d531df860cb37d7e69afdba1de5b3b6a4c9be3ffa261237917a64df4d4b8012102fc14d9f4327fd9ad8e8a06cab93aa4b13795274d224ed57fdbab07b22e2b1276","76a9147e033be88375f223fc50bdc6f88a0c160683427e88ac",0,141,"bec0f1f9029a65e8973e7ce2b43ff6ff0c7e5544f0f193433c23f06becc47f55","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000396d339af6133c030446ae9f6f328ad3e7635bd09488a4ecf308eaa71bc1dfc110100000000ffffffff34a1e2d6998988e5dd20ddfd78f9cb895f1426743e6eb53f6cecb9c828a3b3fc0100000000ffffffff5d785c97b3637b3897ba06f6878e73db7dddadb18d8df82f3b299079137639cb0100000000ffffffff02c3cf5d020000000000001976a91481cefb9a8f7c8dca36e77fd665b44e823b4e8f4188ac7dc904410000000000001976a91410af058288a956db91be66f1fb0514c03878401188ac000000000000000003d8ce55340000000018270000050000006b4830450221009daf1b6b21ca60be91dd67a0841540035eee55308b3701b8242c507f02bf440e02204d263b623811de9e4836ab6d4722e96969f293f8de1cb4ab6445bcf60027b11b01210376494942bfaae5ce28b90c850d71937fb262575741992da031d4fd83c2db791a2efc3f0a000000001f270000030000006a473044022057530c029fd2cb9cdcfae842f0746526049080ba95effd9b37aa4b19a726c0b302201ed54bba6e5b673ee321b707fdc074a5b4aea5d3e4b340f51b28198b2aa94755012103cd38ef67b79f3f22a973ec0c82c93d970b36f33057642e178353e9a97106519d9ab1e3040000000023270000010000006a47304402206fb5a490a963afc391024a70cc3444861de7fe7c8ec5195b4aace04b38c45d270220585a432b627e5676fa9354e0d30150885ce87485eeb9ecb3ebc33e8443107eeb01210379f208a1286234d9d4a5991bc6679614e3bb7241b8f077f87ec2e7bf0328d234","76a91422889b2ceaac98a7d7a9f5a268e0b69ff51d73ce88ac",2,222,"28ac303654abb42c225805b70ef858924e2261720e0447eb1f0b1806b80fd9be","OK", "3 inputs, 2 outputs, sign input 2"], +["0100000003b80de7e0fc581d5c5428133bb7feda56fc40b4600ad802c129461c1d696c5a1b0100000000ffffffff238d70b36013d82adb69cc515e6d9ef8f4505d03a8b7ee2f92aa25b698b24feb0100000000ffffffff277664b7335cf5ad8c8bce91f09a1a85f336dc9505db9c1cab07dc915079642e0100000000ffffffff02158dbe020000000000001976a914c7dddb470f86ec2694da2d57090889b606d77f8e88ac9c3c752a0000000000001976a914c15f2c8452bf573954252dd901f96696d52d20d588ac000000000000000003426d391c0000000005270000010000006b483045022100e18b0019e660a15f02baf2ed22729474ab293c445b0df31bf43efb79f222446702202e1b390782e2def738768e1e8a9ca7f09fbaf9658d52a4102bd7c3fc812d3630012102b43a6868c552f14a3f8c2df45fcea574560d7ed3fd642efbf3ce5a7c480967c46de1f00c0000000098260000010000006b483045022100ae07b342ededc327a94918f2cb75ef3cf15068c06b705655172fb095155d9b23022019e7d3fd31f4f4e2deb82eb88469c3e812e7e4adce5120c2c1b45a1f174ec6820121020f3e58435bfcdd600c43e6a0aa2f8da4bb09d8a58d3d8f147413729e4d58784c42c655040000000072260000050000006b483045022100fdd2bb7c3289cd10c8c554150a39b41de5255099065aec595035a62c3572e677022027920d19b246265c3263175f79452da3d41b6fd043e5b6931d92027aeeeb30440121038dc4a26be27c3900933be4916d06db31ba768fed0395e1795cca0bdc15753d27","76a9145456f39904968396d0b338c0ae804a8cc5aec1ca88ac",1,215,"6b6cdd0f6f07ab975138fcebcddcb0ca43f76c124451a707ec4fba31ad77ac1a","OK", "3 inputs, 2 outputs, sign input 1"], +["0100000003983e67c755b7398e812a793b06aaf7f6641f56e8daaac327ddfc414d35494d6b0100000000ffffffff837344d2b48d552fb48b8efc89a1686a1925944f08a83c9c8e236873e311e6da0000000000ffffffff0d9c3971a659510390bc8356f38f00680645bd096697f47aa854482f71b595b80100000000ffffffff022b9ec0260000000000001976a914ea18b4cc94c941b768a96b0e848d70516add5f7588ac0d2813010000000000001976a91448f4a7563f86df4335b582f988864b6e323194f588ac0000000000000000038923c323000000002f270000030000006b48304502210085bcaaa0448e8b43d8445c74c2389d1a37245af80710c5ecf956e5b0033e4f6702203016fed0877dadae2323846bc9fd6db00a96fa64d8ce54e20f48de17e0646b3201210317e2a0f439e8d8c8bd1137807034a56636f5a64143feae19089de86fc478bd8bc3cf5d020000000027270000030000006b483045022100e43c2ad98d60f737f4f80be4cef9259384d2aa9d9ef17f61649707b105b4c82e02201aea18ebaa9d53e4a9f31ef2f6ecfa921d3ed14e0897dab03018354497ac19d6012103c7548206690d69c46cef2e36e09f21a3c5ec4c5ca33b4448eb00c818945ae5da4cb6c9010000000014270000040000006a47304402202786a415137c519929572b1258ce9f874a1e46e98f237c96b7b6a3804c0aac52022005e84f835a3233f11288deae51ef2d28452a0de1816e8182d644744175dd34260121027c2bc97b8933f2a9498ea44e766ed78b858181de07eaaef3c039ccf489bd0e2b","76a9147bce7ee910285878131a30e35bba3cfa5c94ab2388ac",0,78,"f21c3749a626e5a8b220e3d7f5e2e099ac715c01944d5d229df251d5cbb55f43","OK", "3 inputs, 2 outputs, sign input 0"], +["01000000022b380df4c16baea2c1325bd6e600fc29ba65706b0d455e1f0d6731465a0ee7a30000000000ffffffff4e6b8ca39e68b2b9c4bb9b06c6d48f65ee58d5482f929b9b25fb3ee10b11a2830100000000ffffffff02c6d845370000000000001976a91487b2392cbde3c732716a3ce02099be7b9af841db88acec103c030000000000001976a914a5eb57f483ed682a6ac39fa994b61b83fedb104c88ac00000000000000000295f1f92a0000000034270000030000006a4730440220087c0d70cfbd9a21144c76eacb105c956bd0f2caa69c4bee954f975382708f410220228de3042039d6706b5e12119b76d6b6606922ec9f312ca7b86cc53acc0205a401210299a9283b395f8d3dda23665c0d688876c3ee37a8c410986b09e8a8c1fc70d06e7ddb9e0f00000000cd260000010000006a47304402206c10b0e7e8d3fb27c74a52b93b39ae18a3011e6069bc142168176053c475add80220031d6213f5e59085a80794c1755b5b604b35b83a3bec7b97a7b7025c5ef9657d012103f26ddeee7fb5a57be1d88161f4a9229e609dc20ef9d7ec27a9fe1594f5182a33","76a9148108e27bb4cc64eb2988133db2515ffb37d79b5488ac",0,159,"db2e64142ce28112f12ec0b2c6f4399fd7501f07e340a14de48a79e88ce9e733","OK", "2 inputs, 2 outputs, sign input 0"], +["01000000027b8c84316c3fd6c11669e17e0fb7df387bf72ff4c138b6286533cb4ddbbc1f600200000000ffffffff47daa5e5c3357927874f5a16132fd333432bf206e3ed2648da899c48ac7b26910100000000ffffffff027c3df2430000000000001976a914636cc764a00d71adc7a61b7fd2f03647913dc29f88acaeecdb940000000000001976a9147d9b81b63cf48984d55715082f32c13c3bc2237888ac0000000000000000024f20eb70000000003e260000000000006b483045022100ef42fc6ea1796fcd46c416e824b9f929c52c9d8b5468efa0019e18f0bad0dd710220138f6c3c2454865381ceecd4419e6ea9655ac17539aa46377cbed7458ff4a97e0121024a2546600c5cbcb53971a19e2d481e69b6f39f8f8df55ebec340eaf238f5a4dc3bedf9670000000034270000040000006a4730440220226fc8bd636e6371c249c394e958a5f28301eceef4165ef9db35e49830400d80022022492dd95f051e37d49caf0776db502994fb921f80900290b180387549cb2884012102491c22cb77133bcf9bebed66fad0002e0fcbe46d3e7655f0eb283b996e78d9fb","76a914b60ee40ada8e797ac6e363ad8c781155000ecf7688ac",0,85,"c809e3937969074f27384d362a903e0f68cca08254ef014647bad34c2d61d37d","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000458e460c35bfe3b45b79b0d78c8f484bae465a1a7ec21086d06c742eb5c1c34e40100000000ffffffff537bac445821c7fbf5a4bb4cd762418fbb7061fe86009e1499a58d8a710673e00100000000ffffffff4e8747d1f301a1456f94b42c99f158116fbb422ca5229d0f708933cf2a40a2320100000000ffffffff2e5eeccf8603d24916dd2aacc59fcbff55d3735648e3b794b01f31c53df849f70100000000ffffffff02bafc50370000000000001976a91487b2392cbde3c732716a3ce02099be7b9af841db88acac8ff5000000000000001976a9146b3274489ea258c293b361467f05dad48f4571d988ac0000000000000000049318e01f00000000e5260000010000006a4730440220574cfb4aa058d842d2565054036b701b505e3ed1cd20aea12d39d49a0065429302207d6811d17bb60d30295dff422da4ea549707273ea41abbfb0f722a3ded0ab24a012102bdc3d97210b123995784e94428741ef2a744fd2dacc9fa819d5ce041e374033607d1461300000000db260000030000006b483045022100b4005c7264b0987e6fb1b27d4dd76ac2fb56396bd05a308f63de44ab2e5ef40c02204e8f894f4834a33c2a2bdbf066deb0c83f03d4af894bca55f57ea1f2837059780121026c02770277c1a89a1ee513d83f2c379191411729ad6e4ce1617b628599b397efec103c03000000003b270000040000006a473044022005172c933e9272bb5e716d225af9f37cba8f196a61cf45f1521155cafcbc881b022019c0bfb4591dfc50ef1b94599793fea9e3e1d0e90510d4b2da952cbfff0bd364012103f5e610adba9e7d73a599cd3a394ce2ef24008445713e70afd3f32e245d9c4ba84075fa01000000003b270000030000006a47304402204a108870f6ff3223915296140e15d85938979679e3048e40a670ef0d6b9142ea02202844c44671c41574c070ac417c6c5eafe3c5094eaaaa39016df9982a56d282870121028d6d6fcd89698f5c0800b65eba4365bfd81565ed6fb51b90d904188f9cd6202e","76a914ac45d22f400972f05f9fe570fd9747bb003eda9e88ac",1,98,"fbfa3d61c22fff75eb72de307286370dddeb8af2e105e628f81eb9ddcbeb616e","OK", "4 inputs, 2 outputs, sign input 1"], +["010000000263bc1ace5809711cae78ff7d56161a87d73b254b78dd9a5ed417425b6c9d25b10000000000ffffffff8befe3984892515e56985328b698f37224b368c986aad97c2276eae44a36766f0000000000ffffffff02048f8d840000000000001976a914f64ecb159f7e676dd3d955b5b15819dc9ea6453088acb15d63300000000000001976a91471968cb6be8fdb58fa637860a1159e01426093a188ac0000000000000000022a8b1c68000000003e270000040000006b483045022100ccedb413fdf5f6d247edfd4f2e576d23dd845ef7a774fd387013f5f1a837057802206e34ac69b135a74ddf2e7f6e4e51ca0673e05a59617555a733ece7709b93066a012103746ef026f3ca0e31a735343b2f01660bbee1ee4c822766ceeeb67d6a2f29527aeb44eb4c000000003f270000020000006b483045022100d7cb495cde1d379c7cb8a52e492a5866a199dab207f38f8cba03b4a19f43f13102206fd24ce034226a0655c95912b0b9cac611b06cb7dfb16654a6dc4e97e762f217012103adc8441aa79387904b25beb38038ff470dc148771e9ada665f0cffaa891f311e","76a914dd1b67b36f0927b279914f814fd6eed220ce431788ac",0,11,"d341b36295ade71bc904669d8513172a172b9d343cfa5679540c6ab9f274a511","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000298f4bf806a16321314660739b666b08250c8899b88b8a6492df9edd6f59a387c0100000000ffffffff662c0feb49b03452a403597edef828691d6f4ed4b5ced2c8db250015db9af4450000000000ffffffff020ceee0050000000000001976a914cf7d781a011b07a5ae7672d9ca43bdbfd0516c2e88ac8684c40e0000000000001976a914eca77ca8bd21e899cddf0104dfa2e75270ef10fb88ac0000000000000000024308040c000000003f270000050000006b483045022100a635833cfe677d8d34cd48e1807f09cc4185a68f5232c4e5205554c7138faaea0220544d0136664c612cd662163c92fbf041d3ebc2839484f0588dc3e634475579ef012102961181238accb05c3730902b98bd7202d323b2d9e27c3eaeb8d979477d0058e2af4db808000000003f270000070000006a473044022015c0c4e2d0a09dba7ad3e0af042ef3898eeb8289958edb6c2c042d319a9c2073022015d489d225a5c48e35880e7147631b82a7ce1eb60932a33f476337f8fca4e7010121026550c14e000a74dfb875de474563960a9620522cb7fcad22662a44afec335f78","76a9143d0be7a9bb49147c9d2769d96e07b040c19ef0ac88ac",0,61,"5a07c3fab9489279978951298cc7553886ff85b0ebbeb57b9bc18604126b86c5","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000005ddee1ff6d14d632acaf69667b04b6bfd36b40e7468b506ac503df0b72e72c1990100000000ffffffffdcb7e2a0584cebdfa1b48b087322be1026aa0d6b5624447632b1a664319ad4f10000000000ffffffff287a79557472d6ecf58b0e48520490f42913e1d9f9edd35da0f1e3a08d22dfd50100000000ffffffffdab12c4ad96ed9128f86d34451d2431136217ca38c44d3c1e4a2bb1b3891b7fc0000000000ffffffffac8f77c28f43e6ba5b54f5fbc38adb8d96f71c60e8f9b9057c66b9263441cc9e0000000000ffffffff02b87b1e000000000000001976a91460691f43146c14c24ed9bb785bb4a6468a238c4588ac8d4c51270000000000001976a9147b958ca507c5e87637ea94f510251be510971e7e88ac00000000000000000557844c1e00000000dc260000020000006a473044022042159603d49036f396650bcd879400a99891571afdbedadb659fd5324b5c71040220347537795cf2e56c5c8556c728a8726fd07a79a5e672cc069975ed86f7c6fd5b0121020151b26e3143df7b608e9585de5311d4c6c82a0176f02b8c5f0efb3f454aa500689863070000000078260000030000006b48304502210091345bca8f977c810ddbf73a9fa9bc826f10d20e6cad0d4a417477ff8a391a98022065085789255889d1c1b27a1590a1c9eb9d7330efbda819cb2306aa89b6b222800121029bdf662fea548a78b7bfb2cef9d88dde23c194125910f347715f23dd092211970d2813010000000036270000010000006a473044022071268c15f1f4ed8d2674c0ee1c810624d464f7c3835a297031c921ca2c19c88a022024c71b3af0d356ffaaf1f1c143b5503aaa28a8614d5a423b4d832cc4adb10b86012102d77b9501a30fb09a7f3368923b0eec564c0cabd80353728f1b88d27f7f0653dbe5638a00000000006a260000050000006a47304402203c2b4bff4d165bc239d07940ef4ad12f866ebcb2aa9de92841b80f2bb69b4ee6022024e0c3dff2f836e591595520754138a585f3982bbea66ff3fa4038de87159b550121028a0a51138cc79616806920c32730bca5f956978b27bf818cfec785d9cc519b1f54e64f000000000012270000030000006b483045022100883621b98605641e9d3c8239308570912462e0c804e1d28d14e3dd4e28604b9702207d96c844094c1a55b349369009cb9d0ae821436c610fb8abbf8786da0058ec680121020c759756ae0939423d05819b3162f8347e82b9402b338a7931e926b003c1cedb","76a914efe835db15368c7c298b442461a116dd3cd2035388ac",0,141,"ea617a45c5e2ea5e8af0f2e2020c49944d5ee416dd4d2340d7196dfc3b45955e","OK", "5 inputs, 2 outputs, sign input 0"], +["01000000038cd7fc409c5eaa71cff909462e76b752bc651d34ecfde3a2c82851edb26ee4d90200000000ffffffffff5608fcde7ff89a73b1804976bf7bb098d567e582755fb738e995f0f8a498c50200000000ffffffff3bb4e147a0b79cae14671afce49d5b3d9a802e3f98a30009fed851ea7536c8700200000000ffffffff02af0f4c440100000000001976a914e5b3e87e0bc8a80634511bb6ef550c789904052388ac1e7242090000000000001976a914556bad738e87a14c1e818d16dddd5abd9164dda388ac000000000000000003ef17ef700000000046260000000000006a47304402206444249950d927efc86617cd12d704c817ad0b5bf56842bb21c630af177e9acd0220671e3670eb69e519dfd84af75a1217ee0db899592ba7c7c8c0bd73650a70ea1d012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d4988fda756e00000000c8230000000000006a473044022003cd84a249de49490beb895ae9ac8dd2fdab3a4824b8f9604f42f083ea209d3102206fe357a4dc61fca23303df4b043cc1d0830c7b748ffa5b05a852c974bb5b84f7012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d4988fda756e0000000042260000000000006a47304402202da96cd72076ebd97e0540e0463f1f9957e7652c462be62903ddfbe3dde0ef2902202aaaccd486a39c3ecf35c7235ac3feb279c4416b6ba2ead4c4173c0cfe7ee095012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d498","76a9142ec5027abadede723c47b6acdbace3be10b7e93788ac",0,234,"38639d370c7cedf07ae53f8aed7f6ea99472114adc35885ca0d1213aaf25ad7d","OK", "3 inputs, 2 outputs, sign input 0"], +["010000000255471558731c831091407554f9c2be6ac9058d0b1deaf6d13fb27dfd2eb6b9890000000000ffffffff184f7eb7d2888737348f7374fd3396af5ca9a59aa85bf96fcbcd882152acd5ca0000000000ffffffff02cf5f9a510000000000001976a914671ce684f3d155597fd77478a582009835df0a2188ac6a6f891b0000000000001976a914943cafd6ffceec555943b79670aff4aa4e5f309288ac000000000000000002456ced400000000043270000040000006a47304402206cfd24ebe984ccfc91847ec8ce6c3fac9d02ccbca6eac2ab344e3566a2c8ccd7022050de58239603373e6d2146532fca899fb9ab0cb563315a511517ecd1c79808df0121030a57f0e86d45208f7d48a58d60d01481d79d0ceb213f4c3e193fdf274bd221c654464d2c000000004d270000030000006b483045022100ed580b4af1fd6170ee6d60157ce337c21523155107fdc606519a80b677589681022060d17c4523e6d11f57809756b0d8699bbca182af64c76405a2aa49709a048a2c012102c5340a9839edeaeb44794c3678e9268a517fbbe2dbb7b3984cb7ad9d12f857fe","76a914bc7877c7f95c475db2c4aaf8c4351bb9ac778ddf88ac",0,120,"1d578fcfe7b8316f322a98460b78eb054c6b73ed1954768178ec04348251d6fd","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002eb0ae789c87296d3d171d84cc630dc94c9bba7fcd362fd3f952aa5787933096a0000000000ffffffffe967e9ca19670ce1fc82fde570d15b00e8015718b5c0543e025e4ac5ad7af3a70100000000ffffffff0252e4f40b0000000000001976a91489ddb928e5297a60e17b8b9d657da6ba582b604988ac00ca9a3b0000000000001976a914d0dcdccee85b33121a6f37770dd8820c8f208c4b88ac0000000000000000026f0d1d250000000058260000090000006a47304402201bf20ab677a8da4b0e678f25c4045ea0ca7aaf5dba35c44582258505149678c50220369992ce60bb186f6e14af08145d950358748f864583fec8f61f565eeafa657c0121037b90154de4eaa28f20b83038511f52a2f90e014ca631c0cd55b2de1b70b4401a4384892200000000b0260000050000006b48304502210083f538be48d369a0381aaeb8508bed70b35a11245f878924d9b1b7e10860dea1022075ec72690ecdc94338ee367a8dd2b199de14929be423d3ddf8be27a07c0f512a012102da866e3527917b3b35d10d2796e08cc52f834c51d5364001a15df41aa5b2cfb6","76a914c6efda318823b6c90cb3995d8f8c0950f997e3d288ac",0,121,"715d9892ce0e89269403b5f372299326f9ff85dcc8c2c6ee951e6e7d1abf4fd9","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002678d22be64330afb01668fc8a0620b502522e2d3c2749cdbcd4ebb5433dc76430100000000ffffffff64babc49a46146511f79cb6a7117f01abe00c4e04d819422dbd78d163129b71f0000000000ffffffff0208793aca0400000000001976a91446156a37b1b91139df197ccf905af22890323b4888acf3f12f6c0000000000001976a9147e556d716311b52679d7657f7e29725307f57bd788ac000000000000000002a0f73eda03000000a3250000010000006b483045022100b92d67e02a9e5d4dbe4e3503ed04151e9bf7ccbd361d928003012ba9b7a2210e0220106dc995eb746b562d91c178b4379e22bc27f54fac06274bb1ccae108f656e890121020443606e5dd531b9005fcfbe0e9b25de9c21486542c6ded29c169610ae7abfacbb56425c0100000067260000010000006b4830450221008abd16e18a4640893f8e017a41a6cb7ba0caeaf4a12378c86b1b0292bd2291a202201d5237fee63ec99da82ed7575f7425877199116db2cb460e18676f35b5d21494012103fe01957b06d45a743658cf6b296c6640f4569ba27aa198ec759cd2c96580946d","76a9145f9b3c9994e6d8ec76496865d1d1c9b5e7d2041a88ac",0,9,"df0fbc39951623324e604246def75e716fad1b44aa70366073ce982bd5886330","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000005ef21657903e5f5d750cd4dd9bb03557dc56800a753bbcd85b2fafc00dbbf61380200000000ffffffffd210d747cbeba34ad20ccfbb4b01cf1dda203edeb43ba825c31c4662f6cd2da30200000000ffffffffbcc0dd57c8d37916a9d1e92edad64cc7910ca0b79c54c20b829e94813a4bd6050000000000fffffffff9e131ac3a6508f5b7bc9aa2eda4ea7da47c73ec3783cbd0b1658aa66cd49db20000000000ffffffff98c5fc1bc12700b5ae92490ed3240d3441e6b36701563f9234168a44129487dd0100000000ffffffff026f2a30000000000000001976a914386c066ad8a3a590a59f03b3e638d006707126c888ac8091d2ed0000000000001976a914f0ab8d28d4e8b36fca906b5cdeb9b811ca1021e388ac00000000000000000592c2e47100000000e7250000000000006b483045022100ef4d58ee6198a289e849a73ccf1247fa74f4d77862d1a443c34f2940b09c172c02201795d20fd44a137701e9be757720b9dddd7bb8c6f26d903193d1c12b44447302012102ea7da445fec4aa75afdd581878101f732b5313b9aa606e778ade722609f8e8cb8148dd7100000000d6250000000000006a47304402201493bbe7866009992cca94a4b385e8a618000e9d2359279749b4d7d6b756cd2202204a68d9a65e53b4976e7d3374b395cf913a153189fd82af89bf41fcbd30346687012102ea7da445fec4aa75afdd581878101f732b5313b9aa606e778ade722609f8e8cb1bcd83090000000054270000020000006b483045022100dea38dc57470a352ac4f96c6330a722b27927d15f76a02280ee93f42bf209184022044dc83072970221af6b55c801dc920ca04e80f053d02634c08ccaca3c2937f5a012102715d3641b97e91b31915fb26a1a079d42a4178b88864ba10b1a843c912ce5f83a6d2a2000000000026240000010000006a473044022005a5a05a57edc45418210fa7f63e61f61857f5295372cfc5c8fedf668808b3bd0220146aadc88f5fa38e057eb7ef6a697d50797d1d52623e269cd90134c0fdfc9a330121037f837a1d2ad668c335a8d2659ef235b801c4f0f66cc94f9aaba4425796bdc2945b5c660000000000a1250000010000006a473044022061fc7e75cb9fd6e5e61836a9eaf4af314ce339707628aa31dfbffbf27d29c5f40220389f19e9305e5c4f7529269c264baaedaaa9b4d099e64ecfe599597c388df82d012102f54c48d2547f652f16ff5f5d5e1415230e246fac4980da68dd6005ad8e55f7df","76a914d392618e1d9923f5f49fe6240760ee039eae388888ac",0,252,"3de4bfe0175086bf476854024bac584da4def4d04bb389b30fc0e9ebe7d89290","OK", "5 inputs, 2 outputs, sign input 0"], +["01000000025153127f81c29cbe857c024227530fcb70240d42d9fa2136e023569eb5493cfe0000000000ffffffff2c8876ab140626e39264969c0b5818b1c23f220222457e202b1e45ead0fe5d2f0000000000ffffffff02f1b9ab170000000000001976a914708e87a7460aad5c43415d3135ac681a12ace25a88ac88d4d1000000000000001976a9144de57898b7b9cb551ca30240364d5b82e407bf2988ac000000000000000002878d9f0c0000000053270000010000006b48304502210093dc11a7e0df6e7b0e99990340fb53a32bb4538f9cea8e23a51ed256518489ff022079b1f420368f3d1acaf55f9ed5890aadaf5d9cb55ee79973e5e0842663a54f1b0121029a2fe815e7c63c00311c54a6be041ea3fb44ad48a161a9b92e8249605594c0e252e4f40b0000000053270000030000006a473044022027c31ce2f958629817bc877624205313303b7944b9fa2ac067e18a45fcc469850220530c03496a2f3a145f9c4d4e3c7b35d67897d9d14637d8315d5eca50f56c4ec8012103ad634e78b76270bf65f4faefd4e97a33d2f75cc92c5bc63e3b8a9fcc1e45b0e9","76a91489ddb928e5297a60e17b8b9d657da6ba582b604988ac",1,148,"890c133de7f89279d08d001323bb9b5b381a9824c510cd2e989c0f4410c04545","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000002343e0e25ded28123e1bdf098ebd58123ff8a82b3de98e1154c20dc1b84b0c6550100000000ffffffff1a64c611b07c0556683ae462defa3d00385c051650b02598933627fe4c989c3c0100000000ffffffff02099ccf3b0000000000001976a9146c3cdd3a86d05307ecfb763c9fe5723b77c3bfc288ac6b9a0d0f0000000000001976a914d29193d3fb8e20783ecb6e76291bcc43f6e9d32388ac0000000000000000029e77b4320000000053270000040000006b483045022100bfe643adac41da30c6ef5331bba7c86609724695d340c810bc691128a8e822a502205568868834aa07cc7377601a4cf57734b4a0bb12efe31ae8ac80e0a53e90e366012103f44885e48df4251bca4d0a1783ab201b74a6dd7b42bd17bcd8aa34d28892c7d036a23f180000000053270000020000006b483045022100f1729c9dddfb8fd05fea34d9b23fb8ea71c1533666dc2eeb0f46cb70970cd32f02200a77e56e6fedbb77298ac3b70241aad0c4fbe80cbe477af21ffdb742348fdd0f012102dff1b2927d8253e0b33265c92fe8876c9ef8b5d6499ccdba240e53a046b2d4ad","76a914c48f42bab42e600bd63b4d8a5780ba66c277d90b88ac",1,168,"b92c181094c0e7495b19c46c92ae0c16c2b07b715645c74a6c46d7b674027d20","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000218ead938bc20f34153f27c525c70e8c840417ca9402ff9a71ecf338bdca5ac9b0200000000ffffffff39b0cefd760736d3111068a5bc2664e0dbe6790cdf69ffbdf1ef4de467d2aa3d0000000000ffffffff02d60354920000000000001976a914f458dbc99d018e6bb2f623c1bc52c7394e33474288acadfed0440000000000001976a914878b9661f2ae23b57bc37c0c06f4a351b19830f788ac000000000000000002cf25c26e0000000059260000000000006a473044022040cc707d9c4c7cff8077e9e0a04689f229078f0dc750c1913173bdaabb716460022054289369ffdaa2fb43af921b3975105a1b8379c29ef249b4bdf01163f2146d2b012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e82614c079680000000056270000040000006a4730440220568f1bf5986596f91abc73b2def50a25b964b35f1bdf99863d6910bfeb29b9a0022071f0dad39d4b04150b7d959af3417cb63f3730b66be42fbc0384503ed1e76062012102f56be5d34c9cf775c5132b59868663e74c263f19451cc33cce050c7cb7d90262","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,235,"9009c9926a616a95e385afb0969fcc0b5ef732023ec40d89ced453060a6665db","OK", "2 inputs, 2 outputs, sign input 0"], +["01000000025153127f81c29cbe857c024227530fcb70240d42d9fa2136e023569eb5493cfe0100000000ffffffff308aabf60654b7f785638f1fbb7764ad10a810f6cbef3f3998b6b2b404e001410100000000ffffffff02b2bee16b0700000000001976a91459b1e701b1514056b9838896f4f07fea8594dfe088ac7870362e0400000000001976a914b85a8b6685e0364705e3ac34b878bba4869cde5288ac000000000000000002c028441c0700000053270000010000006a473044022043a148f884d94090c97bfe8066925517594ad1b0f8de42cbf798f5b94e577ee702204b56bf7fcbfe73e1ab8f4d7b48d9ab1bbe45a23cfc8716cbd4498515b8fa3c15012102aec44c266faed1a49da60731d4f9e8b1caaa687c1f396e13f68f2e6ceac05ce0cae9ea7d0400000056270000010000006b483045022100ae18d377866b6b8e3882f1a46443935eebd2023a97d283a86416519a5853defe0220315f62303aa07f97b57bf32708b66c504022dff03e975f797e4f7f0fe8834a9d0121031429f43e5fd7d67e72e3ea223e4fff734ca4ee7a6466ad63064ee4b4e22cedfc","76a91423021692428d8a53b4f9d879c41d971495bd5cbb88ac",1,70,"1d4c7e4702c6b3777f51863fd324f53b445f4e30ae5167e47ff6187f0f9fe6a1","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000002f127425694a762979acbf0fb327676029bcfa76105d0066b8448cd033de4307b0000000000ffffffff90d8c28f9e6c683560cc084b888079c37454206b96308774692d18b41b7e91940100000000ffffffff027324a63c0000000000001976a914eea01d5a056c492f358f0cd29319ec5fba60066588ac224e061d0000000000001976a914c81273be5527b1704f5b4b2e4539cea7bcd7716888ac000000000000000002d56122350000000083250000010000006a473044022012dbaedde1b44f47ab64b225c6159b2a8bcce7d0ca343feec5d84181492246e602200b030e6cc2bab943675829140411f9ad6321ed0119acb9e43e0d249dda576d4b012103c42a2d76f372c9de53eb5bb3ecf034c326a4196f6d5471e0c07d4b061d33f4a020f4a0240000000056270000020000006b483045022100fc43182d458de843715898c33db6ea9e29c2ddcc51ffbe2e545d4563bce22d710220748ab7eba79b86c88797cf09f14809ca8d2ad5325f0d5eb04c2861c26b860e47012103f5b361afde8895042abbd2a69ff36757936c85dfcded70a9f3ae96ec58282404","76a9146c740a8ff7f989a38945759e7b2c32447f42d3a688ac",0,69,"b2a063a03faf47abb249089a3cf77b2c06b08007093aef41b1467d0c2e868a73","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000003214c1383ed9d9fe29c6b51080041441ff55b96061376d723dcf13ab07c349ccd0000000000ffffffff4e7abdbe4c32930853cec57a45b52fd8afad3b2536453bee422fdab6acc2808d0300000000ffffffff13b34d154111b3c66a756f054ee7cf46203452e5bd56da6abe9b1141657c29750100000000ffffffff02009435770000000000001976a91487b2392cbde3c732716a3ce02099be7b9af841db88acd1f901030000000000001976a914331ae7e20019025e756a26c247de6cfaedc0021688ac000000000000000003099ccf3b0000000056270000090000006a4730440220147c5bd9156e1beec9538de6da0cd35942de00bba2b23f152259bcf5618541120220381975d99cb55bfd70de164229f86a45f27144de6cb93818a86b07fe219338890121038d3f7ffd13431f01c0e5ffa206c2ae3f30d7ac88de0dd0e1c1a3347cf4e6b91af0214b2a0000000057270000010000006a473044022069937f0b76c84472e34ad1a16efe47c44448278d8420356b4d42ee4cf0c75f0802201546da8a362800be28da6ede1a74787f90b7d5b3f86651e536963b60584ca4640121038dd3d5a07e69fe34cb75ebf3d273521a0ba3f6e5f159fd32b3e70c995262053c38b333140000000056270000060000006b483045022100da452c1686c4ea69e087c22ae679676a2c373ba2dff66703175bdfad50818541022058dcb0c5cc59aedc986893465bc8720356e32bfe652984fec2ba234d50832a460121025a25abd756bb330d90f89f2f2ad8b6f3a882dac823c676512326f67c16790c78","76a9146c3cdd3a86d05307ecfb763c9fe5723b77c3bfc288ac",0,107,"e9a5c1e547b72c5f85e74b8ed5c21c5c097b64dbd5c002d49139664d29e4cf9d","OK", "3 inputs, 2 outputs, sign input 0"], +["0100000002b687cf4fe1738037af84abfc47ecdc56d4435ac8d9a0d9718b884a5c933504460000000000ffffffff308aabf60654b7f785638f1fbb7764ad10a810f6cbef3f3998b6b2b404e001410000000000ffffffff02199781030000000000001976a914b32242be15570428c6e6a4013ebff72952a18d3b88ac671c98090000000000001976a9149a31762e9363ccd7fa57bbc7e4006441a05e5c8c88ac0000000000000000026609a709000000005d270000010000006a473044022059f0942256763191fbca85588aafdadae47f52086ab7bf0809f503b0fd6d00cd02206d651febdc50c02026d53c9fc368197633d2820ed78d2c4eb77d44f99df060ee0121039e88586f77f2839f3a889ff0f794969350ef07397fd233a50fd7897a6bc05caa7a8d89030000000056270000010000006b483045022100d87cc2f1cb9505981102c256dfc7adaee509590dbad08b5b00dbe568eec2f89702202ffb8dd8dab4959c7176fd0f99912985c045dd7be8cc1e003494007506a0cae0012103c94ca080f6038a0192b989e404339b0323d0fb993842e6f19bd1ba9910db0eaa","76a914934f8fb5764f65a57a348140af301eb9c064633888ac",1,165,"b3a0e35f840e81f97b1f1877ae69122578b801e630749019385cb30182031bd3","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000265e0ee4dd89f7a95d83237128e8fbd6da1ba1029f84bfa349aa9e151a3dc36c40200000000ffffffffcba9cd6af2a5eee77bc08e24f2349b5700ec584a333a513e33a0f3634510f28c0000000000ffffffff021092e0120000000000001976a914969895f407f4fd6333586b29f629d94329e6942688ac10a89ebf0000000000001976a9148b271d92f5c960590e87592a055dddb98433d15888ac000000000000000002efbd8c6e000000005e260000000000006a473044022006afdea787ca50259708d244f81e87886e7a8d74121d57c05036f6e49adae51702203adb598188667e8c22362105e9adc9d187405b99a8600d846f69275197713fa20121024a2546600c5cbcb53971a19e2d481e69b6f39f8f8df55ebec340eaf238f5a4dc915f096400000000ee250000010000006b483045022100919690b5f0b61395f05343c18bfb88446576aeec70e02a171bbdc95fb7c688280220656e255eaae902cc450f69fa62e59ff95a3b330f9f7aa38147f9224274b987320121034eb42648b39e868e05bbf9f49543042ca209282becd696090cdfec93619d8d3c","76a914580fdc2d85bcd2d707dc07a6030407773875429388ac",1,105,"cbd44d992b7bbfd561d36f87ae76afcb4ac354e0f343c13cfc929569e76276cf","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000002292984e7392b1990baf3bf03bf9c9f48a6ff73cf28d1fe757fcddc8ca97427630200000000ffffffff401d2a9ff9eebbc96a0e27a89eecbb134f75756556ec934e85ca157da8cbc4b30000000000ffffffff0297d6a61a0000000000001976a914a173773a9fe7127fcc69bfe5acacb11f859acd2388acb372d87a0000000000001976a91402dd4bfed233b365f0788d8749f305281c89d6f588ac0000000000000000020f68d16e000000006e260000000000006b483045022100f9e29beb4047a5b3ba9d75e650346c95ec8c67915b5592964639b682e1345d0702206dccfadb3164b249e9ac51faad54505c267f4be72336141fa6712f8b8f185b93012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8269bc4c4260000000067270000060000006a4730440220674311766bda0381410c20900cc5c4e790180c2cedc3aa65b228fbe04716156302206928d9176616ba158117e381969bc4b5f5fbc203a03820b17b8a0da2094445bd01210252bbc11e88d518b2a4ba7339b8bda6c35f777a189d06399006dce180c1b84416","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,47,"b6ecd4a02fc3988d5d12097fca60331b7e89f2b2cb899a75cd580ca6b1a88a2b","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000267236e9de24ed6cea61ead2e5fc834d0c00cc5ea6bb0cf6758c484c3396ab53e0200000000ffffffff6e0fc00f8ec37add3d5bc0f04a6573c19a22d5486330eea8dc45d748e9292eef0100000000ffffffff02d308f5260000000000001976a9143d6e0d4b2b77e268c7e5020a7b6be48d5ea5b23088acf500bc760000000000001976a914b333ba21194bff795f21e8e5530457e77359170188ac00000000000000000292da756e0000000070260000000000006a47304402203838cbffa4aa7b4922f14f88f5603fcd3f9ad350625546c5d23625c5d5bd513b02200748bbf84720f12cf28e4f069d2585d0aba94b59e33d90202d34ab373b8068840121024a2546600c5cbcb53971a19e2d481e69b6f39f8f8df55ebec340eaf238f5a4dc9612522f000000006c270000010000006a473044022045b272e2959e9c14bc100eb163ae9c65df5cc25c465fe872fa9b563f3eba1b3602204e032e2b758deb8ecb36aac2c61a6304af55b6a10348bbc6d403d7d2a2a79bec01210339f11cabf7c786372ce833bdaa434c448759d0d55f7d41c8cfa65f43a7664f19","76a914b60ee40ada8e797ac6e363ad8c781155000ecf7688ac",0,111,"01556613fa029e3917e773a062179a7005a5d171961d6ff71913ca42ea945e2c","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002c07ea5dc68cf6c974b37afd2a3d5493f438e6c491fdbef27204873a409dc21080200000000ffffffffd61d9c09a79f8537103d16f4fdd4c2bb64a4866d327a68d267dc2d9f09bb8ba80100000000ffffffff02e3bb7a7f0000000000001976a914436c6d367c86f3d377d98eb7b7c02f73a5b5192588acd9efb6180000000000001976a9148e48664a9e83efe2534018a8b2f2c8950a1a027c88ac0000000000000000028fda756e0000000071260000000000006a47304402205431628b8445aff33fb3667b3660e65bee2b8038d0ded610ca3562de7e6577e102201f8d20f5cf6176af8ca8f355eb4c944e0c41af7c276539b0427192fbfb385aa9012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8268db4d2290000000068270000020000006a47304402202be5c80807e8f81b08008f762ae4f9f427b86556e4a5b794969f1334ea27cb430220480ad914d088bac7396b123ff77b352112a9818c8899b5ebba3c48fdfb4f8d8a012102775c4520d5c23e902684f6c4f68f21f269bafc43994935364c4c2ff5b331c787","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,114,"41fba28eca26d2b5e0af24b03726da523c08831c0bb870130d59ed0d36e26b15","OK", "2 inputs, 2 outputs, sign input 0"], +["01000000021a474a2b72cc32e4ddc9135de956341aed9abdcb8ed9dcb952a37800c5831d7b0000000000ffffffff25ba3f7ff1229583c992d54b1131b4ebab8fc931fd8bc8a714a2c6bf896f52210100000000ffffffff02e306521d0000000000001976a9144bba077bc43ef06a5e6b7b69d3f69344718626f088ac0065cd1d0000000000001976a914c920a89dde5ed5f2253301d0bb405b02ff636a2788ac00000000000000000248f5b91d0000000036260000020000006b483045022100c6ac16a8efb63d925ec7b2a57fdd08f4a9e9da5985450dc0afd6f328830a0d7b02205099e4272564083d77a8f078a493e68babb7bc0ca1be77b91f393985aef08a2201210229987f6f80f0483b810f657780d87d66e9b3438aa7e014e3c3d1e6aaeba8e825dbc1b11d000000009f200000040000006b483045022100db790fff2dceafae9d11c2c71da205d0def1422fe8376a16582b9caaa902f7b202203f9ab4c23c08de55bc72dda5d9e28510a1a47441e3323434ee2c01eacebb6a6301210229987f6f80f0483b810f657780d87d66e9b3438aa7e014e3c3d1e6aaeba8e825","76a9140e67551ccfa0243fddac8f0309f4dc4eebed3d2288ac",1,114,"3b3c677d0231c2c39d75665f683e25783115abcd72cbfaa85c1eb4c89eb3905c","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000020bad39def9f1021ed51320194c0840095de2b97f1956909b631105e1e117ca4d0100000000ffffffff0945293bb4e84ad9bc2c28125f53016d86df4665c54894d59eaa690f5bbb7aef0000000000ffffffff0256e3a5070000000000001976a914d4a98636b4978281c17b3162cc1442a02672e24d88acde2e8b390000000000001976a91487b2392cbde3c732716a3ce02099be7b9af841db88ac0000000000000000027819fb220000000060270000010000006b4830450221009946242421630788daddd12fb39c158358a7d8f910119a372cc14d033068979202200f03340aa73e80c7215d5e452f3993f1c53118bb97c210753b2b5adeb9229bca0121035579fd554304739e376bef197b6669259b66087cc322b9a8731703a88a61e05d1cdc4c1e0000000064270000040000006b483045022100f94842215fdf94b2894821b6984a9986d7756f4f568e960dc8084275a27599b402202427cfc46980f871aece58b2d5cf6212697c018e38d8d84be70f13adfc61931e012103267d3516c0579a8c5218ca8874740e7e23389d52eca48bdf581b5b822d079d55","76a91471d90e3dd58bd39ec5d921d342d7fb2700a60bcf88ac",1,238,"6061ea8a4eac1c426302412a108a4e0b9f90442a631aeca8d4422ad2d0fdc6df","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000031a64c611b07c0556683ae462defa3d00385c051650b02598933627fe4c989c3c0000000000ffffffff1dc4a8031d85cfab9ad3285c73dea1a745ec01364ceb73cc3020ffe93608f4a40100000000ffffffff026ed9771261faa5df18a40f73b31bae1cda1c24f6654b2e54a7982f7fed19ad0100000000ffffffff021ba0db190000000000001976a91484bf1c08503bc24db86bf9c0a630c6196b9c47ac88acc9e451000000000000001976a914ebad8bc2381e1ca35f83dfd7ae3483d787628c3188ac000000000000000003c07fdc0b0000000053270000020000006a47304402200476bea37cfbd0be3caf7f43d6874db199e223456fbbf0a4f0756fea6936c78a02207edc0ccbd06b506073cbd49006d81a1a040acf085f26b0eea48ec41d04e555650121036113899ef9ebd2f6a7b52722f2ec08b4d6521c79572887a775c6ef4dcc284212faeeb0080000000063270000040000006b4830450221008c1a74fbcc2987db01ffc27aa90d738a0381323460bedaa9e900b661b6cc32930220078a8fb5717161576256bfd549955e2fde1cf49c010195d790997c578923d7730121023a1a32834b9bb80d82d30318f53fca12fda5dc0f8c9e124c3646838c32c2d0bc8af9b6050000000061270000010000006b483045022100858f79af249a56db0b30c93449fbec781397856825f63e9f5c4a63b6a76b11010220396b509baee33005772b3ec69f94e5f23c3672e38b6ce34a6d7fefa992891e25012103e8ba7e8125a1d75c27742d26eff8b2f2e26fa3833a0c15d212f2315b5b8bd94c","76a9143e475479f9456f5059afa14d34aaa831d7eb38a288ac",1,192,"a19d06e012a4ecc11eb1d2a2d464156b68e3aee442823642a21e7368e596f579","OK", "3 inputs, 2 outputs, sign input 1"], +["01000000027392998b7cf6e75148f790e9364b934d4215f5b23a33426af3fae4b5e9c63a790100000000ffffffff0dfcb9a374b91ef9630492aafdb6236779f4ae6adc35bfc9f3448906bfb2cd870100000000ffffffff02f2cc39760100000000001976a914b677106f14e53cdd0c57ae9d43ff2da34e15218188ac76261b621400000000001976a91439e1ba69f03231bac6166a78265ecf83a871433988ac00000000000000000285d93b4c12000000af250000020000006a47304402204647460221376645da7ab3f2c994eab3a9f1e05b72de4bd1dc0948b9c8e03a19022016baad0dcfa95215c5bde8ece59c1b0eec0a2468bee8fd96e1ecf757e394e0a101210319305972e04a035cb16d9cfd09d46671f5bf5cc8c37f3ab765113b169a83b7b643fd2f8c030000007f270000010000006a4730440220102edd16a0edea421adcc75eb5e58a0a4831b87d5aaf8f3ceb5d6cbe52312dad02207c1918625c48112f523d7c60e5289d890265f8a4b9dd23bef10f32db5470f74a0121020ee220c8d3f0ec9048c2508585439aafb52f81409dfafffdb1b7dd9197e5be99","76a914954031f93d88a59aa7677c1bd629cf4b1f91d59e88ac",1,34,"14e7ae9e3976e77b2a03314a19d3576e1a9feb707482ded421e889a7d9c68d70","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000522447a1f12fefd129d9c7f2c124752828a2e05980bc80267cc03ca84b9f738840000000000ffffffffdec20e140b6738a99db2583f16d8497e0128720cf77c8ef50af42b1e65c77d780400000001ffffffffd2a2bf1eaa62c1c1bbb73fc41c3f0b40688486efc07c13b58177776a6fc5188d0600000001ffffffffa56509fcc6f5acf535ec574ea5d20729dcdf8be583a9d850408d4be9ea0cfb5c0800000001ffffffffb6731914318c8de697af87a4db7396c5e28415c2100e283ce4019c9cc3b94cd30200000001ffffffff023cc87d000000000000001976a914efb642e3dda63c76fff807fb34873e36c8c4b8b388ac00c2eb0b0000000000001976a9143c561807dc79c18f0cee83fea6ff5b318130f39988ac00000000000000000500e1f5050000000080270000010000006b483045022100f1caae3659da9cc6bd32d87c4d6c8a0c01a026d4b5813e4bb56168f3763f5f3e02200c6429400f18d06558819839cd73d3f63044f4b7e49d0eed1b4b3b57ea8cda5e0121034d967a9303909ba5504f5cac72b3d4720a90c96cf4589839c63db9db9168ebcbb1002004000000003a260000170000006a4730440220366c95c411e9943a4d7d2109da8d96162190556d784eaf137e93da65e651d25002205191350539054531ba57b5c478a57d1ac7e826f7736665f87255322f33e787b701210236ea2eb83c7cfdc5e335d60f08ba285b5c40336bd41e90f5ad103a87e382ef2d26403301000000000c1e00000a0000006b4830450221008aa63982e203722cec4cd4c16cfb78a27d6e3c0cf58171825b79bfef0472d64602201c5667e27af36c973e0a6053d0cee58b56f9b1e01d4c61d6ed35e56a2aa0abca012103dd2018d1f7d518c61264b1339d3e3b5817cbb434f0958f96b4af3401a264f7a5fa3ae10000000000bf2500000f0000006b483045022100d622f9cb9ad17f4a24ef031e53e6c1a725757b4a6c06d8c9affa909e2cbe9f190220329f707d2f2135bc51e9793b89dcbbda8157761d496450a13d9740d7ca83f0920121039aafc4fe8888b01436cf8aae34f6dfcc54b3e90de503399218d6a24e1bf438d2ebc3d7000000000062270000150000006a47304402207a7fa5a7ee362b7c8076914218e7fa51d2287a60ad0053618cab42d33ed1f73602204800ddd16de118bdca29481b8511216fdddae36113ab22fbe8c3ccd64972c9420121039cb1d74b2d02e97e9182eff88806bf5f4a7b67ae4308df86c5df0e2f10fc072c","bd76a914cb7a1ef649afe299f0e0d7cab1623b43d5587a9588ac",1,35,"8c794212e03bebeca17d13ec0a7031f54547a324dde1a77cae4199119504bdde","OK", "5 inputs, 2 outputs, sign input 1"], +["0100000004efc713d51efb84907a1d95e6b0ae5b237540dc59a42b350d504d50e70e08c75c0400000001ffffffffe7f4162ae158fc3479df30da575987a40991f883f8e53f9ead8d6c370f99ac090400000001ffffffffe06842f22f636fca8569d6992ba466fa5524fb5a41308162bda936714a9de0b50600000001ffffffff9c8549063d054fd3547e7c1409e827dc276b989a202c6601dcb720d602ae285e0a00000001ffffffff02640c22000000000000001976a914c1582082528cf831d9e278f8a4e231665c288ff288ac00e1f5050000000000001976a9143c561807dc79c18f0cee83fea6ff5b318130f39988ac0000000000000000046333a7020000000071230000170000006a473044022026cef9f3179b748b6b96ec1d8edec18f172aee32ae0176a7ea6aa180ffe04e3702204b5ffa25227b9fea211895a1902a0ce2508647000a2d0e665bd6c7f9d432d3df012103c4e292e9a0cca0711e5c4f0149abad521381d52bf304df97d15ba8acd91816fe6333a70200000000dc230000050000006b483045022100a1ec6d887eea416014833a0047e9938d5202384abf86983309e451bcdc9939ac022032deef054f181e25a5f3dad3b5a692e4b8b3a02ae58a2557b343ed08d0214fca0121033c021118ce264ecd4e6249115714d870882a9924f8fc5aa61aa5be69fa28f6e9b72f90000000000073230000150000006a47304402202940d9fcb737cd006b08ae9b68c1716874cb621cc35664886281d226926b436702203e016a162b8c42ea35134b302f15f71a49d243b4f6245ee992413f4fbe6839a50121032f3f18de0e442aa72ecc92d3b71c508a0508f6b63c182aff491f51ac862f764927a2850000000000ef250000170000006a47304402203829b184c559b58c9d6c5d537d6f6f7f754d0f7bc56e24b0b1bd3c85f8edf8cb02203fd97b3b4e00abf7c2cd90e7128f064b7f1a68d75e23d44caca878a33e63c6fd012103cf9104a1b8069e3e6c5a4750d5740c9f9e4aafb687857ea30a29e0daf21a8711","bd76a914185d6c4ec5d74f7be05ddefb61d307a594ed6bd588ac",1,129,"ae92df5d7123b2810a3ad9d1c15a15e6e0282dbcb97147770b91017723de4aac","OK", "4 inputs, 2 outputs, sign input 1"], +["01000000051a509b699081779116d42dc284f3c1382ff6ad08b97666108f45069cb025cbdf0200000001ffffffffef84d0a29d37c2d8b986da3502e22c9ab70e86e6963958214b0b85d48296289f0200000001ffffffff5bd7dfa3f1035d0dc6a62c5e57f0afb6077c3175ae55fd680507ce4fe99e116c0000000001ffffffff8337cf0bd4a492290665a0cb22836ea5e68ddccfa0b74b7e12eba49c843a78aa0000000001ffffffffd4904ab65d1b3e78f4051163e28171ca631b5683d6698bf658df87fcf1ee53d50200000001ffffffff0200863ba10100000000001976a91414f542d8c6defb4b0b6bf1148b6b24bceca11ba088acdacb1c130000000000001976a914de8a9e4d665b5d40dd647fd8ca1f7809521ef56588ac000000000000000005cc37435f000000003a260000010000006a47304402207b49b76119bae86e0f51ce1a5222ac5124608f7de29e4610233255734b984dad022002950560b91ff8062d3dffde6c1994a312143453287d4364b7cb00aa8636d9fb0121032b127280718aecae9a522f84e9d477f48512a51deb53e46c31a0d589baca27d7cc37435f0000000002260000030000006a473044022074b95574f12df37d35132e469b6735f3fcd676998454e8ecbc2c4148ed1a2cde02200dbabfa41d6ad93657401d965b4fab11bd67ea515bcf4f347aa0bb978341914d012103ef83c15373bf3e6fceda34aa97767cc58d179892e47c1f1607c0e06abb0d2d78defdc45300000000312600000c0000006b483045022100db6e120868162a2bc1f82e07aad90ad458c6d2fa03581c2ac7904ed06f909a6b0220115170b6daad9d3968ffe13e5da0737bf575bfe608b3fed3188ac3360c7e701f012102c9cb3c037b48adeb0caea64b866dd498f46b024a426d13f2dac030777d46a35bdefdc4530000000071230000190000006b483045022100eda79d61cab4ec251062cf903ccca6145451c2fd8aca2d1d5c93923d66ee18a502204affed4f0d007a6a4687adebb90f4b31454813cb7363f0edcf643442cbd78ca501210396b720b748e8c7d143315458d7bed8811d60d27b4bb25ac5810a010cf11ad08f067de04e00000000c42000000b0000006b4830450221008f6ca0946a39392f28b544457a6f77ab8a3722ac5ee74e05e620017105addc1a02207418c4a579b7a605fa10a1a9b6cae10023097ce1ce0eb09cb14620e5a7d5472301210388f1741dec84d165f01bb860fbec29249ea26484a9a36b863f5a7fe7c997aeb7","bb76a914df50e8631a000d177fdd2adb9b08aab97219c17988ac",0,184,"495856bc809a271327b69b121c169709d3af92e289c73491cda4331d02a13c09","OK", "5 inputs, 2 outputs, sign input 0"], +["01000000024f0d70dc8b586e492a82120e23e970ad35e6808ce494ff253e20b32b3818e5000200000001ffffffff8e0447be9d574b862078786aab1932f148cf5b6e9f6735c56e11542de7a4312a0200000001ffffffff02009ce4a60000000000001976a9142bac7ab6c7738233d9aa71884f5c4b19c7c29d8788ac92cdf2060000000000001976a914c4cfa0eb4aea7a82ddee5460dfbdd86824ed15e988ac000000000000000002cc37435f0000000017240000010000006b483045022100a5a6bb705f8b2984c98a0544576dae968fc03ab6e132b6e25b16bb93d6856baf02200117770b24fc7f88c40b79030cec5ce3543d99e1dba43e0d6eb0a0388d289e0301210357a6135950e4ea96814787bc8c66a9f355003ba0cdf38ae507ce8a9938ddb9d2067de04e00000000ef200000140000006a473044022075f87743241858681ba3a406b071315f092c87de5da845e369aeddbc45a80e0b02200ea9877755fa13e6a3bcf145d3c4e9c54012917ff3b68603a65552ea25cb71e2012102817865ba78430eb73ee4786dfaf8cf9346809676a97bca013e4e6b5c950f1c28","bd76a914d26d1bb4aeda83ed088b7e14a7eb09a9bcfb8a7188ac",1,168,"2d5f2a877745534ade72a5f2c7d24d1e0d758d4d680b708989559ca94a707d5a","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000003b3ec711132c8def07d3076db017974a526ed762a2e6b3db8fb4663e1159412810000000000fffffffffd21c3d9f12891c8a02b9b3390d6b653ed325ad33c84941dddd1e02957fea99f0100000000ffffffff46358c0f93302024654cd189afa29863aaa18881eef0fc68131a8c16437b18e80100000000ffffffff01c071062a0200000000001976a9144b9ad490ea901445f6d72fca9430dbfe09a5548b88ac000000000000000003001a71180200000084270000010000006b483045022100df2c5dd5f4f78a417b6264b6588d3b8955e238cdcbb94e928bb233ac427a072302205e81c3f5058966561e5977ccdab5a1ed7be8bd086dd30bb76f7d3c349e4e2cb7012103486c7edd3704c2abc8fea2394b64894196cdcbf806dab77e6d8ea8f00ce4dcc500c2eb0b0000000084270000020000006b483045022100fe5711a1cc3e5faa5065a306d2cdd998cfb623938d84273dbd19fb4e6eddeb1c02205066db8118d9e592767a4b20c86f8df663d64deb6b33174f684a714201392cc7012103486c7edd3704c2abc8fea2394b64894196cdcbf806dab77e6d8ea8f00ce4dcc500e1f5050000000085270000030000006b48304502210087868a6b260c2f2f382bf92d118615d4f9b730434498c8c736cfb7417bd69ff702201bae2a758cb32c2872e9af0da8e2d680e52e606f1f4559618d5a71cbe4efde2e012103486c7edd3704c2abc8fea2394b64894196cdcbf806dab77e6d8ea8f00ce4dcc5","76a9143c561807dc79c18f0cee83fea6ff5b318130f39988ac",0,117,"6df772cb2b133c80a96a92df103fddf3f24e0e381fc366e3a5dc0c57bb1d3645","OK", "3 inputs, 1 outputs, sign input 0"], +["0100000002b0b133106cb86702880f160b0d5841fff358aa38f8649f40db18145da03de2e60000000000ffffffff13b34d154111b3c66a756f054ee7cf46203452e5bd56da6abe9b1141657c29750000000000ffffffff025bc42f000000000000001976a914c58a4f5de5798ad0b99cdaa93d2a53f7bf7354e588ac2761f1020000000000001976a91464148b61ebda7d13bd75f04f1d877a0e598867c888ac000000000000000002c3cafb020000000083270000010000006a473044022056391437f398e83227f7ed3965641d5c10aaa819b3715a07f85a46c95a92476d02205433d8f86051c22f598e1d6bc4ed6e93799a0821172baf878ccaf2424c0283920121035c803630112561bd1f167e7f08fd2a544e0cb0c89e0f914fbc023a13cba4ac4d1f3e3c000000000056270000060000006a47304402206a2b20e452ed2d739f369d93e627a112353eea912ea8a51458f58ac8776ead0e022037d4e107f5f98aa162b11a13666fc13108729d74000ee69f8061560afabc9a38012103033a9a45624033eb453249fd0f262d673d0315d2c782d009d1214b12cf3bbc63","76a9143d0099a4e789d3378e7971f3c00f9884616c6dec88ac",0,11,"4158025c8efaf66308f60bff4e30d1cb68760a9b23db417324368138baa5ef2e","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000392e4cde687d2fb2ef2e5401a1702388f1dc70599b784e8f94eb888a20be495110200000000ffffffff9e021f4aad350fae62ed3137acfc995351f8664e763db794452df58d9deaf3c90200000000ffffffff43e31c401f2c7a65679a2cf5d2c190e25bba1c346e2376f3e4e84530722a97f50200000000ffffffff02bc12c02c0100000000001976a9146c346e1988daabe13cbb87e0286bbb815bf85aaf88aca127d0210000000000001976a9140e1066de287725b7ccff13d61820d18c45d3e48488ac000000000000000003effc37700000000093260000000000006b48304502210082c02d0ffe09d0c965a7ccd73484858bb17681b2c7fb65d7d8944d4b1698083d02204d47f5d53fc47f456e583a55328978d3b897d38cbe828fb26379feaaae114d38012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d498efeabd6f0000000086260000000000006a47304402202dd67e39c23a329c3d0f5d5bc34f3bf123c74e7160dd9db221a97141d4d661b702205b0d1b0140904ad339cdcc8ee972d5c302dd0030df0f9b0d0b4f7a42fb01cf9c012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d498bf9de66e0000000094260000000000006b483045022100b431a78fa9c874bb130faa21cd6d0277295872b07ce94f6875a67fc0dd38131d02200783b420941deef4e2bf4325073235ef187a338ad87296348fa6161c2ed005bd012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d498","76a9142ec5027abadede723c47b6acdbace3be10b7e93788ac",1,153,"bffe7ceee3400c65f06691ab154964bbb8062c0d9d1670e1e5c3586d7a42e113","OK", "3 inputs, 2 outputs, sign input 1"], +["010000000210e9b6a37b3c65e15dfc087a4b3dddba5c02bba7ad299261f5657005b84c12bf0200000000ffffffff5039336390e9b65cdd05c407e6756a455a5ded6e8c109a62a5f6fc4e0f7961700100000000ffffffff02e7ac36780000000000001976a914212a84bfa434ad74acdb8360068b6168b3b8840e88ac0b0163480000000000001976a9144b3901ed2507c020fee3d9e525be7387d4f9fa1d88ac0000000000000000025f6b6e6f000000009c260000000000006a47304402200f25e823f8285619ff2cb99d3e5e1e59a14cef75ad7dd7df72a8f685d3614b4f022009978eaceda9c78b06f33c7f58aed95d3845186ad60ff6bbeca5689996080460012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826f32542510000000093270000030000006b4830450221008561e83257b57a9f73eae1512d0f9675921568980f327e721f226c499043624b02200b47004f68aa908ea8578d2bd4363ce9b97110bc8745373b9e7adc7a5d22205b0121022e33e573884999cd9b05e8af1e8b50c4a0b2573f52cbcf70cb97137b8288b054","76a914fa9eb7abd13ac1662cdc4408b2d2072f911509d288ac",1,236,"b0796bf814b89755653fe53606addebbae67c1d4aaedb2f23d7ff2a2aec5ebd8","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000021269cb24fcbede8bfcbb17a548b1cad32c80abf0a0edf0ea079a00e9111e48fd0000000000ffffffff416ab0189dd68bf319ef116f9e94403b3bc078cf3c747c423e823703a150f4380000000000ffffffff02283f1da70000000000001976a914f5028394c0d7a2dee4969a28a858efcbc9235dbf88ac1e1f04080400000000001976a914c5fe6b3a658d1fb2e590528188c16fa862c0277488ac0000000000000000026a70b3570200000093270000010000006a47304402200a2c7962f40392e6b6133611e252e0df76b71ddb78244f7455bf0b22c7639d6d02205e3e6e3f989c1e6d727dab71671cf0540ed8c707e01c1623bdca2fa7b4f5a62f012102297555afb3dd3951984bec1a77779b63f6597b21ad38914f9460184e31ed76223cd18457020000009a270000010000006b483045022100c25595ea41d02fb4c5fbedc57ed6c5b94c52e0305c51e5a0e3849a92e9f4226e02201ea9b781321ba05ba0e812ca1684b1885ff30bdebf468ccd04811862b93daaed012103044c2b69b4053dfbfd3cec33fc4913af35d50b5250bd780f68c0455156aeaffa","76a9144890c1127997d17c1e17fd0fb6cc092b44ada67588ac",1,100,"2da03dc70a74b0fcf2aa99e5ecfd8f0b3c6d294c47027573bfb6d19344308e45","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000002a2d009ad9fb7b2726a1d9007582d5d5e9a7e4c4ed5143029b06290398f4426900100000000ffffffffe68bcb9222c7f6336e865c81d7fd3e4b3244cd83998ac9767efcb355b3cd295efc00000000ffffffff026076fa9f0600000000001976a914cf013fc1e930a8a0d70de9cf3ca416d793928be488ac70b278000000000000001976a914c35f5c7faf5c1956faf2584be2059ce1c80a607c88ac00000000000000000200c2eb0b00000000df150000020000006b483045022100cc6f7a9efa7d64a8b334f905c835a4718e224a049a53d5cb566ae1c7512345b702206b1c7d093f72de6372504cfb2e550dd67bd630f0704b0ff46365c9f37d2b90f3012102f5cccf4abcac1b884a49b94e2a0b665f2ce6db20a1a9d775bc206f7877c9e0f3e04aa7940600000001000000000000006a4730440220191e6fd1d2880cd544cf2949ac514f492a61e9d6072bcc00c14931cad224e1ab022033c9ec127023b0029d6f892ca6dc3dfc1a482f2657101f17fc5d5ee53fec9260012102f5cccf4abcac1b884a49b94e2a0b665f2ce6db20a1a9d775bc206f7877c9e0f3","76a9146c0bd0d662066b4507c0fe30f96e444228bb7b9c88ac",0,144,"0dfd2f804d77831ea16c7d84f6106c01c0c6e8115a8a0c0b7dc49e6185f419d5","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000412901fba3a8afc9ee1211b853238ace6c64fdd0a1001ad94adaaa135412f86450200000001ffffffff37859ac4b7695397ed9a9db76bfd1363a63d547f003aad33adafbcc2c7459fdf0200000001ffffffff5549564460985f60639d3fdfc556f9b0bbb08f71513298ada969ccf1030edc1b0200000001ffffffff06cc16f7dc769d21e7f0b42579ac4ee10a3d9f5f7cd77f2141cee13ede7d7f9c0100000000ffffffff02008c86470000000000001976a914043f4f0fbe1ac3a42279ccd768bb6f90177061a188ace83487000000000000001976a914e81329d93bf35b1225ee1fecb564c0d5db94485d88ac0000000000000000040e8bf7160000000071260000000000006b483045022100f5f96bbf7443d6aab90eece8289ac4a324a7e7a498932ff6d334b65d61974f870220648b2720c1259f4fb9f6d8035687aa29d5d3c67515663ccafee5da6217241d47012103312a59d336b4d8b142c3ea2246ddc92eaf0554533d59c01bba450060a22ef9df0e8bf7160000000071260000030000006b483045022100f3485f91b98e0fbaaaf80b7395547f64b3713d14470983c8fa70bbdd749b24c202206e43fb5644eb5351ac91dccf117085743a60007a9561b022432360969af8b2620121020d35f09c17782e3d76ad3d60f082e52540dc59997b6e666df313defac496696b0e8bf71600000000ab260000030000006a47304402201d601455939a1c078852acc48df171c6329cebd7244ae153b27693320d6e283b022045904d4a0338e4d99b3720ba440fd19506cb28b6acad23d18942afe733831266012103b783c79189b36fe53d1f2f6a72a2b49e30669c1efc4a4a36949edfbef7f1b38afe6a730300000000f8260000010000006b483045022100bd91afc572b76c743a0c2f2345c99df42483d988b7a4ac80a62e9c7e36b1283102200be98082663e5b92e271823b891776c4c4da97fd5a736aa7b038b3e623dde57f0121031a2365956bd1ee32b4dd7b5f48e676ccffdba81518e7e3ad6bcb3a9bf494b324","bb76a91498090186c13bf2737bec0baa8a71d628e5ac51a388ac",2,40,"e48096ba550d76c58db6b8e342d6cb18e3ce4e6fa3c1d462b62cedb0497bc54c","OK", "4 inputs, 2 outputs, sign input 2"], +["0100000003b8e7516ad17bb1c3d5c8733fc20b974c65f282ba3ec2c17503dab565f551252e0000000000ffffffff0945293bb4e84ad9bc2c28125f53016d86df4665c54894d59eaa690f5bbb7aef0100000000ffffffff8d5b04d0e70a0f7e1f7f1f76f765d9bba873de3f02d88ede285f7105b34ddd4b0100000000ffffffff024b48b0020000000000001976a914fe28da69801b8711ff9e02879628b5508e12a73988ac359902000000000000001976a9140b12e5b5f0f384246efc897e7947ebc0a66e45f288ac0000000000000000033ea07d020000000071270000020000006b483045022100a53b026c928e09df1436c10c5aec7edba3f71c4d6ea29358bbdbb959f60acc91022072f5c5076e86d29232b0a6f24f40df845a0f99813ce2ad6ed0dfb75366122fd0012102c5b6a7418dd82524a6743bf7953a2a752d68d67c199fa4069dec5d7441b72942103f26000000000064270000040000006b4830450221009fc512ba8542d77c05867905fac662677de7de5ff32decda287b0c74fa82faab02204075b04c7f3263c8f8cb5403e046bb8e3545a28b26703da56abf95bb40f8d2840121037395ccbbce05ade5b96c26294556805a56b6b73050e48a96f4db248c235f914e92e525000000000058260000080000006a473044022018bf7a6fc1ed165d31e72893d46ecef5bccef29fc273b49161ce36f3a32b53890220342bb9a977f290c6ac04335fbe3735c0f1eb706d4b5d0e3de62a969e3652bf9a01210271ee4bced0dbe8ccfd1a7cc5cbf0421ff2553b84b00c267ce60afabf5f994213","76a9144d1ab5cbd8cbd0432021a4d3e1b3d27bc991366688ac",1,55,"33d287d7a7a08825f96ae92f6ad3096187aba33927583a0aa6278fc609b169a2","OK", "3 inputs, 2 outputs, sign input 1"], +["0100000002673cf13f8df078588f772e189b5207b5bdebd9d7dd509ac49e9ae604787c5fe20000000000ffffffff5bfabaa575092d955145470fa430a04ae93608707b32538f6c9cbea20989b1e10100000000ffffffff0218ad92350000000000001976a91492fddd4a10a196d761ba06150a898bc8f4638b5788ac96968a0d0000000000001976a914634f264a2cea5ab312aad28b738297240d9f523088ac000000000000000002b92df12f00000000b9270000010000006b483045022100ad0b96f54760470bc7553fa209bd815739a24b9fa7df99ba0f2cea3cd2a5f0df02203932b70e047305c05431289810026e6a35924d80943900cca9bf056b8d1ea2a6012103838f5a8643d86c18b0b7e8ce4348a995ccb164cdbc9ff836c1ec86847061d15955f9421300000000b5270000060000006a473044022075de97ded7964dbf5a9c69d61a88d52d386a25cdd6e1e32d6abda47e9df2d52302207bdf6a7d35430a2e65401e03c10ff1db3f03bdb96a3dd357737cc2d123aebccf012102e68985a34ce39f66fc1d401e34e5f0e79ac113ea9c89373a4be44f10b1fecbbe","76a914934c1895df32cc72eae8b9e9c28541fd9ee0b3c988ac",1,136,"7b0537b123e430294b6cf641a3865b994066eca4a3327695691ddfde8cb592cb","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000026c7b0d0b41b1a24f254942f39a1bcd6589abf1e65a7f454edf0af41fe63ff1910100000000ffffffff5c042cb9dceafb683c1b17e2c7c9afad7d693ef85bea30f53924b7a8644f19da0000000000ffffffff02eb71d4050000000000001976a9142a724793b137b3a5d2d0474195804cd005f4d26c88ac7edf0b960000000000001976a9145b651cc2aba90671597d3225360b66389f1612d488ac000000000000000002c748c35200000000bc270000010000006a47304402201647514dccc4a48e4693b4b0e80e8d08b8830b8398802dc5d1eed4dae8ce8b3402206bff15cc894a5cd00994e02485ce01dd36ceaa5513e86d451c98a037c8f8e01a0121038497facb91778bd4a5677b4d981d73d27d6add820cf20a3abc0c12dc2cfdc7a802ec334900000000b4270000060000006b4830450221008a2662278792c8ec48b6c3e6dfc4c30d74cf90e1e3afb1913b7d4a548723e5890220715b311fada2ef16be81de341a313039aeb5e9dfe17ed7fbbe4ad192818f0e69012103989da475feb2330d748123e2d22ff5225e07c9406dd46488f99ef5ef5e689e79","76a91479c6f70939099ff299f8ba7f179a45ec21599d4d88ac",1,99,"d636b2e4e22121953f367120ff3451638498bb1eb4753eff9fe4a9d8edc822ab","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000003803cbeef4a4b5792c52a7c243458483a4f6aacc6ff214a23e74a05daeace8b550000000000ffffffff490db443b75a55183e9bd3abad347ef185fdf0c6b83ffd9a1cbfb801c6ab27f60100000000ffffffff70bbf69236da3c8e579819900ca371ecdc9d376728eae5cd4005cc9c4c6c87490000000000ffffffff0240131d500000000000001976a91424a6c89f041168dda2f2821ef2296c79b0ea728888acb9f3a0010000000000001976a91481b575e02d75773b9efe4e74815d43f7a0153ae588ac0000000000000000031e739d4000000000b9270000030000006b4830450221008dc519e928243d1c337ef1fc6dc793238fd0dc6d2cca28bac9409f05176723fe022016fbdf2f674a362e37d3c45dcb80a3a6f150fffac15bf4777eca2edf1761ea28012102d73f6d8e35be886a458f09d0c6ca1a3c6dfd8dc527467a6d1685ef46d508ed0496968a0d00000000bb270000010000006a47304402205ebd13c1ac8cab525c24bfa3260cde02c672fad1cd90ae56a0283dee17a2f988022034b4dd67323172af5d6d9dfca9e4d3e8e4cf06868d474e5eb2c8d51d26d8ad15012103763c6e23054a11f2aac1fefbe5b31b981b8ff285d7f22d82bc5f3e1bf98ffe3ea5e0ac0300000000b5270000030000006a47304402204cc4bacfcc092f4ddf2ececc791fd8335a80feccc14aab51d5a61e5b7573f135022028471699fcaf3f0a4cd57883967e8f959318ae739fd162f230fb6bdba4bd102601210310cd3af1c1f03c1618838471ac047a3dc5750564722503fe8926e207d5952dc3","76a9149326f8f90f6ba83464cd6ebec0dbfbd1504cdc5188ac",2,75,"bca94b744e9291a1d0692c2f34bd931d0a140ad038ccd4da68c4d939c53f4834","OK", "3 inputs, 2 outputs, sign input 2"], +["0100000002abe9a624df33eabf5ede7b6b05cdfd7af192f7b455d0cc09341a3adf2ed088b60200000000ffffffffcc27a0bbd069f1e526c644e5d2066f7f413f6e0766c3892eff6bf120a5b9f7510000000000ffffffff029ea31c770000000000001976a91402dd4bfed233b365f0788d8749f305281c89d6f588ac618ff05b0000000000001976a9141882eeaf6ee632607f562fb1872919348dc7e96488ac00000000000000000250a1a36e00000000be260000000000006a47304402201d40e1aa1b3a69315552f53bffe3c19305324f01944fbe9c1104a7cf8f5de6bd02200389445a8bf8892dfa1ec6c4a7f98d11d676f08db41f8374e27eafeade9487ef012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8260f75806400000000a0270000020000006b483045022100d28bbcedb324cca75a1a97506b5b089b42a51b808e3a62e39642e55a72f5174d0220666f4b3e5a92f9da6a944ebada0e32bfad7ebba10d948c23797cf6d95d7a88e3012103747e37e8dcadf3019277eb7a7ae76564a3d9059487833cb0cfdea98952aeb6c4","76a91492c2b5235036cbe0d818bf793b9b4cb590c562ef88ac",1,152,"07aa3c9fdb9d358756cd6da12351f74c023dab50ed5f88cdd5d615f72acd9922","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000372c44583514f8efd1bb33f9898896677410fe9c54e72546cc2b4f7d126bccf040000000000ffffffff4167d60e471c1f2b3854e6fec00eca4746f9e3039330a91c98b022b0a7383a690000000000ffffffff3a71ba0f3f5f4916fbfd86b661577ecffa500d9d370e0df088c4c76e3baba0390000000000ffffffff025141b8080000000000001976a91451ec5558f4b3cbf8c9917d038b06707b5618e29088acb35f7a000000000000001976a9143efb80acf5eca76cdf0e09f0ce4132d5a5c5073a88ac000000000000000003eb71d40500000000be270000030000006b4830450221009164c5de147bd8db20f4cd30262ef76f9c328734762eb358e867e53bd1ebd8d60220112b80cc7e95a4cad447de5633ad8c71f201650d005a310d518aa4ba8f4fac74012102efa52b5639266e93c2dcdd8ff29b98f4f9ab6e1384d724e76ffbcbae0bbc5bb0719df40200000000b4270000030000006b483045022100f88e77c6edab7102985a42f573e90717773153315aae4796142921b246bb3c74022000e615c5cadb6329c0078e63207547f799330553369133a249bb68b25b446bb90121031333711ce9c0305fb29b719897eb729c9ccd0c7fe1b279689821dd166c2ced250875800000000000a7270000030000006a4730440220210f87d4e67a617631ab85319d01b191c7387792f8e79fbf6eb79628b04ba19102204661004b6fa2f4a8738f7778023d6edef124511c35f701cfc63e76782429073d012103a1db3db8d3b13df05af84c0eaedc7d5f9cce394a8d236de20edeec752f93e63c","76a9147ca0f86b3b0590ef4db9b7a476a41e461e18941488ac",1,229,"32786c21d64604b893b282411ca4310c1c814bcb930b926de007f2515354690a","OK", "3 inputs, 2 outputs, sign input 1"], +["0100000002e001ef3b2c7e81a0793838b18007c14026e82ffa24297851c50bb1e55fce8b330200000000ffffffff92e230b0482589998ae9c6fae4f3ae51c004515a8a525b4507eeb9080cdaccb90100000000ffffffff02f86e3a840000000000001976a914bcb8d2ae1d97c70a4093d00333e16c82259a123d88ac110a07070000000000001976a9145d57d29126ea1f6c2cd7a27d1f0334651425f82f88ac0000000000000000024fa1a36e00000000b7260000000000006a47304402200d5266cee152a23d96e9967f54bff368eefa45169be15d0c9c6ac8be259b34ad02207178e2569976c371cc460a9ebec3e438fd8bec144a1c3be2349bcbfef5cacb15012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8261abbb41c00000000ca270000010000006a47304402202cb9ae3a49ad4d93ddf4750f91718b533a644d8242ae0e2bbb170ced8d1bebeb0220136cf6c346d1c1bb194b4f0496405005f5aa9d02853fd2a032fc828532cab62201210387075d406b2932bbf0aa626f905ca35d19a281eeade136c6ecec8e745d8ec92a","76a91409c5b6719f9628a2ff5e8428ff382a180e2bda0e88ac",1,129,"43b5e09e2bee800924091412dc57df45068ae647dd546b6e50f70a1fd3cdbefb","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000457a3c75ae123b3d7398e1bb9c9c2683130d4b3f0c77101793c324a8c444756d90100000000ffffffff7875ce1b2a73efce3c2eed2a08ea9b3eab7af422278a751a529fb2c61e69f1190000000000ffffffff09daa4abc74e7c96f7b781e9c5e6384a01be76dfc42fd574b1f61f3d0d3ba8720200000000ffffffff4c90da570a781292dcb532273425b0f729aa741ff1cf332dcbf5bc3924c133830100000000ffffffff02434e550f0000000000001976a9141e61d4917f0455b88fbfccf4b3e4cc225f53fa3788ac1acc692b0100000000001976a9147abc09206671ca706340785730c77e54de76cac788ac0000000000000000045dfdce6700000000a1270000010000006b483045022100c8c4a7cbe628d558f8bb19aed41271eb749116d7d0d85702e07c2d912bc2243502204cf99332b086792ab6b863a733a64b3de8311120139caa2b5d815ecacf3c558d01210355d19712c12d9837eb35c53362da474d309c43ea4e26dc4fe45db70b02f59d5ca8e818660000000075260000020000006a4730440220434f17d43fd3df3850ee33dd2013100e4babcccc19e561a8e58725dfc37e3c3a0220769ba11c24ffea2b4639664b83d44b3c9d121fb49c5f671f62261b6da9927211012102784fcb2ee01ae3eee931039b92df2ac884bb8f06f9fa245122f6698f9e8218ee72f0bf5800000000c5260000000000006a47304402201574ff3ec16ce0b1fabb499d74f95e2c94ca058470dcbb9948c1f31d2dd56eef02201f9e6ecac6a8e60761f1d9daa0be467711e8429c19edd5913015f9cb18292f720121038a8fc141ccdbf8364bdcd0b2ab035dd3f8293e2f60462bbbecb025daf5caed13268f631400000000a0270000030000006a473044022075dc8880376a743cfb8c56435129784bcc9d5b882a53e7358c9e590d807da2e80220185bf697b31eb9b6162629e3351b4d2c7badf2bc0f3b57ca443b146e0d84b520012103e756c30a3388808acd1a55bc6f35676373e035aeaf4df7c923f6eb9c7eea3b77","76a9145f0a6582796c4ebd5049a8eb2e1f22af82eab9b388ac",0,239,"b42ea2a9b596087fc8c82ab6d3bf183cfb287e33453ebe2a71a2463b26d7afbd","OK", "4 inputs, 2 outputs, sign input 0"], +["010000000427ac03c82b435093626072e24eec7e999c5fc8f95312afae5d59fc9bc61ecfbd0200000000ffffffffc11792fc959a920332c19658625a677017a4e9047c08a3f93a55e30126c8a4c40200000000ffffffffa5d07ea117aaaa2b734bacd451e4bbc6104f138371bc9a9edcdd6fa0998799de0000000000ffffffffb9ea0d27c8307b2fe7b9d881427ec933795b308c8cad53d443af302c4605bce30000000000ffffffff02fe8329330100000000001976a9145aee5effd64a5ad0fd661c641982f13d312b286688acc1ed27160000000000001976a9146c88521352121f5f8fcad0ffb77cc94b75ce13b088ac0000000000000000043009d96e00000000cd260000000000006b483045022100e579b6d5399aa4a835daab2ba276cab4dcb3b5bb36e09866b9909345ba3a5fa90220559184d2d58a21ca063595377b6ec51745f1ce060a2b0b25b6facd129ff9adfc0121024a2546600c5cbcb53971a19e2d481e69b6f39f8f8df55ebec340eaf238f5a4dcff6ca06e00000000cb260000000000006a473044022070f3ce9a22a05b9796bce8ced8a463e961e9188ff56dbe2a24ea038be0f6a7e90220208bda4ac2ff10192e8df180253f434c3e23ffc82a2c03f1a62818b43b475cd50121024a2546600c5cbcb53971a19e2d481e69b6f39f8f8df55ebec340eaf238f5a4dc4fa6333b00000000cb270000010000006a47304402207fd36b8ff437d75bb957a571ed79215933fce32c66ac9c5e0289e77fb45b071c02203bca6842d4a501027362389a09e42e19ccb3c873f4f5a99b392561130cab01fa0121033ef4d7a511506c2855508d06e2d45e638bbcf38f7181352a7275042c17783e58a138bb3000000000c8270000010000006a47304402205b1872be315c3d00c18d3b99c3ec6933af5af841a5e5465c427dd8dc047c4a2e0220244956b09be6b4710174ea19179e1f5f26870188e192ed5bdcb4a2f23ec80b1e012102e93f8951bb9e353f3b9f9c1743d4f177e50fe08673e0384aec5ce63970d13d09","76a9147734bd44ab694c95863cd31a5831edef441b9e3688ac",3,34,"e6d47dd039c3ecdececd08c1bde567a6acdcb67bc4da341c48ddbdc19c1e30ef","OK", "4 inputs, 2 outputs, sign input 3"], +["010000000352f42d78a115115f54dfde0abd9c68022e9226585182a0dac97986c0824b843f0200000001ffffffff63ee4b12e6a2111de98f41195f2758499f91594efb6954218f564eb5184bda240200000001ffffffffd2d7cb5bc0d33a0f891424b9db493596fbc41278ca6d620bf40a768e7ca724970200000001ffffffff01ea5ed7440000000000001976a914180d4bc3507a96188d61ba75325312e38c5a381288ac0000000000000000030e8bf7160000000031260000010000006b483045022100bb8c9a8c75e4ee7e1ea093ebf70d8b4246fdcab82d76ed0b5d10b5dd467f6b25022001a570cdd52ff98af4b2b5e355428fa05666c2ce619fc42f27975e6fddb278e1012102cb9de3281c68dbc3b7381795e49704c0d0e745addc28f84b46ce2cffa97308a90e8bf7160000000074260000040000006b483045022100ee8acf131499fc5186c377706798aa0a628041fea8e26995233cbecbd2004ebc02206b3d441e43aca21fbf3c989dc5c1ca7a072170ad265a10379c38dba6c4efb090012102cb9de3281c68dbc3b7381795e49704c0d0e745addc28f84b46ce2cffa97308a90e8bf716000000002e260000000000006a47304402206bc122f8330c016a5c4de220f4cdf19d1d230e2d73b09fc58d37bf4beef2d31502206ba8dbd8ae9c41c24d36cbe937391703a631bc89aedbe2542f195bf36f185ce7012102cb9de3281c68dbc3b7381795e49704c0d0e745addc28f84b46ce2cffa97308a9","bb76a914fa7deda702d923f5d48d46ef743e03813848925488ac",2,15,"793ae120f8c1b8182fbc89448f28bfec415c2ca4e9a76bebf2013c6a5d907060","OK", "3 inputs, 1 outputs, sign input 2"], +["0100000005d75d3cd0cc15a0166641b7e9dbf4f5ff9e045524370476ff0654c39125ec10b90100000000ffffffff00554d4dad7df6922c422d7bf163d5020f540424ba145c0840e36703994288a80100000000ffffffffd7136afc96a6e583dabd79de7a396c031950d42422f57624dbade2f39608ae3e0000000000ffffffff6254500fcf354d4059cafa74f8bf7a278a6c2ad510ce144daba26ae412f57c4b0000000000ffffffff6e4505475adee150d654c164d2506d58fb13214acba8055a8aadc7437b68ecc60100000000ffffffff028af750010000000000001976a9142c62951c97ed2f6522b79cc1fc777659140a312c88ac639696340000000000001976a914c727ca5d48b0569f67a452ab2046b2ed2198e1dd88ac0000000000000000055946f2160000000063270000030000006b483045022100eec455fc99961804bfb8e7e7a38c2c7e8b249a4d03b7d80509d78023fa50da6402203b017a9df983d1b07fd32b726bc80094bf74feb75aa78d560e34587c025b9b0a012102a4325af8f1a6d8c21cbc73b3fe3138dd5eff8e300c0b2234076c547928169b534f82b91200000000ce270000040000006b483045022100b6525862837ed74e1d684fb4aafe340fe01062c5fb1f9607f58954902cf4f35a0220371ea4d4b0c994ef2697f578b971e79b029eb40107b6718c9f9ceb029a6c949901210259d34f52518f23de0d529d3d91023f99f79fbbb88dee9e92a80b2660e49a10360d0ca4070000000084270000030000006a47304402207774f3d0d8d09c15a9dae2416fb2c552e1c4cfd63ca4a3aac0537ba0a85c4ad3022045082b0b9af01ca65a6fb3bd2918d00ee2884b30909ceadcfdf5fea010504548012102d733dee37f85850f46ad912a154c01996a0a40f46404eedade4d32b6d596cc463f8c240300000000ce270000050000006b483045022100fe06babe652441a453763ab4635eb98ce2369f78cd2b4227954b100470b50e4d022033d4fd8bde39c1c08e40d628d2af2314119816509b51e1e5b86a7ce27e4b0ce40121038a23d88dff617239b6c2a99f08ba677fbfffe8779a8dfffabd7a7421e23166c7b9f3a00100000000be270000040000006b483045022100a0b3596a745a0b01c3989a5651341df5a5fe27b48c6713ddcde5638cc90f845602204e61992369b79a8a4c89b63ad3cde81ba89ff79b1fc9ca3dbbba481c77b596e001210358d261560031c6859b2ac4ff411fee24e59ac3738353fc1ef37e3a7a45e99e40","76a91433c7630e6bcdd67d72b350df2da2eeadec74ef2088ac",2,26,"61cad3a81d0102d9c3422300530b4a6ccbae818932dec698906959dc174947df","OK", "5 inputs, 2 outputs, sign input 2"], +["0100000002b8ab571ac91d6ac4cace6ef1175c158746e360a7f5b5207327447ab8d9dc515a0200000000ffffffff9c5b464b1b532007b80145aef8b05b2e6acee146d393cad601e421021d549b2b0000000000ffffffff02ff4970000000000000001976a9144f0a9ef3ef29042f81900fc1d13468fe036f82c588ac9d1228590000000000001976a914796cbeb2779b070b9be7dfa00d8a85c6463609d988ac00000000000000000272485e5800000000d7260000000000006b483045022100fc717a0d85380656f61e16eaf06a13d7a40cca2e70a0e18b2c582ec091992c0802205c64113eac874358c923eed3981cc181a8cfcf7b8b6f36a2c29c5c44663cce9f012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8268af7500100000000d3270000010000006a47304402205bb5ffbaf43a27544ff9583ac360e86cf765a80e9df2341bd2dab1c9a9d1daad02200b76df5079b9b68bf8edac79b84a5e17bc2b596823a6b9fc0517b539c53ae7300121029e09e91f51bec66b0dcf837d90f8869940a818382e441e34f98ced2cc09bc854","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,135,"c7e5f59f1da060dcc5514076ed49bd7b911d793f46e71ec5ef3898921fc4bdb8","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002c5540963f7afceefe03d29ec5440a211eddb9fec9fbddeb0d8f6e61cc98bd1b60100000000ffffffffd92a52945056c577ac672df81a38f2d1bb601ace017b0c8bc83776168e9b7c880000000000ffffffff02f50264020000000000001976a914bcbb8035e112a22aa0a595430ed5db06979a952588ac9316e2b00000000000001976a91485bd9f52e9055e3fc90ab98f59e53212bc9a0d8488ac0000000000000000029a07656200000000d8270000020000006b483045022100d9b295ad4af1fa5f452d6c0591c0db1ef3a5e46e2a3f89b0d6937585ead86f9902206c745a5e02f53ee7e759ad07733c74e40fa5e4bd56dd2d32793f2d9b4e6ec9e40121034b6d916eae9f6fe1fe1a97f00c32e35e50f1ecb24a65de828b33237d1e5c2ac24ef5f75000000000d2270000010000006b483045022100fe76fd56d3ab99d399e297c28e4d10360b923538f57eaa89960f3a44406c2f090220559e4bc26fc1a7ca720651ad57440cc4d3db34c718a4236b022733240904d044012102d0fc49a4a3cb624418f6a40c3de4d6180ea3257062c43c0402e66e551799702b","76a914824ee2f3896eb959d7823894bd4fb7cf42437aec88ac",0,77,"731a5a626d38fd6f3468f3bcbcf39cbe60b1aa07b10eabcd37346a7ec9057a20","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002851b43b10b5105ff862248bd48e9b2671380424033a7cb881ca611ff9e1082a00100000000ffffffff6d7f131c8f8763fc09dc39c783a599808711fe847d269ff3c2c2c8368bafec5c0100000000ffffffff0240d0cf040000000000001976a914e6fd2c5e9f71a1282c826281f99bb03f0b9c2f3188ac17169e1c0000000000001976a914ea53a074f657b670eeeaf726704fbf63a61bccf888ac000000000000000002c1ed271600000000ce270000020000006a47304402202c1c8dcabafb1155c09f4f2d007f08df4877efca573d61f62d7f1c0fdf21960a02202039593c33701403fe6343fbbb25a1aed3500547bbe189a8968b8143235ffe260121035bc01a13d3bc4bd36b467a13451b037254bdce892548809939b6210d78f6d0e1f6db5c0b00000000cb270000030000006b483045022100a87447d2d9f75b66780e6429c09f31d3cc1d2a63a91b50a31bf5334fc2d58b0d02205cf506dcd0e153db3861395cde76e3e71ae50998ad0f01cde1e661c50393d2bd012102906f27256af6d9e175f0fb479c0e211386addcd86bcf3de9a06008d4a5c96aaa","76a9146c88521352121f5f8fcad0ffb77cc94b75ce13b088ac",0,87,"7252237cb25777f4ae3f6a9b17c733a7f0c2beaee2a2a727749cba2c85e13a6d","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002c7a0b3d351b5a2969cf4cf999c3edfcab45eda6111878b19f1c5d65b72e3d4e20200000000ffffffff72aee7b090cac7773d1004c61faa2c64e132cf61c4d174807b3ab188ffbe1ca90200000000ffffffff021638f8650000000000001976a914dc3406b844b4800c34bdb2dcff72526e6009c20088acfba5b6770000000000001976a91481b621fdd473e2e660f6a322c436c9510d05d62088ac00000000000000000290ecef6e00000000dc260000000000006a4730440220085420b6fdc6ad2f87f5733569c854e7ae74fd0c037ee44f92cc09ef6201370a02201865ceefe511b1bf985efd6f91bc87ea73a04f1060929794a77c210324d47372012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826e1d4d56e00000000dd260000000000006a473044022033181b7d1dff043a3f5a103930141966247c318e5efdc07ac72d68a8764322d702201cf0e2abe7c6200009cec8e53140a9b2229b0aaf82ef1a0a038f5e463fab9986012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",1,108,"469ac88585efc75648affa8d2871cd16c496487e1b4e93018068931216ca9b05","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000368fccf63d8b5cef5b47c96ac67fc8a76799bc304f95053ae7d28f9248b0765780000000000ffffffff095e07eccea18989fbc716829f738ef77fcb9fa3db7e5cb084666562dc64bdcd0000000000ffffffff5b7e138a026749420767b4f877b14aadbd721924959761e59b22c47e207908f60100000000ffffffff02f385bd020000000000001976a914ded38f638c22356f7e072aec416623347acf40a288acb2990d000000000000001976a91482f04a3addcf5e72296285deb97f745e9e1b7a9388ac000000000000000003f502640200000000db270000010000006b4830450221009906d6ef7b048eb8d47462e34345c946a8e6f0e62f96d2481ec161bafb217f8e02207c48587357c1b5ecde98a8d5fe15c1eac8758064a62712985d82b9fc16f4d65201210343352d93deda5f147fc34cae868cadefbf3cbe3bbaca29a8aceea78949836bd6ff49700000000000d9270000010000006a473044022038a68f9af8dbbe875260cd13a85df07e1eeb9b6442fe5fc08d56b5f7fe8d016402204ca58332f7aff6357193d1b744b3e7f290740ada1d7d9f7069623c5545ca954201210213e03c750ae45ce88676c218ccb8d22d211df2ac158901b0aebbcf22bc3ee58c11b60d000000000053270000050000006a47304402202b7394994484dcbfd10e4bea9b4ecb6b2951b4cf56df1badbe308f3dc7d4e08f0220690a91085f74ec600e8d017979e3bc4953042e57d3a8814350fe098d7aebdb97012102728face1304cefbef0f014b0f6a9351538bebccd8106ef557106a307ee47047c","76a914bcbb8035e112a22aa0a595430ed5db06979a952588ac",0,180,"4883ee9a3753685c707c8cbf356852a7b75cb7036ef5ff28f8cf0d215f158d89","OK", "3 inputs, 2 outputs, sign input 0"], +["0100000004b9e7e01836cfa52cedc10db56669fe16c5d6a51611897edf2bf97bd73dfd7f9a0100000000ffffffffd3dc6d9c539329bcf832f097acc79b182999c07ee754c132cc3ac4b8cb6742a20100000000ffffffffb218319e796e2fac1daade09cb16265a2fac121c828bbd93beb1fb241503bb120100000000ffffffffdbde18588f97b11e3d0c9c3c068dcc3ec473bad4139cbe6b11688ed8b650e25a0000000000ffffffff029d5e31030000000000001976a91464148b61ebda7d13bd75f04f1d877a0e598867c888acc0d128000000000000001976a914010cf657d1d3cc587591c7c492674ee9523b1a7088ac0000000000000000044a7e43010000000076270000040000006a47304402201fb3119e44c4e95c718d88636030280345a80b22c8af07f2bff4a1f6fe8f5d1302202c0af955a316a82bcc570e2fd5a420ce0e607bea3c34da8de1fb11b6b28ddd3b01210300a9a874279c5b69d38ffc7e9f85bff41ba83809d5103bcd313c012f13ed2b368316e20000000000b1270000010000006b483045022100a1d57c3dd16b5ad8dd9f0fef29e13389a2b4f823fda282c76e79c2c7cf0c00ad0220119bbe63640b7ca15886689092a3179748d2207530b51436b4f5a90ff528b7db0121034ac23f2a1724484f8b79234bbc6d073c5630901cdb52195d8ef8455c0673426be922aa0000000000c6270000040000006b4830450221008532fc9cf1771de7c083afe681fa6d93c6c8cd62208dab60e94f3d1875fc24ed022052b101a74375fae6d0c926c941d0df28a9f3f2ef117ce1e32d599faa5b77a388012102fe4925fec117d6764f868fc4d6d4ab32029ec9fbe935830772db2f9f1ccd21d4075ca1000000000068270000030000006a4730440220045c7db5ecfe7f040261f7790311c635db4d32bd9282c901bf1ce1878a8b8c66022076da0621e22b38b45b0769533bcacb28f8d5dd228bb9f17983afb7dd2ad825c3012103ed1a2832e609b7099c451f697e100e9676cc6305a6856d754bd76656722f8b24","76a914190ffe6378024221407e777f1fd025a3a1dc0e2d88ac",1,96,"bf61eb6e055d798ef1a37c572b94ece709723d74944372d3066bda7111daa8ae","OK", "4 inputs, 2 outputs, sign input 1"], +["0100000002cf0cf94956f44d73a8c002cb4b16304c13081067507052288abf87bc9e7f8e0b0000000000ffffffff496d0bd04cf0bd40cea36167383692f8f7bcc3e55f5a792fe26c5040a3b0dc5d0100000000ffffffff02900aa1240000000000001976a914f65aef9352c101a201c0ae617a8fcb8bbe9ef26388ac978495550000000000001976a91433844947e2545a84201b9c41f62ee199891b47fd88ac00000000000000000210b77e4700000000e1270000030000006a47304402201762dffa24faa76b81f023f105112e620d1a6e71cea4b161941162b36521c6770220028778a0442190b73586bd450a8583c8bc7b9a01e433b08f65e904d25696efef01210374af9d797ac4cfb080eebef1472dd41179bc01646736062e3cb9867482c1b8fb77bbce3200000000e1270000040000006b48304502210082d999d9791c8e97869d29fe0708415bb260ea9cee86b364af65e07fc74e41ef02206730faeb3d53504993bfbb12b6922bb8b0b9f266e4a720d58879bf907046f63e0121034d0bf9bfa16d91a24c990cb2b5d91283b2cfc206358b72304350864607a4b697","76a91498a5c0c0369002ae34fd95a2a40a5589da27d83488ac",0,172,"fbb21bb63d89fc81867471ca260a81a8d3a6523ea77736b386988082d5355973","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002beb18df675b44203878a50ef2773ae47601d63e6412e033e1650a15926030ab60100000000ffffffff58ac27f3cf3d7b320d510b78d00597bece245a46e3528715ab034b9cf5e13c450000000000ffffffff0288be27120000000000001976a914fb06758658d30d4afee1b1c0b1c2886056c54fd888ac41d0a1020000000000001976a9148a1a3f826a4bf7b9a64a49071ca1c18a4d9bf42e88ac000000000000000002b777140f00000000e1270000050000006b483045022100b527beaf9d7441a2ad76e88715d19833fcc6ecc387ae892bc0dfea619f6d613b02203a54cc462aeedebec91f6fdab66c21b6770f78e27515bd127c8339505b976366012103e6ea30fe592f0d1ba1d7cb2b987a264ba30b08932c1a49da90043be0c9ffba3d72facb0500000000e1270000060000006a473044022050b3490cfb4c3bf9d6ff3de4da51f86cdfb636320dac16bc169ea2b3897d728d022017c89c452b5947dc9f2d208a5dac6a20c4c701161b405c139e10dff4d8658d11012103512488045b09fb2aef89a1aa90773f1b24ca228f86af30f7721f9ae020d86409","76a9147e53e9533961bae02d7e37ddb1a8d5829e2b6ccd88ac",0,213,"b6bcf77d1ff079b6b749b8c851e8fd72deff7a0f7703d260fdc992792ee100e3","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000005095e07eccea18989fbc716829f738ef77fcb9fa3db7e5cb084666562dc64bdcd0100000000ffffffff112d4c2ef49a411029b74875c46c3d76b1fdc096a2a4a032d204ceeb777d18240000000000ffffffff03cf80f833cc1a8d0799f09c3a764d5dc89efee66378e901a97bc49175637f140000000000ffffffff0787e8acde164c649f5cb531412b6489914e5f7dee4d3f552dc424931fe1e5cd0100000000ffffffff0fdffc07c454cb921e25530af17efffc6e1608eb589178657f6a45b3c24dc8240100000000ffffffff02c4ff44030000000000001976a9149c1094fe5b344a0eb49968e347f2b4a4c640ff9d88ac40613dee0000000000001976a91407fb5e3a85cdd14465621aa416541f706f8682c588ac0000000000000000059d12285900000000d9270000010000006a473044022057f6aa307607303711be0482cf5c5552031c8c7f4d5ce2f362a6682c8573c4b0022038ed00255a597347d84a688183fdc83b72b77a87e6c27af6c40c10e542e5884f0121020e4301e7843c228ad121070a02bfa6bc8fb7c9a869b0003b77e44801da746473210c4a3e00000000e4270000020000006a473044022051618699830b873d9e3a2d5060ba435ad1f3fa8f5ef628ce63fe168abca8f83f0220481c4865329521f9397025b2092f31bae3b87ca89780b587b5f0d2062a55224201210350f61dc99fa8a7746ffef23ef3578e3b824246398f0f8bbf57ebde1a3285e9d6eb467f2200000000df270000080000006b483045022100c77488c843a2fed485f06f0316ace94e1276e9cf9f19a5b068100237b9532d6302206daf555d4b33e45f3b101f210590941ec1bdcc5542ae1f9c1f5dc68301e7ff900121038bed519e937f1b4ee9bdd3afd00c21bcbaedff98ea16de4dfec806c2540d14f5bcc0ad1d00000000df270000060000006a473044022047c4ba542493215b8b34dcaebb871d328f65f8db82db60dcb3e285a85ff4d9710220044edd687b869caec7cbce042fa42750fd70fb5ece8384a32e3aed47346d26a50121033ef3cd830451ba8eb929010da7f3cd7ce04bd3a721b4bdb07e4760e60b03b8965f01111a00000000de270000010000006a47304402207adbd93d09d01430f8a90339b0d650ce82af9b24d3748e0cf12b4f28a706390a022065f98a2ab448d5493f28bb0973e30df74b83fbb987322f4a2782ce44e8f23f10012103b50cb22a4ee30d39337257d1b04c43f70929b5527bf06d5f6512015c190eaa0e","76a914796cbeb2779b070b9be7dfa00d8a85c6463609d988ac",0,22,"fb705164ed3bd6279b86014646e23b14e585a9d65f8fa7af096db57e2324db37","OK", "5 inputs, 2 outputs, sign input 0"], +["0100000003a385a5c7680fc2bb4db45b604c038f977dd2f240bc30234a172d6cf53606164e0200000000ffffffff7890116cabc171a8d8b2b8e4e97cb19e6cb766b5e0338fadfeb30da1b950c3940200000000ffffffff818aaea2655198976187d4903e7747570d5f1667e530bd1d06cb93714b05afca0200000000ffffffff02818cb52a0100000000001976a914afc4386d70b6c1bbc12b374a48a52a5a40e48b7188acd203180c0000000000001976a914001e7d95f50b98c8c2e5337a315daaa53d473fdd88ac0000000000000000039107a76f00000000e5260000000000006a473044022056e99b06b04e4f8b80702b5311d25a16ed9b243be92b763ff5a176172c97acbc02204fce7b6f1575587d020af0b2a385279b371124967666749036b71e98e7ebbd98012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826b084ba6e00000000ea260000000000006b483045022100bef5f5c33a39f6d65f4f4144824c87afcf6f262375de789f869f999e4444c37f02204dc1384148e0937c23a95f75615c6fcee2e957a46f9bef3aba4b55ae6c4cb512012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e82672e7825800000000ec260000000000006a473044022008737a695b9fec67ebb95bf83274f8ae15d63cb5abe7c6b97811cd42949d4cd902206923fa5ba52bf0dc9456ba5d939f7c3b921b7dccc0a8c2d1a97e5f68b1440f9a012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",1,111,"d635758334c46b4f1e401ff3d24cfbc7666457e478cd3d03c7658a7a91520f36","OK", "3 inputs, 2 outputs, sign input 1"], +["0100000002836017c77937e04986f80317f6567a45d44657ad90b9d4ed9061f54403ad04070000000000ffffffffcfbc1254d43f9064e61df72c285ca26a936a1de1661208b314a9b3fd0195b2590000000000ffffffff027b8b803b0000000000001976a9148b269cf2013a255dc1ba23f69fcb22e70010b78b88ac05042b0a0000000000001976a9145e547bfd5e49ba66764f644145b9ee224bb9a7e388ac000000000000000002a192212d00000000e4270000040000006b483045022100a45314aea85e52a74af2e8c3ff0dc914271892bbe6f5602f4d71ac820e1fbbcb02207743b936492911c1ba4eb1eed779fdd6ea8254b2bdd11fda152722aa022397ec012103117d191d3beb4de25dd1503cc32f65479c1a33eb058ea3df3cc05ddf766b317b3fe0a01800000000ea270000010000006b483045022100e7b15edfbf8afa13c2f54ee8f3e823356390233089a21f65047d8b92eda73fcb0220229c0931dc60d8261701e1659f8c4152f74839a956546a76bad06e6daa6bab28012103b064dd1ebcaf614f6eaf15b800933aa414b9f4547f426acd0a8db335d3a0fc68","76a91495eaedb1ad7599337ba2aa6e77d6b36f6aeeda4a88ac",0,209,"ae5490ad44b1eb37fc88d7cb9c636ee9c6f2d85f9b767079005796fa568f01a0","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002d09acd5e7951b35766ece4108632eeac5f9bb2dab18b11beae21779c446f59b70000000000ffffffffaaa53ed89f1bebfac15f986aeaa4e60e3718ee13e2a70f5cb201c6de5000201c0100000000ffffffff029e491f210000000000001976a91410af058288a956db91be66f1fb0514c03878401188ac262fcf010000000000001976a914d9a6f0a12165c9d61ed93070635e3b676713464c88ac0000000000000000025258ed1600000000e4270000050000006b483045022100dc3a1bcccffe941af4d0d9b5c18207d467f627383b4ede8a37cd77c1d84c213b0220322f5fd6ad7d8145cccf9dcf488e92244786089c0169b067eada25303812678d012102fec0f25a2804c6e65023b47ba100b9f1dfc854751e3fe49e43ee83ad5e715bffd203180c00000000ec270000010000006b483045022100f369b0f9a9de1e304f08d1d67a899187f3323bbe69c1a86a457fe830b0618aeb02202726ae2bd1e1b960d2dc26f398cb8cff8bf558fcbabcc2763555649721450b650121036c36a772259c747cb780d6858fe3b47868231edadd7840fed649348a954edcfc","76a914001e7d95f50b98c8c2e5337a315daaa53d473fdd88ac",1,208,"f76a4df4a8584e6ea3d8237eb278f571122232155fb1c088472b3473dcd970bb","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000025055c59678b6ea89b569655b725d93b1141ba3a0d46dcdcf9c46233388d122ec0100000000ffffffff506d197add77d38a2b791445523242d6ef0add553c6a254ba5fec3d723288f910100000000ffffffff028398b30b0000000000001976a914ea5eb4700b9fc732be663def8abf53f199b611cf88ac635802010000000000001976a9141621630857209887af5823bf1a01da184403e18888ac00000000000000000205042b0a00000000ec270000020000006a47304402201b9752e189e2166878133eaa12004df32391f7cb104eb94c7777ffa4b55db1a002207815da2c1c59fff3810bb4721c34194c43588cc76e1c013db6d3c3f0c0fbf69e012102eced671067e2a816b115060ad4e6827eb741efe6b5d04d6eddc565be6388d2b741d0a10200000000e4270000070000006b483045022100cf33828536b7d94d38adefb6d05ac3678fe5018b03dd88d1c6ffd5cdcdec9aa202204e7ab8735d4278d7a7a4747d29253f1d46cac79da80e4ce7342ea8e0e297cad2012102b052cb00e72b5f0f3f6ecc3ff114c647c23b4ab20c93b0cce59de5bb824229d0","76a9148a1a3f826a4bf7b9a64a49071ca1c18a4d9bf42e88ac",1,115,"aaee9fdb3d551a86817c81d478f2f994e4233332202ec82f9120015e8985d539","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000002606ef938fc1521fbce4cb54b52ea750ab64b9ca9b86553fa466f3defb8a831430100000000ffffffff80d8a7dc47079e387e2052e29c3212850491b445223f5bdef42c7953480636460100000000ffffffff02c714ec020000000000001976a91464148b61ebda7d13bd75f04f1d877a0e598867c888acc1a306000000000000001976a914e40e3137d6157052cd709c5546d9c568a0c4aae788ac000000000000000002262fcf0100000000ee270000010000006b4830450221009c01ff42998a777e29dc09c098dcaaa257b49170c4d921b9b38cf0875447c264022076c8c5a61266214a8aa22743bd9260c06a0eed64e9d43362f7656840cff1e46f0121039775850b37d977f4e46eeb21a5208f8ed3b4c43e1df5d9e40aad2436892415a3c26c3a0100000000df270000070000006b483045022100ff650e9caf0a015b7bd58247c07cc961fb10ad4adb79b90842ef294fc9b350520220248820de1f91ea20cd18643849bc59378761ab7f6ba1ab5845cf5c9cd0067e590121022933c02750c1996ee1b5130c50a125e0199965ca828e3557417df466f6697342","76a914d9a6f0a12165c9d61ed93070635e3b676713464c88ac",0,79,"ef786f2d1feb76191c1859cd8f04ba25fefa8ecab0b0381e9a2f46fb64851061","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002e7ee524e2375355e9cc1039a102568a78f3a6c7acc55798f4cf6fcce82f01f660100000000ffffffffe86a73bf2e0f3b8e13dba34646808783d49d9bc33d6f2056f71f2b83d95b321f0000000000ffffffff02174d6c120000000000001976a914e2148899f721f51d47682c86fab5fe7f83303fc888ac5d4e983c0000000000001976a914f112fa4c40a51970db4d449f40c88e397113ce6088ac0000000000000000025c9c7328000000006f270000040000006a473044022047d7c8a09615b1b896f8a1d1ca7bcc14026e93d0a7089fcbcf67aee13c30bf3e0220145658e27dfc983f6b6e1e3441961e4c4fe587ace2386e6526482afbd30f26b2012103dd53f4eb39ce9d951af4a10b89255665b9700ea0c96dd227e472cf16db7c5a0b78e2a72600000000f6270000030000006b483045022100f554054a8a1d28a93dc5f7649d5b9d916a6dd206f4329e880ba706e40603a179022056a06e481deaeaa62d84446554ae7c47571284221250e0829564165d234d7c7e012103c6dbb3fdcb91282d502b36fab1971e53f0e07425dd3266e30ab1a5e2bb3549a2","76a914959e46bc922db3edede89cdcbda4bf30ae0c07bb88ac",0,20,"57cfcfafccfe992100ac2386690b584e5d4d34e9bc58ed6038be203261079d35","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000387b8e1b69449d2a602f97096ba59430090b47ee72c697037a5d5bab285982f140200000000ffffffff546f3f246b596ad736fb06f2de9ccf114d67dceffc6c114659049f57959fbc3e0200000000ffffffff4233f55cf6efaf5df7cd3312f9ee0763338a392e8cdfd153cc2c4577d8153b340200000000ffffffff02a2896e0c0000000000001976a9143def19339ffd128b6f79da13cbf6447456783f3488acbe3dcc410100000000001976a914e5b3e87e0bc8a80634511bb6ef550c789904052388ac000000000000000003b1b1eb6f0000000003270000000000006b483045022100bda2fe47ab1a18fe458d84cb9b943131dc172fe8729cf70422454112400ad0090220263dc292944d107df64d5178cf86843f3104c78acf02771f9a2255616d0766ed012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d4981f3bd96f0000000001270000000000006b483045022100a9bdca58581d52422d31469a175351201a60a80ee2d2a116764da85a87ca85a4022071636a78d247ad8ea6e91556dbcc2bd9808556948e72593db42fd0fc0ccfec2c012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d498d025c26e0000000006270000000000006b483045022100a9e3806fe478345b58fdc0c1df8bd253d45b8585afd9dae1d71b27240a43714c02205fdf76554c99e444979b517b73ec1a7bb79959ce1d769e5c724eca4c1d089f79012102ac8eaf6ba9cf87976a7b6712f2f9b0bfe8fbf03c935fb27c42b4a6198538d498","76a9142ec5027abadede723c47b6acdbace3be10b7e93788ac",1,127,"3dccfa3650b97619e3c239adaa215ca636ba16903c2f3f533ed65f1c00a663f6","OK", "3 inputs, 2 outputs, sign input 1"], +["0100000005dbde18588f97b11e3d0c9c3c068dcc3ec473bad4139cbe6b11688ed8b650e25a0100000000ffffffffb7cccd85370f1af86779bd27c5642909d8cdd3fb478a44ac916ed2b1c6614d220100000000ffffffff6c54d84e5ae537b2b5ebecb6d6c3a9d9340f0e7687f9e646170f6065bd0e72770100000000ffffffff902fd28f78c810ff429d90282fa9b6897d74d4d94b4da776192f40bd550947100000000000ffffffff9b0cfb5247a03048646bf7b625c71a4c14667bfdd342c7a7c1819e5445ae51180100000000ffffffff0282c66c010000000000001976a91407d3a45a3fe916a18e3ecf6c03dfdd143dcd3aec88ac00c2eb0b0000000000001976a914b1037bdf6a4a1ff685d7e896f0f92c04d29629a388ac000000000000000005b257c9020000000068270000030000006b483045022100dc6fcbd0c31573ceb56f353cfde3fe8918222f6f77fd723698bd0a0c523cea9402202cbb7fd86ea0917ea2ae55986e52ef05e6aa7b3b7ef063de46ab19b2bd010d19012103c5313e726701b9d18b95619530db0db2c3b69665ac0e12aca09fda80cb35ae37d951b40200000000c61e0000050000006a473044022004bdecad9dc6c515210b3d4cf62cbc68b6cfe29a912487d50f44b180a20789dd022039a2d5a2563ee722f4fabc2eb6b7ce292c014291f6fefe97ed77be71623e8eb9012103c5313e726701b9d18b95619530db0db2c3b69665ac0e12aca09fda80cb35ae3780e7bd0200000000501d00000c0000006a47304402202f0d7751d026e46cdb5b9173514e9f5f3dc8ecb315e10701b04888d0f9f7bd9a02206ba712eaaf01bfeec4c0bb22ea744276b39a497d944ad6cca236da5d5d93e75a012103c5313e726701b9d18b95619530db0db2c3b69665ac0e12aca09fda80cb35ae37cb74b10200000000341c0000020000006a47304402204a9f608845da530fdd00278060e2e83a24ca56ff3fc2d16bcc105d354205907202204d6644bb467d4c5dc124e038940b1a0fcd4c79e5d33b17c3966321376b7d77c0012103c5313e726701b9d18b95619530db0db2c3b69665ac0e12aca09fda80cb35ae3734f2af02000000004f1b0000010000006a473044022057cc8b32c5ac4fbedd6fd721cb2a928aa969d25cc2ebf505271a01b20399b2d602203ce25d9258194b482f1c93b0cace3ca5d268d0eaed05dd4e9495c19f4259babd012103c5313e726701b9d18b95619530db0db2c3b69665ac0e12aca09fda80cb35ae37","76a91407d3a45a3fe916a18e3ecf6c03dfdd143dcd3aec88ac",4,238,"577f2edc711e5a690196ec0a0a4da0f569008029eb1f285c1ada8ecd8d949b33","OK", "5 inputs, 2 outputs, sign input 4"], +["0100000002b399d19cd8f8f61ca94a406c42de3848fa07b053fe5584f068c17fd775a922110200000000ffffffffa5f78d100c230a6117ebf8988e9d71048e65c4a4493bd8970dca0e012729ecac0000000000ffffffff029d7929190000000000001976a9141c6bc6ee8562b0304b8e5f2e8814dfd3b65cf86e88aca0fcf3840000000000001976a914f20d8300a5b873b3da5f364634b460272f7ea5e488ac00000000000000000290da756e0000000004270000000000006a47304402206832c0a822e85c4eba6166afc08978baa3a419af13b71316083dc6998fa23fe2022045f30daaad52a91608495dec821fbdd840e9ce36d78569880eb0e5bb2be7c8e5012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8260d7fbe2f0000000001280000010000006a47304402206b234eb140098870764d1b974b5f19b1f41a4a8c33433502b479c6c40baca88902206727eaa04f24aba070d3297e48e468628823039917c8977f87b10635bdcb67c70121039b67568949a11e2b498ebf32f40e798864206445fcd34041d6c861e5d8af0f3a","76a914e9d0a8c0dd795fcf0e56e8a35c5bae90451f676088ac",1,30,"76808912edd24ced43aebb3d931f91bc6708a02a7336475e74d904b681a7bd38","OK", "2 inputs, 2 outputs, sign input 1"], +["0100000002b76f42ee5f9dc323433c7db96eec206f8396870f672023cb8e5826e6c3c40a000100000000ffffffffd2152fba67c09145c122ded917ebbe6aa9d753c594e1f64f9f12f73256ec99e00000000000ffffffff02819d97000000000000001976a9148501bc1638809bf3d20b09e41f07e3fbe13d400188ac400d380c0000000000001976a9145f3cbd7cc04e08226fd9efd03947ea835df4aacf88ac00000000000000000285595d0b0000000020170000010000006a473044022044e4fed65aac9e5649a9b0eb563a716c210968523976b136c32b56954c817a600220500ae7e95dba1026e4bbdb6835eade04d26d533b7182d1607fe108c087746000012102204dc7850f043f426f3dfd5dbebaaf0a385f885728426e3769be028bfe14ae577c9cbe0100000000d81d0000010000006b483045022100bf5ba5258c1d785fcfe369bbe51085422eca590c50c3ec52b8d1ceda4c0e35740220300604c38d3fd7b9857a2971ec8ddea6cf9ef1a3ab8d2fdaa78333674646b13301210381256f1f7823f144ce6911e66a5e75011d209ddf8b3376298d2a91eff52ca720","76a914dad49fe840a24e1664b2da3f181e2376f8083cc588ac",1,124,"f5b27f065562f3c23dbdc1699478e79b4b091fe2cf28205d8328a1d72fa1f14c","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000027d12955cf1f1da94856db3a61119aa51cafd87d2aa55e64db2d3016631076d390200000000ffffffff046dda89436a4acbeb9ef5c8b8c4dc53837dcd999e4d6bdb6fb95511ad1e84cb0100000000ffffffff02dec1f4760000000000001976a914c7d537095297d2acd0c1cb4c2cb5e2530bf54dbf88ac7c5f67440000000000001976a9142d8935095de02069db75efb79fdadfc72bedffa788ac0000000000000000028ffe696f0000000009270000000000006a473044022056a32d2074a97b2fe21b67b0bed280aa29e81fe393dc6b55e0f52116f32e4f59022063bd7fc144395fb9a7ce82ca21f5867fe0920fc69e66b0893e5ba2af860b50f1012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8262b06094c0000000007280000030000006b483045022100bcbdcb2512cc60c96952bcda2c1149e06298eb21489be353c710b2411e7b914802201ff31333c8c616ac1ed01414f5f2e9555dd87eafbc888bc4ad91510324b00c770121024c4cd65f86a7c6f1b8a75482f4b1a390136827ffe50f32b8aee510ece52197a0","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,149,"5de2023db57fb83db64f3f798f07d547e685777e404b7153ff7aa1c6f3af21fb","OK", "2 inputs, 2 outputs, sign input 0"], +["01000000034aa2c08d62537a0df86de1a51b32b5643b599c6f1aa2eae01bc1fa5fc75fa1400000000000ffffffff210f9d536e5738746401bbb221118baeeabc8393901da5d1c1ac77f5fc9542a50100000000ffffffff7d9e15d95efb24a5ab36fa78db1531d1c1f4f9356639800f2d97a7bc0173dcba0000000000ffffffff02284759000000000000001976a91448f63d1119c5c456ba1a478aef041d816c8c808688acab617d300000000000001976a91405f0ad88bc0c202543cc4c518d4d794dfa1c639b88ac0000000000000000031122d21500000000f5270000030000006a4730440220621a3b447e5f51dd276c50f0fdc2fca52e4ea190962d97a60f04bcd9905e9e5c02205277f98846d8abdef01442eebbe5f5d98f2b4f5d357c172c13068f59c11de549012103dd4b92442bd0727be99857bd903b638e6cdccfd3a4bb336000e98890da28a28aa6f0bc11000000000b280000050000006b483045022100e17d574371a21a9f5639eedda887525191a18ef3a6996ae8b3a4471b5f926f730220343d6b514b3249e4be824db1add17e8d8f3c8f498141e8139b9541ab2b775d050121032bab9e580a7f237cb918c6b2203987f06bf5b9a119f70baf4d74f53728e744337c795e090000000007280000020000006a4730440220210653cee25b0f6b8cf42ebd080074bec3aebbcf2f2289af8ed2db9270dcf6fc02204e97d25153e2bc52c127eef3ec4554b78b5e04638ee355361f6e2c08334380fb012102266cbd70cffd302b258d4283c63cd5515279aec443dc2823116ee70408eb0018","76a914d555ff2acd9b82482c90b681adf8a39f4120740188ac",2,67,"","SIGHASH_SINGLE_IDX", "3 inputs, 2 outputs, sign input 2"], +["01000000021d20dfc8a152208c629011f02b930174381bcbfe6db3d9e5bf13efd24b08c08d0200000000ffffffff138e53eb6e3360c11406a2e694eee1db385bd23503a779dd53f2099edf46d3920100000000ffffffff024c3886290000000000001976a91482f4e7cb10b8bdea2a286593626599c3048f53dd88acba1546770000000000001976a91499181eaba77900235db6111e705c1d195517722e88ac00000000000000000272519b580000000011270000000000006b483045022100fcdb53421c2fb864801db63f3cfcaff701fee70d68c67cf8f10f7ab5d233ffb302201afb95b742d18204c4bed66fd9a7798495a407a5b3cac002b7582d8bea8699a1012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826f4df4748000000000e280000010000006a47304402206d56cf1832d161e2ab0ceb48a98ff5247b245ea2ae975a43e21e2cdba012c99602202dc0bac3001796c7031c2b23451fa50c155923e37181e9733d79475be0187a4c012103e788e762c7498a33b47ca6ad90d781f44e055c00336b7172759f69b14d376cfe","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,179,"6114b45dd62b98438fd20e0d23ee25816bcfffeeb2e579b72764d6b86aad6a09","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000002df0853fb5b20d86ac0e4e6191ec45339e87d336b43c57e5f38d9fa4b0678615d0000000000ffffffffd3f55bede9f3aaa62cb51ef70c6e5af8430d6f58514bc6ed7ce9f40de2e8f1230100000000ffffffff02d3d5fe410000000000001976a9147ee4da3f65fb223d61a383ed488b495eeff2dab388acec1603050000000000001976a914fb335b41d81c44d8adbcc7a489c2ced8099a324288ac00000000000000000205cae530000000000b280000070000006a47304402207112d8f5438c94381b10d7980311b94db10e79c185c8ddd7dada8761f16fd9e902201bde05f0d32ae198fb09916037ff5f6b055d510a6a66f9053a953ada35321253012102702c31e90f87a71dfa7fdc86f89c9c6c0df296eeb89cf93839095de2e91f74a71a063316000000000c280000050000006a473044022067de1dca22033ae1f40ca40e5cf4df995b6d2b75edce6879c216e0d02640140f02206718309c0ff117a7d232b6d403908cc391aec7daa9c6efffccd3a92dd7729c0c0121039008aefd5855c648dacf2c06cc278e0b369197177aab6e31687cb434f54dd42e","76a914cf7d83a408027efe67e4068986c14d7606eecfe388ac",0,148,"e9f644d9aebffc465657e904c093a254e99faf02ab8a5f99f0a97e24224a0401","OK", "2 inputs, 2 outputs, sign input 0"], +["01000000032af83ebcc339b8c2777ccdfac7127fcc201aa8a46beaaef1b3257de8295a0a5a0000000000ffffffff46c3f8cd44c44741eb3f5386484a5467544db6c4b969285d01cd3f92310b7c960000000000ffffffffb4818a46886360fa6032e5c6191488f5b818f14fe23ee81bd4520e1ce704fef00100000000ffffffff022bbe2c000000000000001976a91468c20d65c1564ed79d151f453b82d4bf15a3dd9888aca29ee5020000000000001976a914ded38f638c22356f7e072aec416623347acf40a288ac00000000000000000330e54802000000000e280000030000006a4730440220586d18aff615ca16bd372d6839280fd8ceb4e084d6397685bafa55f16521e51c02203338a4ed7c67ae98b8522cca9db240b7ee5219946e19ceb448c973921332637b012103fbbb51905ad6c967d85ca65208d0b53962984e8d510f741d74a9a378950d5321acc9720000000000e4270000030000006b483045022100d2677acf43713c43a80116491870b01c37ab62c6788806256e9db8e604e3836802204cda01455ac951edc4b2d14b3e96a86a44555842f20c8b42a1afe4b98577d456012103d57fb747711aa56f92f9e4146ade440ce0c8ac8971a855d9c6173bfbb68be49251916d000000000005280000020000006a47304402205ad97a861a5408032010e83a26b794d82d0a8fdf0520408734231c5ae4ba5d83022021e3c8911f84a72951f87fd31230fc7c6cab210a2ce33899d42b92bae2d4c273012102eee7fd5927b7a31989d3dfd0247f6f970339e430d7fb74db46108d5059386ef5","76a914519cfdcce0ff19a68783727c7e49ffe624f3f95d88ac",2,145,"e0aff47f81a4c432e8c74d8e5f58ab15d493e53c9b45ff2234cf686f11cd956f","OK", "3 inputs, 2 outputs, sign input 2"], +["01000000024c8e3cda71798647df6407dfac12fb9f373850bf900064311c123b767905f55c0000000000ffffffff062e2af1e78f71dc699188878092e8842181d11dd242d1c6af57e1f711d712450000000000ffffffff020b42d38b0000000000001976a914d9e156d71a92cb542826938d38c709aa07e7bfd988ac252e2b070000000000001976a9149b63a413fcb030273545f457d0ad22cf1485a03388ac000000000000000002441b8f690000000016280000010000006a473044022046a59daf111e82de78f0af5e71c26cba0c453c7b2f4d27fa8d338a277ffd4e0f02205c97af272c3afc5fb9000a804198139fcee17bf1b3adff48e45a9f6e7e9af76c012103def48d1d5265fdd21c0353eb6aaa4a5fe68c0ad8204961569aa5c7a0bde7d68f4c3886290000000016280000040000006b483045022100d5bd1dfe1338fa15895eb33403fd83c777f580d87f9181b045560e6329e6a7d1022066013117e4b9b3a92f9017df180eb422deb9fb6d8e15b1bf004925b8c196afd20121026c2c8b438b44a895a75ba89f0131e49cb30c135a3decba898b0e7b3ea31cddf6","76a914f90a19f603357344bcc2d6e210c1a0dda35b2ab888ac",0,150,"9c5bffbba59d06e110facfe00ff35c4e91d439b5b6760f2ff4a6e624e4243171","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000003c62fce12f440c2d40b55e8448db57daadf4661788d5aa5bf82a3d68f2d5be17f0200000000ffffffff712701e9ffc776c63b472322bff93373f5b480ef11367d87f6241ce2f9577f860100000000ffffffffa85aee67d86d07d5af30ab6c1eaa793e6973fd6558272ddc54e1c260aaba7dfb0000000000ffffffff027daa22ef0000000000001976a9146da90994317bc8703b6a4fb1cdb850236a52783188acede534030000000000001976a914da5c71913fea66d560d6c508c296cf94a5203c8188ac000000000000000003f0bd8c6e000000001c270000000000006a473044022032768262dc3048e186a8fbd87298ad683e1c915fbf516eaff8e26c8c5b569c1702204de795465bff885a2f212be5a717da0567f4765214c0cadefb42894414195e68012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e8269fdf28670000000019280000020000006a47304402200d667c582dda34fd2f8a1938b0fe9e9e0e5e0533a38a71ac7204cd0efd0ba2fd02206a0ec96c582c9c0f69d7b781e7945fd46e6d9d96e6f81ec86b80151ff6bab10a012103263f9d8bfdf6c62784948d8e190d96750392ae9b01ddd80da79ab31aaf706c4a3bd6b81c0000000016280000080000006a473044022045ac04d4f528b04eea63219c7478a425c4a67e51986e345550f8b3cc61c0b746022040b4dd449fc3628efa7d8564a01cb966a2a428b9141dd39aed95b25696bd79440121028eef79cd962e56c9f09851da6bb1b7ff447e3dedacc7bf5a0e3db61bc89b5625","76a9144ef3a09dd0eb2a193d84250bff3c85da41d591ae88ac",0,124,"842d69a1ebd41ef866129ab5a08a3913b9ba058c6139ce26873a5fbb2971befe","OK", "3 inputs, 2 outputs, sign input 0"], +["01000000034afcd7268987abea8e69293c46579fa7fcf035a44d56ccd38ff0f32468bd674f0000000000ffffffff4f564b2376a0986eb23fcb0a764dcb97eb37e1f2fbde800b1c6a1db21590bf410100000000ffffffff36cedf17506f5663661cf5b1393f1714bef1b254a083ab9b45d994b977f6fc6e0000000000ffffffff02364a73050000000000001976a914d4a002ec2559eeb9ac713d9e3227425b056bc5e088ac854919000000000000001976a91443c8f62ae3b445cf8d69e9d94eadc8748aeebd8188ac00000000000000000340d0cf0400000000db270000020000006a473044022007f5e7ee53dee0ca22c751080bb0c724bd5931254db61c32f6dc7ac00a76abc402200b97b68c27372107fe15281b423b322469d01c6b22c402afc35fb17b0589c8f8012102294a3dc8d5faeebf441e54148d8cb1b44fcc747d90b42ef1e8c83198f655d5deb35f7a0000000000cb270000040000006b483045022100961b1c4f906aa6070d5c3556af8510f8ec85b9478476a93296bff460d48f605702202f2b2b79bcce61a2e34a33ef8fe42a625ab531ae290cf5ebe1856cc4543ae5e201210287e8c57a55754fc408d5b0c88e0aa9e307e686468eb7de5d51b14b2d0b7ef02628475900000000000f280000010000006a473044022037f43b83c59348e474dd674a058cd39240b2e307b5c9729a76806f1d11dad5730220128866951a10363525ce6749d1e01300de00081694a34671475f1894e93a5906012103bc47e3e75451f44c7137b26f98bd14b0c07f7a65ea98ff7b4305b1faddd6b64a","76a914e6fd2c5e9f71a1282c826281f99bb03f0b9c2f3188ac",0,188,"86b5b12e740796b4b3139db460bee2c72c3eaf5056e36a65563d01e65eff0095","OK", "3 inputs, 2 outputs, sign input 0"], +["01000000038e4fbfcff38ff0a84a49f851b3ac43619744fc2b69ae0e3dd49bc2648f6ac36f0100000000ffffffff7cc02d16bbb210809964318af763e88f99da4deb9e1fe247b02c09f687fe589e0100000000ffffffff8dd3aa1a541686209642ab07088b2fd493a8a476bb6750b9677dd20539bbb86d0000000000ffffffff02344f80470600000000001976a9149963b52b3c6347c1d9e3771923329d6f93021c0488acfebfd0044700000000001976a91433d068f98a929c071229fa4db089231a129e366a88ac00000000000000000388a5d87928000000a5250000020000006b483045022100f873e569b8f49b49cea044a9c7952032b77fa16446b2a4ac1844db419326797202207b666b77b37817f884cbc6e655446954108e76d8d373567c5b6c4dc74764680e0121020443606e5dd531b9005fcfbe0e9b25de9c21486542c6ded29c169610ae7abfac8052ac63190000003b270000010000006a47304402203589eb3258176700f3689c976819f62be3704e955b8f0bf6dd34bf623f45bb1902202567a9c222194aa0b5d4cab30eb807e969c490155e01448ffd18a3ff589485110121025489840287f55a35b6536de89f9e57a5c7e7c38dae1dcca085fa9f88816c87928afae26e0b00000012250000020000006a47304402202f5ca67b35488569b0b1c7b919994c42075e7214975902f0b43a973f2967bbc102201ee77febc0a28f91d1496109aacb6f634ea2cfc4558a4d08c567a01e2296bd81012103616da47f59308616fde6092b7a860bf0d7fd5845a960917b3a5e972d37f4f6c7","76a914f54b2d02a2435b9cabeb95b29bef37576b67c11188ac",1,239,"3fc0bc69435583256a9c225de960dcd5041ad3c6b1691c420175f8c9e145fc80","OK", "3 inputs, 2 outputs, sign input 1"], +["01000000023d93b9b715a308a937ec7f1c5e95df76cf65e22613dfe1156030f7c7617fd01d0100000000ffffffff1fb3f72708f80720775664d972dd212080fca8276a8e4a23eee218f9b9d49cde0100000000ffffffff026fb6d5010000000000001976a914644f9f600c64b95a24e4332a1e362ee51b02162e88ac0a634b060000000000001976a91498d7577c9070afd6278987870ec7a0ce367d825c88ac000000000000000002ec1603050000000016280000070000006b483045022100d87b84312d22ba937c5ede305b15ac80bf34df6aa046262d8c148b09f43cce53022036714587c7022132d5a2882bcf672a37d7ce364c370d178d223e5589ace17ce70121036de156af6a390b39724d6210271c67d7a04867bff3f203f2a0133cbfde9266abede53403000000001e280000010000006b483045022100824f4490e458b5cd4a3e926c01d40d846962d094169eb84aa4f553321198e947022049491f04346a1b70fe80340e9245cef2beab72585649e9c0f598274f981b06b60121026d41b7ef20a25eb1db4372d05c8a1b1223864166fb902bbe3cc2cac082269f54","76a914fb335b41d81c44d8adbcc7a489c2ced8099a324288ac",0,188,"e0c1e41b9dc3c0c840959addc08ef62bab321f7b5b6f059a45b426f30d898708","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000003f3f125f54bb533b2ca359a6ca243dcfbbb8ec852418303eecfcf45e7b293d2900000000000fffffffff1300259344f6ef18cfdb8a41e9bbfc8cbd3b1723c39de860dcfc75a098a95080100000000ffffffffe91b0d061bce4aebfda14241e22e9c534b4b834a53ef2a27de836c0a98849c330100000000ffffffff02012f7d460000000000001976a91410af058288a956db91be66f1fb0514c03878401188ac2f9aa8000000000000001976a9143e8443c89189ef8e4b44e204db984f39379a4cdb88ac00000000000000000378ea56330000000089250000010000006a473044022056cd11cc74751116c3c43f0394653928934a9a3eca4f404a4c400fbfafe9d89e022015992b1d1290aa97757456e2edf1de6bf1ff4dee1dacbdddac4a84d54e17c5bc0121035c26bf00262db8d9e855b5bac9f25dec860a9916a6393926288c2a487c188346b569e312000000008d270000060000006a4730440220184f5c4c38c1529472898ed7085442a54303801090c74f4dabdeddfe7ebe6ed3022064df0ab7ed12aeb879dd1db4a381271faa630d10dc3b4a04d10962d8c1cb347d0121031932d1c482f0d017ad3a149505b0002f8a5d311fbe5bacdf8ec909309c8a83206358020100000000f2270000030000006b483045022100f61fde5c84bc89ee5a2a523ef87b1c8d5b12c5820f2030d2ce485c5c16d90ab802202be699b40951d370289de85045ede4d862707d47dab540016fc817fd315c8195012102b5c920259dfe73d9108031bc3bac27b663b4ce69afb0657af388474530f24de7","76a9146944b043c1aa8cb9df724997c7d44ed0ccbcdc2188ac",1,139,"8f32767431df2790f7ae2a639bd638b891ee2550fe687ac9d8f8acd396965642","OK", "3 inputs, 2 outputs, sign input 1"], +["01000000047a2d6fe5f5d03efa12b374e7dc3db342906ecc5cb81735dc8ecf1e4ee5e7d5630000000000ffffffffbdbc4e35d9fe1cd98f71624262a0c48130874104c3dc1d0b3b41884863f52a630000000000ffffffffe86f8c5b20f570d7e1af26c22f8a90a3b426a99b29d15ef6b714c75e2f57743b0000000000ffffffffe7356796e091302a06cca87bf46eebcb3e3a1c56331519bbf9703cc2de2639080000000000ffffffff022e9f78000000000000001976a914ced4a710e223b7f77778b01936e125d0d6f1935a88ac29c25e060000000000001976a914665b687b30717705fb2a3a07ce0cf0beac43b9e988ac000000000000000004a7734c03000000001e280000040000006a4730440220264d7eef00cb9913a173b70d92318e1cd9e2a6779d2c85605554c73088c05fcf0220530854b859d73166595f87e65793e5c443a7e1fcacb59f4efcd0630ccb56ccc30121025efc1b6830fcd67c571ad8a24bfc5b41545cacb441a2a78121e3fae0d0be8f856fb6d501000000002b280000030000006a47304402201c6b9c02181e2d10bd10977884566edda06b611f04bd10abac21d25c454bf76d02203c23b8a20d6930de97f4797ccfd76f322435def3aa9e56ac6f0514d2a99617c8012103e403d7d9c62fd52b1b2c3165524b14ecb44365ae0c400b0ba27520f097b1f4295624470100000000e6240000070000006a47304402204e1ee1cbc74b715a646d0d7bafd05e7ebc3ae560436f5b8531c86e8f4dbace0f02201e8583f76de9e00bb6bdc781ffdd07fa4b0a3a881cf8218f35ed55631e7d7a35012103f0268953ef3c2f7225b2ed8a68d690d6facc231798438673a5611547b62c96024bf68400000000008b250000040000006b4830450221009f6f2fd27944326817c90e098604c3b7c5c4678c6232b7498324a4149b5c26bc022014689fcf1302991c5a9cdc285952a9f20bf8a912650e05408e519fd9d639dbbb012102230bb3d73e7a84fc228bcbbd48a64afa92f1024152cd5eae8c18e3a4e5e36b90","76a914554f6b84c2eb4afc94f793d5ce3c22599a4eab4b88ac",0,249,"79b5354f61f67857eca196cfcfff8fafcfb332c4ab90c31ce9773e547ec15936","OK", "4 inputs, 2 outputs, sign input 0"], +["0100000002f435340832d306a327f2e4a1b9fc1e54cdc6a04a32282262a7c4f52912d470e70200000000fffffffff3758bf93510fa70f7c95b0710eba4443549656bd76b9469c14900056da376970000000000ffffffff02ce34a8a50000000000001976a91463fc3e9dff4feaa3583b7d019f3aa201230b8dd588ac198d2e2c0000000000001976a914aed60217027e6d8bab6df91d289e41cafd289cc388ac0000000000000000028fda756e00000000df260000000000006b4830450221008634ec464e9aa36810cbdcaffcd7f441fbb1100612df731e423a3099eaef8c9502205f7b9797725e1cc54abcf84b94d965b2116b11aa706c1761695a80521bbcd6e8012103cf807ecccb35ea19db1716d41f9fcc7aeacbb3ed8cb24a050bee972c7043e826b8ca77630000000037280000040000006b483045022100aaf04d2b8cc90d4eb6ecd4d68f9b6cd757e3e70c44e39d0b87ef5e1304963b7b022006414e0c42c7473bc9e5d7c278ef32d8a3f694c1048fff00d1d7e205b1ca3f2701210302f777eb4c3064654f2cb84df98d09388b8a7787dcd0a9b61ea36790222b6a6c","76a914beabe13e1c07ebbafab03c21ab20cf2e4d3ed91888ac",1,97,"5aa371ce787448254c424d16e75f60781396a42b8c0ad6ad792ff51ecc52da98","OK", "2 inputs, 2 outputs, sign input 1"], +["010000000295ca9f9cb41b030a9bfb1913fe842ae8f13247fac62b1b6127586d8fa12ca1e60100000000ffffffff583097876d81a0527617d877c6ea42b2440c1730993aa3852437961fbd6d4e1b0100000000ffffffff020ed090410000000000001976a91410af058288a956db91be66f1fb0514c03878401188ac123e4f000000000000001976a914a000e0bdcf91e895aa97784c91ab1150cdd6002888ac0000000000000000021cdda03c000000003f280000030000006b483045022100b88a7eb2bda0b5a21e1122dce1942b5c0ecd39ad053d6294ee01780834da803002205b657ca506f194b0908114f0112fb99bc329c51fe8657fd5639176957a3366d8012102c85fd1563c18d384624ab2ed00ebd79aca470e5f13c073440536db84a6c62809641456050000000041280000030000006b483045022100dd7773c8d13d19d186b7ef10841c5c952dfe7a83a980fa1104100e2785b8a9e802205d09e502fe8caa0de6841ae548372e226369d063a9c32964b0eee71a7af265740121031dddd7bdd29a0d2dd93014bf21172d640e32b8d0e325783fc99d05759ed6e957","76a91470476440dcd05f9460b34f9ad7085fc46f053b6488ac",0,47,"3d7d93fe8a9748ec9a2b51691a288731bcea50c5be432ebe2dce97f0c0172184","OK", "2 inputs, 2 outputs, sign input 0"], +["0100000005fae8a179a28308796042ccf8e7d46b5c458556ef5910c926d58590cb83b76de70200000001ffffffff88d137371b14fb81cf8ce7e88f9670a514ff847659883c36ddf77bf8847f59bd0200000001ffffffff0a2ea614a66ad8b3bf3e839c1c471ab5c90224c9293aef03c8c9abf9321c39680200000001ffffffffc0e49e2609670e64297f0142cbaa29378c9beb7ceec822dec3ca77c996b0e0b50100000000ffffffffc47735b9bfc32aac96e9a0defb25d69de33609da6261323facefe806946c79f80100000000ffffffff02bca9a4000000000000001976a914c3f22500c29480e79dbc69a8fe3667d6b0dab94c88ac008c86470000000000001976a914043f4f0fbe1ac3a42279ccd768bb6f90177061a188ac0000000000000000050e8bf71600000000f2260000030000006b483045022100bde19fa7c2eba0204f9269fe966b2d88e7667dbce6089e80a1989a350901c9cc02202b5b387be974fa3e3b82c0b2afbf703d5006cd60bfa10350da28badfbc9630df012103bf0aa613bfdcff0c35093cc8c1ea2425752c481a7c3f47f7a98f5452cff30f390e8bf716000000003e270000020000006a4730440220622f8cea91ca09697234d686a40a173f5c6fc0a61699b79669a0ab6527b315b9022021671156eff385809f177fd8db505a2bd3aa93cd99bd362206b1b66e22621971012102f10f8d023665f542b7459bb34e25b127091db5ee2b04717c1dcfd75f0fff49160e8bf716000000002c270000000000006a47304402200c52dbfa908118cb753024759e065fd73e38001f8f926e2c95cc3eaf04a6ec13022008be4a714f828d7d3131957ae22b7b8c6bde17d110f154e8a63d543bd48f52b9012103fe877fc3072dfc90e66aa69980d49fdef9e251a2e96c9f5cfd79ee5a2530f122cc2647020000000060250000040000006a47304402203505d1908f19e8865a0a965ed3a5e450209585806dce9dd0d7a663ce1397a5f60220693e62f29eb0fbee19fa82073e245ac60b15c79c3eafb8bebccead92b75a80c7012103bdb617aa6c9a6cce183d7bb1d13dab695817c75bda132a82ce531bb5dcfa462706b9490100000000e7220000010000006a47304402207a6f6c383d5d176bb72ba1bfc511c2d002eb75dffe5e2397e8c5c9e27b18191a0220742f95dc78acba8b107a560efa47a35df3d1dc128e4f67fb2f9f543202f1c740012102a16bc5862268f98e66c4aab93a14e39aab6b7719b3f21f156c7481e276895e39","bb76a91403b1c00e68fe8978d5099beb1f1f1c3a66c3bd1388ac",2,99,"","SIGHASH_SINGLE_IDX", "5 inputs, 2 outputs, sign input 2"], +["01000000021e88f83682aec1f52de00f53baebb805c8174c39f5dcda39bee33e8fa0dc5b120000000000ffffffffd48b51bf7edaf37f7741e5c0a81b0a344496aee223a80a6890b0cdbcc3242a940100000000ffffffff02072dde580000000000001976a914df48f9f8ed8aa62387c5c6b37bd99aeef5cfcdbb88ac75fcc3230000000000001976a914e3f97ad7411b962fda73473cfdff8315c8e1a93188ac0000000000000000022335fa430000000045280000020000006a47304402202a71317b3206dd6d8bad5eda75220f6e05e82368995b943087f3b8ddd64bb7a402201a7f6b5127c2a63f10ab7a63fe795443dd51d0ed3aa520f88cd0dec88096db750121026319cbd2c2b3cc4f4bde7176289b89bc0775d05e22c517831f96a55bdb431d4ab9d7be38000000003b280000010000006b483045022100d52ac96d92ac9cd2d847ddab4f1487acf6ef747d49021f4927facb2dd453092e022029a59aa689c9dd708f491c810d92a90cc162bb77ef5b350bf0ba6806d5a87620012102ed53068920cffc4987d3e95eead701e0d3a6c9339ba7bb8e8fc98a9f068112aa","76a9147d2ee4e02d0d671babef02de1bf9ba177807e78888ac",0,70,"12d696660e0a79a9f78ac312fe37c71669a88d3a8abc67acad840cdccc5ac1bf","OK", "2 inputs, 2 outputs, sign input 0"], +["01000000022840a742ce9f6d98352b5236df7a2c1c9ee090bcff73a8b039a78a0abf20033b0000000000ffffffffac1dd3b6869b0acc65bba91c6e94fe82b12077edc849e112c5e293a631f5b8330000000000ffffffff0140fc54190000000000001976a914766ff0a4f7464b4955b9afd34efedea37a2a79bd88ac000000000000000002001c4e0e0000000014210000040000006a47304402201c09867be4f5ad7eeff3c3d7ddeba99fba48b6e6796beec484449aadfc169fdc02203ac4da9986edda4c4478b7c547b5003c75afb2e03281470da11ec4fbf3cabdf901210254a7fb53b79287786355180fbccf56b40d36ed6adac7f7f0817fb2995a395942802b530b0000000017210000030000006a47304402204e61c6185c7ac48395510db49b222310cc8d85a0d3a2ed74b8b0e08e95951af102204b26da29b6f041396863e73cbb51a5f302f57fc0c88c89fe7639a6c98be056d401210254a7fb53b79287786355180fbccf56b40d36ed6adac7f7f0817fb2995a395942","76a9146d6df868e1e676751c65d21aecb057f62ceeb69288ac",0,135,"d3e1c8ae6994ecabe25039c899bc29c5323123fa26617d8a04f6cebcd00b86a0","OK", "2 inputs, 1 outputs, sign input 0"], +["0100000002a52ddf09960965d15319094a81636436b990365a2806e318147b8fb550f9f2690200000001ffffffff904da4a23b4936aeafd3dfe2dd97e69142c479c00c01e41d240282533929e06f0600000001ffffffff0207ecbd010000000000001976a91468ff9f7c236d95c7be6ac2d2feadb73e0f33abf888ac0084d7170000000000001976a9145974caa8201b83b397fd3ee1166fbd451b64393888ac0000000000000000020e8bf71600000000c4260000030000006a47304402201987b3c1af3d8422529032feb976dbb0807863aae1b860f3950103c4f538ebcc02203c78da005e6e0594f794dbdb3b9108abe32f0d1bef6e5e3ea43f4ada5e8edced0121035a3f794211722794b8e62bb4a541b1c73ac3a21fcc6735a16ebed0ba18400dc13930ea0200000000d72500000b0000006b483045022100c07f944d17aa2bcb4e175dce3ef2b2ce7b79d1344ea77e41ff2d6652ab21e2010220359eaa04d79d99cd87fd1e409ebcca164774517f072f8e0b21d6f9aff7f2a3b60121032c4b08633ef349d9c943e077b85778a14abc3a78b484822007807243e1274ac7","bd76a914a6f7304b2404252510d28e679582dc136dcaafbb88ac",1,135,"57f6d6dbd9eaee38d82d7cb856752870f222ea983588cedbc0d64e2aa0217491","OK", "2 inputs, 2 outputs, sign input 1"], +["01000000041f8ead373badf9731c1e50cefd7ea90efeccd30750eb6819e7ec7387440c995c0600000000ffffffff1861f2c738a5f65b39a5ab1b43ba23ca6b2fbc79445fc2c651c635523a8f5cf30000000000ffffffff162c88889b34ad7038975fe7e7c2149913a8ecad940e0460d36f0db9f1dae71f0100000000ffffffff138e53eb6e3360c11406a2e694eee1db385bd23503a779dd53f2099edf46d3920000000000ffffffff02f82e5f010000000000001976a914d3e8a119eed55d1c31de3ed31250008387b406cf88ac74f964260000000000001976a9147844d52bb8f21ad3e9ac8a379569e41af431b17a88ac0000000000000000049adffe100000000027280000040000006a4730440220748348b99cb248f6f680cafcf0bd974a309550a803a9c21f8fde84860eb4b3b402203876f15b59963676bc86df3739095b904e1616a5968831c5709d03767f6be090012103683760dea1caf766ea5ba8cefc7bca14ce0c33993d3622c95f4e9cc5ce1ab39b5d18b50e0000000037280000020000006a473044022012a1279c3fbbfe4b7609ae4150332e92f7a6864c63dd32579d0fa15ca7200fbf02200576b455738023282641eecab4e61aaf5db8199c9e7a1d2ce39c47fb936cc636012103683760dea1caf766ea5ba8cefc7bca14ce0c33993d3622c95f4e9cc5ce1ab39bde54aa05000000004b280000050000006b483045022100c32de9e53990f35bac5acd8f3a070c313dfc2c4fc975c97fb2ade6d7431d6c5a02201399155a50af6ffb13af22c78e7fcd8a145251f43ddfcb65c14035b1f1b9c7170121031cc826550c2e53fa7ec43b116a92aa7cb9b371ef8dfb15d551daaa4e21624c28d726b202000000000e280000010000006a4730440220145b5b91281160b844332c69962ff7b9973ad1795a643a628d1c3b880632adad0220780b1cb1af20831ab93a6ee74bd84c98ea217389d709a1f63f07db5789f26621012102e1b05f78c7f5b7d1a5ded7dd043d2360643b871840ea9a6901eae1c2befa20cf","76a9149f6fd531866262f1177b464e85731d44a759d19588ac",3,174,"da6bd3b8a229aeb15b77737f00f9c46b61a206aa869d92be7835cfe230d4fb8c","OK", "4 inputs, 2 outputs, sign input 3"], +["0100000004ed3160fadaf662b69a6a8b66e03ba0dfc3cb51e81bc41c9930a20e9d77b345a50000000000ffffffff8a1801565ff4a51dc9a92c0e22806c030abd939e41da1621281f120d35b37d150100000000ffffffff862410231e57507a0f95172b692a67ff6511de6935d9d393b9bc497c024402380000000000ffffffffa84a08ca52a3f36beb7f4d1c20cfe84ac614ec9ffde8da427b6ed71eb81cef410100000000ffffffff02ca0439000000000000001976a914d44f04d7752dc1e08d7e8cb2e6fa6704d512906688acc83ac9050000000000001976a914c85c89c23b9d9b0ff292f2a9fe1f7fa1283f175e88ac00000000000000000483aba8040000000037280000050000006a47304402206149798ce5c7aca3fccdcf8efbf508be35fcb08c2ab64f34a473f990024746ca0220035cb44366c39ac9a1a00ec85b95facb67e30fbe004a66603edfe5850f12dffe012103a079c17a06b92ffc6633e4e8a854c7592e28b7133c939307b5b6af411afa09122f9aa8000000000035280000050000006a473044022020cae3f9e02d4df670ccadc8c320b00e406952c66437b5051dd18bd1f763425b02200770786868b79288bbdadd80a8e7bc7349a18bf801c763e4db3f9d84db5fa8030121039cdd3724f6a41619272147d8763082d23930226178532e65e924fa8689428e102e9f78000000000035280000080000006b483045022100eefe2044cd21f4603989047bd1e2dfa391b994c4073ad9a7114b8b76aa52a11f022002ebcf680dd5ad214e2694c63c23ad92acab683599fbbc0599ee8f1b21002ecb01210356a8f9fd3b29089c46d332eb01b4f1ed24330568fc27e420c1a03a8797a2d345123e4f000000000044280000030000006a47304402206178e5024915c515d843b21ea8bbc555fecb1fe8592514c8b66d1d9ed98000cf022014c589a84855e226a3ac336feecdd7c4dd535dfadde3783dab57bca51e7babdc012103e36fdfcec5b468d16c5785bd0395b44bc838757c47079b508d428927ffd8cb3b","76a914ced4a710e223b7f77778b01936e125d0d6f1935a88ac",2,83,"ffb1545c3f79a39bb156d46b6fbdd3fdb4ba3c4ca6ee35f99c77b5babfbe3ced","OK", "4 inputs, 2 outputs, sign input 2"], +["0100000002c9f89bfd405f3329ca596f75eeb5ee71ac598ebb5444ad496f9adabbe5eea47c0000000000ffffffff37aa9cb40192aa51aa630df84450d4abf7a7be478436d5b17b1f1f4437dc06e00100000000ffffffff028c20a60f0000000000001976a9141649a39a79a3bb6671b420cb4156158e08fe440788acd465ec2e0000000000001976a9144e5b631d8c589f98b348a9c60c4dcf023e75140188ac0000000000000000024879dd2d00000000f21f0000010000006a473044022005f54f4ba3a64933b531f37526d03caa227365f380f8f680717e8e260264d9dd02206ee9b64246507fc6ad5caf8e5bdc3cd23b0a6d9bdbd6ed280bc0be3d6990c8850121023d8930b24ccadfb99aa23979118830497577428a15bf27323492dc290e9af6295858011100000000a3270000020000006a473044022009411e83b4e80194fdaf31edc663e487f792a5116b4c39a82fa57d2c6297030002205ca59eeaaedcc95c3234b31e4501b22cc5462fdf003e21d9efd344a90554a5c601210373d50f78140b775916d7ad7a1546f2a9dd43e429db34c4d7b4ad54f5fd75301d","76a914a800382590fa084c4ce104cd49dfc46c1276129e88ac",0,82,"d1919a80ce91d794532b48f4558ae7487ecfa491b0ca0f86bb52137cebab98b5","OK", "2 inputs, 2 outputs, sign input 0"], +["010000000323e2997960d9cf6b0990f36f3d133cda0e4fb48d18ec2b88d6f65748093e179f0200000000ffffffff2a3ee4be03563cd21535b9fea82b549f895134e578673802b21894d1022b1f8f0200000000ffffffff1609ba449a9782d8bc2ecb722283e92fbf1f79e0b87ae8a2abced9387e9ea2f70200000000ffffffff023dc8c9340100000000001976a914a051738e9d60ac4d05152089379c47ce36ffc2ca88acd4c2db000000000000001976a91441c0347871c650683943111f7c9ed187cafcc03b88ac0000000000000000038fecef6e00000000a4260000000000006b483045022100e8be2f867c1a046cb4f0fc46a836841d2eed78020153f92c1dd1d8f79293802d0220544b08fba23d213447d9ba3bc8652599ab3d4e91d1ae7c93bfd1267a26af3518012103b89bb443b6ab17724458285b302291b082c59e5a022f273af0f61d47a414a5374fa1a36e0000000052270000000000006b483045022100e62e40ff16bf6b0cbea18dfb655847faaa02f3daa13b46fb8e39aea133bd63200220549c1e47c6a9e3490ee6ebc986b7ccc63e138bee9be4731ebb4923902f6c65f8012103b89bb443b6ab17724458285b302291b082c59e5a022f273af0f61d47a414a53773485e5800000000ac260000000000006a47304402202c4df4e91e01497c1e6bce5db7b754a3ac7c2b4f3aa637aa0ff0dc3b1150dd3b02202e3e8101b4f16e75a9d152eece8948eb8f946d5187f08c80389d9228f33a5342012103b89bb443b6ab17724458285b302291b082c59e5a022f273af0f61d47a414a537","76a9145dcbb6176b228e90ab38ebf7e0e226699ff2241c88ac",2,186,"5546f9d5d257c40c65f562e7e6f842e817b3aadb9ace09a4ca5d1abb70fd35ae","OK", "3 inputs, 2 outputs, sign input 2"], + +["Make diffs cleaner by leaving a comment here without comma at the end"] +] diff --git a/pydecred/testnet.py b/pydecred/testnet.py index defa7d6f..57e7cbc7 100644 --- a/pydecred/testnet.py +++ b/pydecred/testnet.py @@ -107,4 +107,6 @@ GENESIS_STAMP = 1533513600 STAKE_SPLIT = 0.3 POW_SPLIT = 0.6 -TREASURY_SPLIT = 0.1 \ No newline at end of file +TREASURY_SPLIT = 0.1 + +BlockOneSubsidy = int(100000 * 1e8) \ No newline at end of file diff --git a/pydecred/tests.py b/pydecred/tests.py new file mode 100644 index 00000000..fc1ffa39 --- /dev/null +++ b/pydecred/tests.py @@ -0,0 +1,1977 @@ +import unittest +import os +import json +import time +from tempfile import TemporaryDirectory +from tinydecred.pydecred.calc import SubsidyCache +from tinydecred.pydecred import mainnet, testnet, txscript, dcrdata, stakepool +from tinydecred.pydecred.wire import wire, msgtx +from tinydecred.crypto.bytearray import ByteArray +from tinydecred.crypto import crypto, opcode + +class TestSubsidyCache(unittest.TestCase): + def test_subsidy_cache_calcs(self): + """ + TestSubsidyCacheCalcs ensures the subsidy cache calculates the various + subsidy proportions and values as expected. + """ + class test: + def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull=None, wantWork=None, wantVote=None, wantTreasury=None): + self.name = name + self.params = params + self.height = height + self.numVotes = numVotes + self.wantFull = wantFull + self.wantWork = wantWork + self.wantVote = wantVote + self.wantTreasury = wantTreasury + tests = [ + test( + name = "negative height", + params = mainnet, + height = -1, + numVotes = 0, + wantFull = 0, + wantWork = 0, + wantVote = 0, + wantTreasury = 0, + ), + test( + name = "height 0", + params = mainnet, + height = 0, + numVotes = 0, + wantFull = 0, + wantWork = 0, + wantVote = 0, + wantTreasury = 0, + ), + test( + name = "height 1 (initial payouts)", + params = mainnet, + height = 1, + numVotes = 0, + wantFull = 168000000000000, + wantWork = 168000000000000, + wantVote = 0, + wantTreasury = 0, + ), + test( + name = "height 2 (first non-special block prior voting start)", + params = mainnet, + height = 2, + numVotes = 0, + wantFull = 3119582664, + wantWork = 1871749598, + wantVote = 0, + wantTreasury = 311958266, + ), + test( + name = "height 4094 (two blocks prior to voting start)", + params = mainnet, + height = 4094, + numVotes = 0, + wantFull = 3119582664, + wantWork = 1871749598, + wantVote = 0, + wantTreasury = 311958266, + ), + test( + name = "height 4095 (final block prior to voting start)", + params = mainnet, + height = 4095, + numVotes = 0, + wantFull = 3119582664, + wantWork = 1871749598, + wantVote = 187174959, + wantTreasury = 311958266, + ), + test( + name = "height 4096 (voting start), 5 votes", + params = mainnet, + height = 4096, + numVotes = 5, + wantFull = 3119582664, + wantWork = 1871749598, + wantVote = 187174959, + wantTreasury = 311958266, + ), + test( + name = "height 4096 (voting start), 4 votes", + params = mainnet, + height = 4096, + numVotes = 4, + wantFull = 3119582664, + wantWork = 1497399678, + wantVote = 187174959, + wantTreasury = 249566612, + ), + test( + name = "height 4096 (voting start), 3 votes", + params = mainnet, + height = 4096, + numVotes = 3, + wantFull = 3119582664, + wantWork = 1123049758, + wantVote = 187174959, + wantTreasury = 187174959, + ), + test( + name = "height 4096 (voting start), 2 votes", + params = mainnet, + height = 4096, + numVotes = 2, + wantFull = 3119582664, + wantWork = 0, + wantVote = 187174959, + wantTreasury = 0, + ), + test( + name = "height 6143 (final block prior to 1st reduction), 5 votes", + params = mainnet, + height = 6143, + numVotes = 5, + wantFull = 3119582664, + wantWork = 1871749598, + wantVote = 187174959, + wantTreasury = 311958266, + ), + test( + name = "height 6144 (1st block in 1st reduction), 5 votes", + params = mainnet, + height = 6144, + numVotes = 5, + wantFull = 3088695706, + wantWork = 1853217423, + wantVote = 185321742, + wantTreasury = 308869570, + ), + test( + name = "height 6144 (1st block in 1st reduction), 4 votes", + params = mainnet, + height = 6144, + numVotes = 4, + wantFull = 3088695706, + wantWork = 1482573938, + wantVote = 185321742, + wantTreasury = 247095656, + ), + test( + name = "height 12287 (last block in 1st reduction), 5 votes", + params = mainnet, + height = 12287, + numVotes = 5, + wantFull = 3088695706, + wantWork = 1853217423, + wantVote = 185321742, + wantTreasury = 308869570, + ), + test( + name = "height 12288 (1st block in 2nd reduction), 5 votes", + params = mainnet, + height = 12288, + numVotes = 5, + wantFull = 3058114560, + wantWork = 1834868736, + wantVote = 183486873, + wantTreasury = 305811456, + ), + test( + name = "height 307200 (1st block in 50th reduction), 5 votes", + params = mainnet, + height = 307200, + numVotes = 5, + wantFull = 1896827356, + wantWork = 1138096413, + wantVote = 113809641, + wantTreasury = 189682735, + ), + test( + name = "height 307200 (1st block in 50th reduction), 3 votes", + params = mainnet, + height = 307200, + numVotes = 3, + wantFull = 1896827356, + wantWork = 682857847, + wantVote = 113809641, + wantTreasury = 113809641, + ), + test( + name = "height 10911744 (first zero vote subsidy 1776th reduction), 5 votes", + params = mainnet, + height = 10911744, + numVotes = 5, + wantFull = 16, + wantWork = 9, + wantVote = 0, + wantTreasury = 1, + ), + test( + name = "height 10954752 (first zero treasury subsidy 1783rd reduction), 5 votes", + params = mainnet, + height = 10954752, + numVotes = 5, + wantFull = 9, + wantWork = 5, + wantVote = 0, + wantTreasury = 0, + ), + test( + name = "height 11003904 (first zero work subsidy 1791st reduction), 5 votes", + params = mainnet, + height = 11003904, + numVotes = 5, + wantFull = 1, + wantWork = 0, + wantVote = 0, + wantTreasury = 0, + ), + test( + name = "height 11010048 (first zero full subsidy 1792nd reduction), 5 votes", + params = mainnet, + height = 11010048, + numVotes = 5, + wantFull = 0, + wantWork = 0, + wantVote = 0, + wantTreasury = 0, + ) + ] + + for t in tests: + #Ensure the full subsidy is the expected value. + cache = SubsidyCache(t.params) + fullSubsidyResult = cache.calcBlockSubsidy(t.height) + self.assertEqual(fullSubsidyResult, t.wantFull, t.name) + + # Ensure the PoW subsidy is the expected value. + workResult = cache.calcWorkSubsidy(t.height, t.numVotes) + self.assertEqual(workResult, t.wantWork, t.name) + + # Ensure the vote subsidy is the expected value. + voteResult = cache.calcStakeVoteSubsidy(t.height) + self.assertEqual(voteResult, t.wantVote, t.name) + + # Ensure the treasury subsidy is the expected value. + treasuryResult = cache.calcTreasurySubsidy(t.height, t.numVotes) + self.assertEqual(treasuryResult, t.wantTreasury, t.name) + + def test_total_subsidy(self): + """ + TestTotalSubsidy ensures the total subsidy produced matches the expected + value. + """ + # Locals for convenience. + reductionInterval = mainnet.SubsidyReductionInterval + stakeValidationHeight = mainnet.StakeValidationHeight + votesPerBlock = mainnet.TicketsPerBlock + + # subsidySum returns the sum of the individual subsidy types for the given + # height. Note that this value is not exactly the same as the full subsidy + # originally used to calculate the individual proportions due to the use + # of integer math. + cache = SubsidyCache(mainnet) + def subsidySum(height): + work = cache.calcWorkSubsidy(height, votesPerBlock) + vote = cache.calcStakeVoteSubsidy(height) * votesPerBlock + treasury = cache.calcTreasurySubsidy(height, votesPerBlock) + return work + vote + treasury + + # Calculate the total possible subsidy. + totalSubsidy = mainnet.BlockOneSubsidy + reductionNum = -1 + while True: + reductionNum += 1 + # The first interval contains a few special cases: + # 1) Block 0 does not produce any subsidy + # 2) Block 1 consists of a special initial coin distribution + # 3) Votes do not produce subsidy until voting begins + if reductionNum == 0: + # Account for the block up to the point voting begins ignoring the + # first two special blocks. + subsidyCalcHeight = 2 + nonVotingBlocks = stakeValidationHeight - subsidyCalcHeight + totalSubsidy += subsidySum(subsidyCalcHeight) * nonVotingBlocks + + # Account for the blocks remaining in the interval once voting + # begins. + subsidyCalcHeight = stakeValidationHeight + votingBlocks = reductionInterval - subsidyCalcHeight + totalSubsidy += subsidySum(subsidyCalcHeight) * votingBlocks + continue + + # Account for the all other reduction intervals until all subsidy has + # been produced. + subsidyCalcHeight = reductionNum * reductionInterval + subSum = subsidySum(subsidyCalcHeight) + if subSum == 0: + break + totalSubsidy += subSum * reductionInterval + + # Ensure the total calculated subsidy is the expected value. + self.assertEqual(totalSubsidy, 2099999999800912) + + # TestCalcBlockSubsidySparseCaching ensures the cache calculations work + # properly when accessed sparsely and out of order. + def test_calc_block_subsidy_sparse_caching(self): + # Mock params used in tests. + # perCacheTest describes a test to run against the same cache. + class perCacheTest: + def __init__(self, name, height, want): + self.name = name + self.height = height + self.want = want + class test: + def __init__(self, name, params, perCacheTests): + self.name = name + self.params = params + self.perCacheTests = perCacheTests + tests = [ + test( + name = "negative/zero/one (special cases, no cache)", + params = mainnet, + perCacheTests = [ + perCacheTest( + name = "would be negative interval", + height = -6144, + want = 0, + ), + perCacheTest( + name = "negative one", + height = -1, + want = 0, + ), + perCacheTest( + name = "height 0", + height = 0, + want = 0, + ), + perCacheTest( + name = "height 1", + height = 1, + want = 168000000000000, + ), + ], + ), + test( + name = "clean cache, negative height", + params = mainnet, + perCacheTests = [ + perCacheTest( + name = "would be negative interval", + height = -6144, + want = 0, + ), + perCacheTest( + name = "height 0", + height = 0, + want = 0, + ), + ], + ), + test( + name = "clean cache, max int64 height twice", + params = mainnet, + perCacheTests = [ + perCacheTest( + name = "max int64", + height = 9223372036854775807, + want = 0, + ), + perCacheTest( + name = "second max int64", + height = 9223372036854775807, + want = 0, + ), + ], + ), + test( + name = "sparse out order interval requests with cache hits", + params = mainnet, + perCacheTests = [ + perCacheTest( + name = "height 0", + height = 0, + want = 0, + ), + perCacheTest( + name = "height 1", + height = 1, + want = 168000000000000, + ), + perCacheTest( + name = "height 2 (cause interval 0 cache addition)", + height = 2, + want = 3119582664, + ), + perCacheTest( + name = "height 2 (interval 0 cache hit)", + height = 2, + want = 3119582664, + ), + perCacheTest( + name = "height 3 (interval 0 cache hit)", + height = 2, + want = 3119582664, + ), + perCacheTest( + name = "height 6145 (interval 1 cache addition)", + height = 6145, + want = 3088695706, + ), + perCacheTest( + name = "height 6145 (interval 1 cache hit)", + height = 6145, + want = 3088695706, + ), + perCacheTest( + name = "interval 20 cache addition most recent cache interval 1", + height = 6144 * 20, + want = 2556636713, + ), + perCacheTest( + name = "interval 20 cache hit", + height = 6144 * 20, + want = 2556636713, + ), + perCacheTest( + name = "interval 10 cache addition most recent cache interval 20", + height = 6144 * 10, + want = 2824117486, + ), + perCacheTest( + name = "interval 10 cache hit", + height = 6144 * 10, + want = 2824117486, + ), + perCacheTest( + name = "interval 15 cache addition between cached 10 and 20", + height = 6144 * 15, + want = 2687050883, + ), + perCacheTest( + name = "interval 15 cache hit", + height = 6144 * 15, + want = 2687050883, + ), + perCacheTest( + name = "interval 1792 (first with 0 subsidy) cache addition", + height = 6144 * 1792, + want = 0, + ), + perCacheTest( + name = "interval 1792 cache hit", + height = 6144 * 1792, + want = 0, + ), + perCacheTest( + name = "interval 1795 (skipping final 0 subsidy)", + height = 6144 * 1795, + want = 0, + ), + ], + ), + test( + name = "clean cache, reverse interval requests", + params = mainnet, + perCacheTests = [ + perCacheTest( + name = "interval 5 cache addition", + height = 6144 * 5, + want = 2968175862, + ), + perCacheTest( + name = "interval 3 cache addition", + height = 6144 * 3, + want = 3027836198, + ), + perCacheTest( + name = "interval 3 cache hit", + height = 6144 * 3, + want = 3027836198, + ), + ], + ), + test( + name = "clean cache, forward non-zero start interval requests", + params = mainnet, + perCacheTests = [ + perCacheTest( + name = "interval 2 cache addition", + height = 6144 * 2, + want = 3058114560, + ), + perCacheTest( + name = "interval 12 cache addition", + height = 6144 * 12, + want = 2768471213, + ), + perCacheTest( + name = "interval 12 cache hit", + height = 6144 * 12, + want = 2768471213, + ), + ], + ) + ] + + for t in tests: + cache = SubsidyCache(t.params) + for pcTest in t.perCacheTests: + result = cache.calcBlockSubsidy(pcTest.height) + self.assertEqual(result, pcTest.want, t.name) + + +def parseShortForm(asm): + b = ByteArray(b'') + for token in asm.split(): + if token.startswith("0x"): + b += ByteArray(token[2:]) + else: + longToken = "OP_"+token + if hasattr(opcode, longToken): + b += ByteArray(getattr(opcode, longToken)) + else: + raise Exception("unknown token %s" % token) + return b + +class scriptClassTest: + def __init__(self, name=None, script=None, scriptClass=None, subClass=None): + self.name = name + self.script = script + self.scriptClass = scriptClass + self.subClass = subClass + +def scriptClassTests(): + return [ + scriptClassTest( + name = "Pay Pubkey", + script = "DATA_65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e" + + "97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e16" + + "0bfa9b8b64f9d4c03f999b8643f656b412a3 CHECKSIG", + scriptClass = txscript.PubKeyTy, + ), + # tx 599e47a8114fe098103663029548811d2651991b62397e057f0c863c2bc9f9ea + scriptClassTest( + name = "Pay PubkeyHash", + script = "DUP HASH160 DATA_20 0x660d4ef3a743e3e696ad990364e555" + + "c271ad504b EQUALVERIFY CHECKSIG", + scriptClass = txscript.PubKeyHashTy, + ), + # part of tx 6d36bc17e947ce00bb6f12f8e7a56a1585c5a36188ffa2b05e10b4743273a74b + # codeseparator parts have been elided. (bitcoin core's checks for + # multisig type doesn't have codesep either). + scriptClassTest( + name = "multisig", + script = "1 DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da4" + + "5329a00357b3a7886211ab414d55a 1 CHECKMULTISIG", + scriptClass = txscript.MultiSigTy, + ), + # tx e5779b9e78f9650debc2893fd9636d827b26b4ddfa6a8172fe8708c924f5c39d + scriptClassTest( + name = "P2SH", + script = "HASH160 DATA_20 0x433ec2ac1ffa1b7b7d027f564529c57197f" + + "9ae88 EQUAL", + scriptClass = txscript.ScriptHashTy, + ), + scriptClassTest( + name = "Stake Submission P2SH", + script = "SSTX HASH160 DATA_20 0x433ec2ac1ffa1b7b7d027f564529" + + "c57197f9ae88 EQUAL", + scriptClass = txscript.StakeSubmissionTy, + subClass = txscript.ScriptHashTy, + ), + scriptClassTest( + name = "Stake Submission Generation P2SH", + script = "SSGEN HASH160 DATA_20 0x433ec2ac1ffa1b7b7d027f564529" + + "c57197f9ae88 EQUAL", + scriptClass = txscript.StakeGenTy, + subClass = txscript.ScriptHashTy, + ), + scriptClassTest( + name = "Stake Submission Revocation P2SH", + script = "SSRTX HASH160 DATA_20 0x433ec2ac1ffa1b7b7d027f564529" + + "c57197f9ae88 EQUAL", + scriptClass = txscript.StakeRevocationTy, + subClass = txscript.ScriptHashTy, + ), + scriptClassTest( + name = "Stake Submission Change P2SH", + script = "SSTXCHANGE HASH160 DATA_20 0x433ec2ac1ffa1b7b7d027f5" + + "64529c57197f9ae88 EQUAL", + scriptClass = txscript.StakeSubChangeTy, + subClass = txscript.ScriptHashTy, + ), + + scriptClassTest( + # Nulldata with no data at all. + name = "nulldata no data", + script = "RETURN", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Nulldata with single zero push. + name = "nulldata zero", + script = "RETURN 0", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Nulldata with small integer push. + name = "nulldata small int", + script = "RETURN 1", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Nulldata with max small integer push. + name = "nulldata max small int", + script = "RETURN 16", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Nulldata with small data push. + name = "nulldata small data", + script = "RETURN DATA_8 0x046708afdb0fe554", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Canonical nulldata with 60-byte data push. + name = "canonical nulldata 60-byte push", + script = "RETURN 0x3c 0x046708afdb0fe5548271967f1a67130b7105cd" + + "6a828e03909a67962e0ea1f61deb649f6bc3f4cef3046708afdb" + + "0fe5548271967f1a67130b7105cd6a", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Non-canonical nulldata with 60-byte data push. + name = "non-canonical nulldata 60-byte push", + script = "RETURN PUSHDATA1 0x3c 0x046708afdb0fe5548271967f1a67" + + "130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" + + "046708afdb0fe5548271967f1a67130b7105cd6a", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Nulldata with max allowed data to be considered standard. + name = "nulldata max standard push", + script = "RETURN PUSHDATA1 0x50 0x046708afdb0fe5548271967f1a67" + + "130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" + + "046708afdb0fe5548271967f1a67130b7105cd6a828e03909a67" + + "962e0ea1f61deb649f6bc3f4cef3", + scriptClass = txscript.NullDataTy, + ), + scriptClassTest( + # Nulldata with more than max allowed data to be considered + # standard (so therefore nonstandard) + name = "nulldata exceed max standard push", + script = "RETURN PUSHDATA2 0x1801 0x046708afdb0fe5548271967f1a670" + + "46708afdb0fe5548271967f1a67046708afdb0fe5548271967f1a670467" + + "08afdb0fe5548271967f1a67046708afdb0fe5548271967f1a67046708a" + + "fdb0fe5548271967f1a67046708afdb0fe5548271967f1a67046708afdb" + + "0fe5548271967f1a67046708afdb0fe5548271967f1a67046708afdb0fe" + + "5548271967f1a67", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + # Almost nulldata, but add an additional opcode after the data + # to make it nonstandard. + name = "almost nulldata", + script = "RETURN 4 TRUE", + scriptClass = txscript.NonStandardTy, + ), + + # The next few are almost multisig (it is the more complex script type) + # but with various changes to make it fail. + scriptClassTest( + # Multisig but invalid nsigs. + name = "strange 1", + script = "DUP DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da45" + + "329a00357b3a7886211ab414d55a 1 CHECKMULTISIG", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + # Multisig but invalid pubkey. + name = "strange 2", + script = "1 1 1 CHECKMULTISIG", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + # Multisig but no matching npubkeys opcode. + name = "strange 3", + script = "1 DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da4532" + + "9a00357b3a7886211ab414d55a DATA_33 0x0232abdc893e7f0" + + "631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a " + + "CHECKMULTISIG", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + # Multisig but with multisigverify. + name = "strange 4", + script = "1 DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da4532" + + "9a00357b3a7886211ab414d55a 1 CHECKMULTISIGVERIFY", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + # Multisig but wrong length. + name = "strange 5", + script = "1 CHECKMULTISIG", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + name = "doesn't parse", + script = "DATA_5 0x01020304", + scriptClass = txscript.NonStandardTy, + ), + scriptClassTest( + name = "multisig script with wrong number of pubkeys", + script = "2 " + + "DATA_33 " + + "0x027adf5df7c965a2d46203c781bd4dd8" + + "21f11844136f6673af7cc5a4a05cd29380 " + + "DATA_33 " + + "0x02c08f3de8ee2de9be7bd770f4c10eb0" + + "d6ff1dd81ee96eedd3a9d4aeaf86695e80 " + + "3 CHECKMULTISIG", + scriptClass = txscript.NonStandardTy, + ), + ] + +class TestTxScript(unittest.TestCase): + def test_stake_pool_ticketFee(self): + class test: + def __init__(self, StakeDiff, Fee, Height, PoolFee, Expected): + self.StakeDiff = int(StakeDiff) + self.Fee = int(Fee) + self.Height = int(Height) + self.PoolFee = PoolFee + self.Expected = int(Expected) + tests = [ + test(10 * 1e8, 0.01 * 1e8, 25000, 1.00, 0.01500463 * 1e8), + test(20 * 1e8, 0.01 * 1e8, 25000, 1.00, 0.01621221 * 1e8), + test(5 * 1e8, 0.05 * 1e8, 50000, 2.59, 0.03310616 * 1e8), + test(15 * 1e8, 0.05 * 1e8, 50000, 2.59, 0.03956376 * 1e8), + ] + cache = SubsidyCache(mainnet) + for i, t in enumerate(tests): + poolFeeAmt = txscript.stakePoolTicketFee(t.StakeDiff, t.Fee, t.Height, t.PoolFee, cache, mainnet) + self.assertEqual(poolFeeAmt, t.Expected, str(i)) + def test_generate_sstx_addr_push(self): + """ + TestGenerateSStxAddrPush ensures an expected OP_RETURN push is generated. + """ + class test: + def __init__(self, addrStr, net, amount, limits, expected): + self.addrStr = addrStr + self.net = net + self.amount = amount + self.limits = limits + self.expected = expected + tests = [] + tests.append(test( + "Dcur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx", + mainnet, + 1000, + 10, + ByteArray("6a1ef5916158e3e2c4551c1796708db8367207ed13bbe8030000000000800a00"), + )) + tests.append(test( + "TscB7V5RuR1oXpA364DFEsNDuAs8Rk6BHJE", + testnet, + 543543, + 256, + ByteArray("6a1e7a5c4cca76f2e0b36db4763daacbd6cbb6ee6e7b374b0800000000000001"), + )) + for t in tests: + addr = txscript.decodeAddress(t.addrStr, t.net) + s = txscript.generateSStxAddrPush(addr, t.amount, t.limits) + self.assertEqual(s, t.expected) + def test_var_int_serialize(self): + """ + TestVarIntSerializeSize ensures the serialize size for variable length + integers works as intended. + """ + tests = [ + (0, 1), # Single byte encoded + (0xfc, 1), # Max single byte encoded + (0xfd, 3), # Min 3-byte encoded + (0xffff, 3), # Max 3-byte encoded + (0x10000, 5), # Min 5-byte encoded + (0xffffffff, 5), # Max 5-byte encoded + (0x100000000, 9), # Min 9-byte encoded + (0xffffffffffffffff, 9), # Max 9-byte encoded + ] + + for i, (val, size) in enumerate(tests): + self.assertEqual(txscript.varIntSerializeSize(val), size, msg="test at index %d" % i) + def test_calc_signature_hash(self): + """ TestCalcSignatureHash does some rudimentary testing of msg hash calculation. """ + tx = msgtx.MsgTx.new() + for i in range(3): + txIn = msgtx.TxIn(msgtx.OutPoint( + txHash = crypto.hashH(ByteArray(i, length=1).bytes()), + idx = i, + tree = 0, + ), 0) + txIn.sequence = 0xFFFFFFFF + + tx.addTxIn(txIn) + for i in range(2): + txOut = msgtx.TxOut() + txOut.pkScript = ByteArray("51", length=1) + txOut.value = 0x0000FF00FF00FF00 + tx.addTxOut(txOut) + + want = ByteArray("4ce2cd042d64e35b36fdbd16aff0d38a5abebff0e5e8f6b6b31fcd4ac6957905") + script = ByteArray("51", length=1) + + msg1 = txscript.calcSignatureHash(script, txscript.SigHashAll, tx, 0, None) + + prefixHash = tx.hash() + msg2 = txscript.calcSignatureHash(script, txscript.SigHashAll, tx, 0, prefixHash) + + self.assertEqual(msg1, want) + + self.assertEqual(msg2, want) + + self.assertEqual(msg1, msg2) + + # Move the index and make sure that we get a whole new hash, despite + # using the same TxOuts. + msg3 = txscript.calcSignatureHash(script, txscript.SigHashAll, tx, 1, prefixHash) + + self.assertNotEqual(msg1, msg3) + def test_script_tokenizer(self): + """ + TestScriptTokenizer ensures a wide variety of behavior provided by the script + tokenizer performs as expected. + """ + + # Add both positive and negative tests for OP_DATA_1 through OP_DATA_75. + tests = [] + for op in range(opcode.OP_DATA_1, opcode.OP_DATA_75): + data = ByteArray([1]*op) + tests.append(( + "OP_DATA_%d" % op, + ByteArray(op, length=1) + data, + ((op, data, 1 + op), ), + 1 + op, + None, + )) + + # Create test that provides one less byte than the data push requires. + tests.append(( + "short OP_DATA_%d" % op, + ByteArray(op) + data[1:], + None, + 0, + Exception, + )) + + # Add both positive and negative tests for OP_PUSHDATA{1,2,4}. + data = ByteArray([1]*76) + tests.extend([( + "OP_PUSHDATA1", + ByteArray(opcode.OP_PUSHDATA1) + ByteArray(0x4c) + ByteArray([0x01]*76), + ((opcode.OP_PUSHDATA1, data, 2 + len(data)),), + 2 + len(data), + None, + ), ( + "OP_PUSHDATA1 no data length", + ByteArray(opcode.OP_PUSHDATA1), + None, + 0, + Exception, + ), ( + "OP_PUSHDATA1 short data by 1 byte", + ByteArray(opcode.OP_PUSHDATA1) + ByteArray(0x4c) + ByteArray([0x01]*75), + None, + 0, + Exception, + ), ( + "OP_PUSHDATA2", + ByteArray(opcode.OP_PUSHDATA2) + ByteArray(0x4c00) + ByteArray([0x01]*76), + ((opcode.OP_PUSHDATA2, data, 3 + len(data)),), + 3 + len(data), + None, + ), ( + "OP_PUSHDATA2 no data length", + ByteArray(opcode.OP_PUSHDATA2), + None, + 0, + Exception, + ), ( + "OP_PUSHDATA2 short data by 1 byte", + ByteArray(opcode.OP_PUSHDATA2) + ByteArray(0x4c00) + ByteArray([0x01]*75), + None, + 0, + Exception, + ), ( + "OP_PUSHDATA4", + ByteArray(opcode.OP_PUSHDATA4) + ByteArray(0x4c000000) + ByteArray([0x01]*76), + ((opcode.OP_PUSHDATA4, data, 5 + len(data)),), + 5 + len(data), + None, + ), ( + "OP_PUSHDATA4 no data length", + ByteArray(opcode.OP_PUSHDATA4), + None, + 0, + Exception, + ), ( + "OP_PUSHDATA4 short data by 1 byte", + ByteArray(opcode.OP_PUSHDATA4) + ByteArray(0x4c000000) + ByteArray([0x01]*75), + None, + 0, + Exception, + )]) + + # Add tests for OP_0, and OP_1 through OP_16 (small integers/true/false). + opcodes = ByteArray(opcode.OP_0) + nilBytes = ByteArray('') + for op in range(opcode.OP_1, opcode.OP_16): + opcodes += op + for op in opcodes: + tests.append(( + "OP_%d" % op, + ByteArray(op), + ((op, nilBytes, 1),), + 1, + None, + )) + + # Add various positive and negative tests for multi-opcode scripts. + tests.extend([( + "pay-to-pubkey-hash", + ByteArray(opcode.OP_DUP) + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*20) + ByteArray(opcode.OP_EQUAL) + ByteArray(opcode.OP_CHECKSIG), + ( + (opcode.OP_DUP, nilBytes, 1), (opcode.OP_HASH160, nilBytes, 2), + (opcode.OP_DATA_20, ByteArray([0x01]*20), 23), + (opcode.OP_EQUAL, nilBytes, 24), (opcode.OP_CHECKSIG, nilBytes, 25), + ), + 25, + None, + ), ( + "almost pay-to-pubkey-hash (short data)", + ByteArray(opcode.OP_DUP) + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*17) + ByteArray(opcode.OP_EQUAL) + ByteArray(opcode.OP_CHECKSIG), + ( + (opcode.OP_DUP, nilBytes, 1), (opcode.OP_HASH160, nilBytes, 2), + ), + 2, + Exception, + ), ( + "almost pay-to-pubkey-hash (overlapped data)", + ByteArray(opcode.OP_DUP) + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL) + ByteArray(opcode.OP_CHECKSIG), + ( + (opcode.OP_DUP, nilBytes, 1), (opcode.OP_HASH160, nilBytes, 2), + (opcode.OP_DATA_20, ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL), 23), + (opcode.OP_CHECKSIG, nilBytes, 24), + ), + 24, + None, + ), ( + "pay-to-script-hash", + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*20) + ByteArray(opcode.OP_EQUAL), + ( + (opcode.OP_HASH160, nilBytes, 1), + (opcode.OP_DATA_20, ByteArray([0x01]*20), 22), + (opcode.OP_EQUAL, nilBytes, 23), + ), + 23, + None, + ), ( + "almost pay-to-script-hash (short data)", + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*18) + ByteArray(opcode.OP_EQUAL), + ( + (opcode.OP_HASH160, nilBytes, 1), + ), + 1, + Exception, + ), ( + "almost pay-to-script-hash (overlapped data)", + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL), + ( + (opcode.OP_HASH160, nilBytes, 1), + (opcode.OP_DATA_20, ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL), 22), + ), + 22, + None, + )]) + + scriptVersion = 0 + for test_name, test_script, test_expected, test_finalIdx, test_err in tests: + tokenizer = txscript.ScriptTokenizer(scriptVersion, test_script) + opcodeNum = 0 + while tokenizer.next(): + # Ensure Next never returns true when there is an error set. + self.assertIs(tokenizer.err, None, msg="%s: Next returned true when tokenizer has err: %r" % (test_name, tokenizer.err)) + + # Ensure the test data expects a token to be parsed. + op = tokenizer.opcode() + data = tokenizer.data() + self.assertFalse(opcodeNum >= len(test_expected), msg="%s: unexpected token '%r' (data: '%s')" % (test_name, op, data)) + expected_op, expected_data, expected_index = test_expected[opcodeNum] + + # Ensure the opcode and data are the expected values. + self.assertEqual(op, expected_op, msg="%s: unexpected opcode -- got %d, want %d" % (test_name, op, expected_op)) + self.assertEqual(data, expected_data, msg="%s: unexpected data -- got %s, want %s" % (test_name, data, expected_data)) + + tokenizerIdx = tokenizer.offset + self.assertEqual(tokenizerIdx, expected_index, msg="%s: unexpected byte index -- got %d, want %d" % (test_name, tokenizerIdx, expected_index)) + + opcodeNum += 1 + + # Ensure the tokenizer claims it is done. This should be the case + # regardless of whether or not there was a parse error. + self.assertTrue(tokenizer.done(), msg="%s: tokenizer claims it is not done" % test_name) + + # Ensure the error is as expected. + if test_err is None: + self.assertIs(tokenizer.err, None, msg="%s: unexpected tokenizer err -- got %r, want None" % (test_name, tokenizer.err)) + else: + self.assertTrue(isinstance(tokenizer.err, test_err), msg="%s: unexpected tokenizer err -- got %r, want %r" % (test_name, tokenizer.err, test_err)) + + # Ensure the final index is the expected value. + tokenizerIdx = tokenizer.offset + self.assertEqual(tokenizerIdx, test_finalIdx, msg="%s: unexpected final byte index -- got %d, want %d" % (test_name, tokenizerIdx, test_finalIdx)) + def test_sign_tx(self): + """ + Based on dcrd TestSignTxOutput. + """ + # make key + # make script based on key. + # sign with magic pixie dust. + hashTypes = ( + txscript.SigHashAll, + # SigHashNone, + # SigHashSingle, + # SigHashAll | SigHashAnyOneCanPay, + # SigHashNone | SigHashAnyOneCanPay, + # SigHashSingle | SigHashAnyOneCanPay, + ) + signatureSuites = ( + crypto.STEcdsaSecp256k1, + # crypto.STEd25519, + # crypto.STSchnorrSecp256k1, + ) + + testValueIn = 12345 + tx = msgtx.MsgTx( + serType = wire.TxSerializeFull, + version = 1, + txIn = [ + msgtx.TxIn( + previousOutPoint = msgtx.OutPoint( + txHash = ByteArray(b''), + idx = 0, + tree = 0, + ), + sequence = 4294967295, + valueIn = testValueIn, + blockHeight = 78901, + blockIndex = 23456, + ), + msgtx.TxIn( + previousOutPoint = msgtx.OutPoint( + txHash = ByteArray(b''), + idx = 1, + tree = 0, + ), + sequence = 4294967295, + valueIn = testValueIn, + blockHeight = 78901, + blockIndex = 23456, + ), + msgtx.TxIn( + previousOutPoint = msgtx.OutPoint( + txHash = ByteArray(b''), + idx = 2, + tree = 0, + ), + sequence = 4294967295, + valueIn = testValueIn, + blockHeight = 78901, + blockIndex = 23456, + ), + ], + txOut = [ + msgtx.TxOut( + version = wire.DefaultPkScriptVersion, + value = 1, + ), + msgtx.TxOut( + version = wire.DefaultPkScriptVersion, + value = 2, + ), + msgtx.TxOut( + version = wire.DefaultPkScriptVersion, + value = 3, + ), + ], + lockTime = 0, + expiry = 0, + cachedHash = None, + ) + + # Since the script engine is not implmented, hard code the keys and + # check that the script signature is the same as produced by dcrd. + + # For compressed keys + tests = ( + ("b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", "47304402203220ddaee5e825376d3ae5a0e20c463a45808e066abc3c8c33a133446a4c9eb002200f2b0b534d5294d9ce5974975ab5af11696535c4c76cadaed1fa327d6d210e19012102e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1"), + ("a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", "473044022020eb42f1965c31987a4982bd8f654d86c1451418dd3ccc0a342faa98a384186b022021cd0dcd767e607df159dd25674469e1d172e66631593bf96023519d5c07c43101210224397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b"), + ("8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", "483045022100d71babc95de02df7be1e7b14c0f68fb5dcab500c8ef7cf8172b2ea8ad627533302202968ddc3b2f9ff07d3a736b04e74fa39663f028035b6d175de6a4ef90838b797012103255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d"), + ) + + # For uncompressed keys + # tests = ( + # ("b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", "483045022100e1bab52fe0b460c71e4a4226ada35ebbbff9959835fa26c70e2571ef2634a05b02200683f9bf8233ba89c5f9658041cc8edc56feef74cad238f060c3b04e0c4f1cb1014104e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1c4d3fa86c79cfe4f9d13f1c31fd75de316cdfe913b03c07252b1f02f7ee15c9c"), + # ("a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", "473044022029cf920fe059ca4d7e5d74060ed234ebcc7bca520dfed7238dc1e32a48d182a9022043141a443740815baf0caffc19ff7b948d41424832b4a9c6273be5beb15ed7ce01410424397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b422f7f8591e7a4030eddb635e753523bce3c6025fc4e97987adb385b08984e94"), + # ("8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", "473044022015f417f05573c3201f96f5ae706c0789539e638a4a57915dc077b8134c83f1ff022001afa12cebd5daa04d7a9d261d78d0fb910294d78c269fe0b2aabc2423282fe5014104255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d031fee342d455077982fe105e82added63ad667f0b616f3c2c17e1cc9205f3d1"), + # ) + + # Pay to Pubkey Hash (uncompressed) + # secp256k1 := chainec.Secp256k1 + from tinydecred.pydecred import mainnet + testingParams = mainnet + for hashType in hashTypes: + for suite in signatureSuites: + for idx in range(len(tx.txIn)): + # var keyDB, pkBytes []byte + # var key chainec.PrivateKey + # var pk chainec.PublicKey + kStr, sigStr = tests[idx] + + if suite == crypto.STEcdsaSecp256k1: + # k = Curve.generateKey(rand.Reader) + k = ByteArray(kStr) + privKey = crypto.privKeyFromBytes(k) + pkBytes = privKey.pub.serializeCompressed() + else: + raise Exception("test for signature suite %d not implemented" % suite) + + address = crypto.newAddressPubKeyHash(crypto.hash160(pkBytes.bytes()), testingParams, suite) + + pkScript = txscript.makePayToAddrScript(address.string(), testingParams) + + # chainParams, tx, idx, pkScript, hashType, kdb, sdb, previousScript, sigType + sigScript = txscript.signTxOutput(privKey, testingParams, tx, idx, pkScript, hashType, None, suite) + + self.assertEqual(sigScript, ByteArray(sigStr), msg="%d:%d:%d" % (hashType, idx, suite)) + return + def test_addresses(self): + from tinydecred.pydecred import mainnet, testnet + from base58 import b58decode + class test: + def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAddress=None, f=None, net=None): + self.name = name + self.addr = addr + self.saddr = saddr + self.encoded = encoded + self.valid = valid + self.scriptAddress = scriptAddress + self.f = f + self.net = net + + addrPKH = crypto.newAddressPubKeyHash + addrSH = crypto.newAddressScriptHash + addrSHH = crypto.newAddressScriptHashFromHash + addrPK = crypto.AddressSecpPubKey + + tests = [] + # Positive P2PKH tests. + tests.append(test( + name = "mainnet p2pkh", + addr = "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", + encoded = "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", + valid = True, + scriptAddress = ByteArray("2789d58cfa0957d206f025c2af056fc8a77cebb0"), + f = lambda: addrPKH( + ByteArray("2789d58cfa0957d206f025c2af056fc8a77cebb0"), + mainnet, + crypto.STEcdsaSecp256k1, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pkh 2", + addr = "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9", + encoded = "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9", + valid = True, + scriptAddress = ByteArray("229ebac30efd6a69eec9c1a48e048b7c975c25f2"), + f = lambda: addrPKH( + ByteArray("229ebac30efd6a69eec9c1a48e048b7c975c25f2"), + mainnet, + crypto.STEcdsaSecp256k1, + ), + net = mainnet, + )) + tests.append(test( + name = "testnet p2pkh", + addr = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h", + encoded = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h", + valid = True, + scriptAddress = ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), + f = lambda: addrPKH( + ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), + testnet, + crypto.STEcdsaSecp256k1 + ), + net = testnet, + )) + + # Negative P2PKH tests. + tests.append(test( + name = "p2pkh wrong hash length", + addr = "", + valid = False, + f = lambda: addrPKH( + ByteArray("000ef030107fd26e0b6bf40512bca2ceb1dd80adaa"), + mainnet, + crypto.STEcdsaSecp256k1, + ), + )) + tests.append(test( + name = "p2pkh bad checksum", + addr = "TsmWaPM77WSyA3aiQ2Q1KnwGDVWvEkhip23", + valid = False, + net = testnet, + )) + + # Positive P2SH tests. + tests.append(test( + # Taken from transactions: + # output: 3c9018e8d5615c306d72397f8f5eef44308c98fb576a88e030c25456b4f3a7ac + # input: 837dea37ddc8b1e3ce646f1a656e79bbd8cc7f558ac56a169626d649ebe2a3ba. + name = "mainnet p2sh", + addr = "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", + encoded = "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", + valid = True, + scriptAddress = ByteArray("f0b4e85100aee1a996f22915eb3c3f764d53779a"), + f = lambda: addrSH( + ByteArray("512103aa43f0a6c15730d886cc1f0342046d20175483d90d7ccb657f90c489111d794c51ae"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + # Taken from transactions: + # output: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d + # input: (not yet redeemed at time test was written) + name = "mainnet p2sh 2", + addr = "DcqgK4N4Ccucu2Sq4VDAdu4wH4LASLhzLVp", + encoded = "DcqgK4N4Ccucu2Sq4VDAdu4wH4LASLhzLVp", + valid = True, + scriptAddress = ByteArray("c7da5095683436f4435fc4e7163dcafda1a2d007"), + f = lambda: addrSHH( + ByteArray("c7da5095683436f4435fc4e7163dcafda1a2d007"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + # Taken from bitcoind base58_keys_valid. + name = "testnet p2sh", + addr = "TccWLgcquqvwrfBocq5mcK5kBiyw8MvyvCi", + encoded = "TccWLgcquqvwrfBocq5mcK5kBiyw8MvyvCi", + valid = True, + scriptAddress = ByteArray("36c1ca10a8a6a4b5d4204ac970853979903aa284"), + f = lambda: addrSHH( + ByteArray("36c1ca10a8a6a4b5d4204ac970853979903aa284"), + testnet, + ), + net = testnet, + )) + + # Negative P2SH tests. + tests.append(test( + name = "p2sh wrong hash length", + addr = "", + valid = False, + f = lambda: addrSHH( + ByteArray("00f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), + mainnet, + ), + net = mainnet, + )) + + # Positive P2PK tests. + tests.append(test( + name = "mainnet p2pk compressed (0x02)", + addr = "DsT4FDqBKYG1Xr8aGrT1rKP3kiv6TZ5K5th", + encoded = "DsT4FDqBKYG1Xr8aGrT1rKP3kiv6TZ5K5th", + valid = True, + scriptAddress = ByteArray("028f53838b7639563f27c94845549a41e5146bcd52e7fef0ea6da143a02b0fe2ed"), + f = lambda: addrPK( + ByteArray("028f53838b7639563f27c94845549a41e5146bcd52e7fef0ea6da143a02b0fe2ed"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pk compressed (0x03)", + addr = "DsfiE2y23CGwKNxSGjbfPGeEW4xw1tamZdc", + encoded = "DsfiE2y23CGwKNxSGjbfPGeEW4xw1tamZdc", + valid = True, + scriptAddress = ByteArray("03e925aafc1edd44e7c7f1ea4fb7d265dc672f204c3d0c81930389c10b81fb75de"), + f = lambda: addrPK( + ByteArray("03e925aafc1edd44e7c7f1ea4fb7d265dc672f204c3d0c81930389c10b81fb75de"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pk uncompressed (0x04)", + addr = "DkM3EyZ546GghVSkvzb6J47PvGDyntqiDtFgipQhNj78Xm2mUYRpf", + encoded = "DsfFjaADsV8c5oHWx85ZqfxCZy74K8RFuhK", + valid = True, + saddr = "0264c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0", + scriptAddress = ByteArray("0464c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), + f = lambda: addrPK( + ByteArray("0464c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "testnet p2pk compressed (0x02)", + addr = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", + encoded = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", + valid = True, + scriptAddress = ByteArray("026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e"), + f = lambda: addrPK( + ByteArray("026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e"), + testnet, + ), + net = testnet, + )) + tests.append(test( + name = "testnet p2pk compressed (0x03)", + addr = "TsWZ1EzypJfMwBKAEDYKuyHRGctqGAxMje2", + encoded = "TsWZ1EzypJfMwBKAEDYKuyHRGctqGAxMje2", + valid = True, + scriptAddress = ByteArray("030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af5"), + f = lambda: addrPK( + ByteArray("030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af5"), + testnet, + ), + net = testnet, + )) + tests.append(test( + name = "testnet p2pk uncompressed (0x04)", + addr = "TkKmMiY5iDh4U3KkSopYgkU1AzhAcQZiSoVhYhFymZHGMi9LM9Fdt", + encoded = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", + valid = True, + saddr = "026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e", + scriptAddress = ByteArray("046a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), + f = lambda: addrPK( + ByteArray("046a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), + testnet, + ), + net = testnet, + )) + + # Negative P2PK tests. + tests.append(test( + name = "mainnet p2pk hybrid (0x06)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("0664c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "mainnet p2pk hybrid (0x07)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("07348d8aeb4253ca52456fe5da94ab1263bfee16bb8192497f666389ca964f84798375129d7958843b14258b905dc94faed324dd8a9d67ffac8cc0a85be84bac5d"), + mainnet, + ), + net = mainnet, + )) + tests.append(test( + name = "testnet p2pk hybrid (0x06)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("066a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), + testnet, + ), + net = testnet, + )) + tests.append(test( + name = "testnet p2pk hybrid (0x07)", + addr = "", + valid = False, + f = lambda: addrPK( + ByteArray("07edd40747de905a9becb14987a1a26c1adbd617c45e1583c142a635bfda9493dfa1c6d36735974965fe7b861e7f6fcc087dc7fe47380fa8bde0d9c322d53c0e89"), + testnet, + ), + net = testnet, + )) + + for test in tests: + # Decode addr and compare error against valid. + err = None + try: + decoded = txscript.decodeAddress(test.addr, test.net) + except Exception as e: + err = e + self.assertEqual(err == None, test.valid, "%s error: %s" % (test.name, err)) + + if err == None: + # Ensure the stringer returns the same address as theoriginal. + self.assertEqual(test.addr, decoded.string(), test.name) + + # Encode again and compare against the original. + encoded = decoded.address() + self.assertEqual(test.encoded, encoded) + + # Perform type-specific calculations. + if isinstance(decoded, crypto.AddressPubKeyHash): + d = ByteArray(b58decode(encoded)) + saddr = d[2 : 2+crypto.RIPEMD160_SIZE] + + elif isinstance(decoded, crypto.AddressScriptHash): + d = ByteArray(b58decode(encoded)) + saddr = d[2 : 2+crypto.RIPEMD160_SIZE] + + elif isinstance(decoded, crypto.AddressSecpPubKey): + # Ignore the error here since the script + # address is checked below. + try: + saddr = ByteArray(decoded.string()) + except Exception: + saddr = test.saddr + + elif isinstance(decoded, crypto.AddressEdwardsPubKey): + # Ignore the error here since the script + # address is checked below. + # saddr = ByteArray(decoded.String()) + self.fail("Edwards sigs unsupported") + + elif isinstance(decoded, crypto.AddressSecSchnorrPubKey): + # Ignore the error here since the script + # address is checked below. + # saddr = ByteArray(decoded.String()) + self.fail("Schnorr sigs unsupported") + + # Check script address, as well as the Hash160 method for P2PKH and + # P2SH addresses. + self.assertEqual(saddr, decoded.scriptAddress(), test.name) + + if isinstance(decoded, crypto.AddressPubKeyHash): + self.assertEqual(decoded.pkHash, saddr) + + if isinstance(decoded, crypto.AddressScriptHash): + self.assertEqual(decoded.hash160(), saddr) + + if not test.valid: + # If address is invalid, but a creation function exists, + # verify that it returns a nil addr and non-nil error. + if test.f != None: + try: + test.f() + self.fail("%s: address is invalid but creating new address succeeded" % test.name) + except Exception: + pass + continue + + # Valid test, compare address created with f against expected result. + try: + addr = test.f() + except Exception as e: + self.fail("%s: address is valid but creating new address failed with error %s", test.name, e) + self.assertEqual(addr.scriptAddress(), test.scriptAddress, test.name) + + def test_extract_script_addrs(self): + from tinydecred.pydecred import mainnet + scriptVersion = 0 + tests = [] + def pkAddr(b): + addr = crypto.AddressSecpPubKey(b, mainnet) + # force the format to compressed, as per golang tests. + addr.pubkeyFormat = crypto.PKFCompressed + return addr + + class test: + def __init__(self, name="", script=b'', addrs=None, reqSigs=-1, scriptClass=-1, exception=None): + self.name = name + self.script = script + self.addrs = addrs if addrs else [] + self.reqSigs = reqSigs + self.scriptClass = scriptClass + self.exception = exception + tests.append(test( + name = "standard p2pk with compressed pubkey (0x02)", + script = ByteArray("2102192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4ac"), + addrs = [pkAddr(ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"))], + reqSigs = 1, + scriptClass = txscript.PubKeyTy, + )) + tests.append(test( + name = "standard p2pk with uncompressed pubkey (0x04)", + script = ByteArray("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" + "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), + addrs = [ + pkAddr(ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482eca" + "d7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3")), + ], + reqSigs = 1, + scriptClass = txscript.PubKeyTy, + )) + tests.append(test( + name = "standard p2pk with compressed pubkey (0x03)", + script = ByteArray("2103b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65ac"), + addrs = [pkAddr(ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"))], + reqSigs = 1, + scriptClass = txscript.PubKeyTy, + )) + tests.append(test( + name = "2nd standard p2pk with uncompressed pubkey (0x04)", + script = ByteArray("4104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782" + "eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac"), + addrs = [ + pkAddr(ByteArray("04b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2" + "c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b")), + ], + reqSigs = 1, + scriptClass = txscript.PubKeyTy, + )) + tests.append(test( + name = "standard p2pkh", + script = ByteArray("76a914ad06dd6ddee55cbca9a9e3713bd7587509a3056488ac"), + addrs = [crypto.newAddressPubKeyHash(ByteArray("ad06dd6ddee55cbca9a9e3713bd7587509a30564"), mainnet, crypto.STEcdsaSecp256k1)], + reqSigs = 1, + scriptClass = txscript.PubKeyHashTy, + )) + tests.append(test( + name = "standard p2sh", + script = ByteArray("a91463bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb87"), + addrs = [crypto.newAddressScriptHashFromHash(ByteArray("63bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb"), mainnet)], + reqSigs = 1, + scriptClass = txscript.ScriptHashTy, + )) + # from real tx 60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1, vout 0 + tests.append(test( + name = "standard 1 of 2 multisig", + script = ByteArray("514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a47" + "3e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4410461cbdcc5409fb4b4d42b51d3338" + "1354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af52ae"), + addrs = [ + pkAddr(ByteArray("04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a" + "1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4")), + pkAddr(ByteArray("0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34b" + "fa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af")), + ], + reqSigs = 1, + scriptClass = txscript.MultiSigTy, + )) + # from real tx d646f82bd5fbdb94a36872ce460f97662b80c3050ad3209bef9d1e398ea277ab, vin 1 + tests.append(test( + name = "standard 2 of 3 multisig", + script = ByteArray("524104cb9c3c222c5f7a7d3b9bd152f363a0b6d54c9eb312c4d4f9af1e8551b6c421a6a4ab0e2" + "9105f24de20ff463c1c91fcf3bf662cdde4783d4799f787cb7c08869b4104ccc588420deeebea22a7e900cc8" + "b68620d2212c374604e3487ca08f1ff3ae12bdc639514d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e" + "71e610b036aa24104ab47ad1939edcb3db65f7fedea62bbf781c5410d3f22a7a3a56ffefb2238af8627363bd" + "f2ed97c1f89784a1aecdb43384f11d2acc64443c7fc299cef0400421a53ae"), + addrs = [ + pkAddr(ByteArray("04cb9c3c222c5f7a7d3b9bd152f363a0b6d54c9eb312c4d4f9af" + "1e8551b6c421a6a4ab0e29105f24de20ff463c1c91fcf3bf662cdde4783d4799f787cb7c08869b")), + pkAddr(ByteArray("04ccc588420deeebea22a7e900cc8b68620d2212c374604e3487" + "ca08f1ff3ae12bdc639514d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e71e610b036aa2")), + pkAddr(ByteArray("04ab47ad1939edcb3db65f7fedea62bbf781c5410d3f22a7a3a5" + "6ffefb2238af8627363bdf2ed97c1f89784a1aecdb43384f11d2acc64443c7fc299cef0400421a")), + ], + reqSigs = 2, + scriptClass = txscript.MultiSigTy, + )) + + # The below are nonstandard script due to things such as + # invalid pubkeys, failure to parse, and not being of a + # standard form. + + tests.append(test( + name = "p2pk with uncompressed pk missing OP_CHECKSIG", + script = ByteArray("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" + "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + addrs = [], + exception = "unsupported script", + )) + tests.append(test( + name = "valid signature from a sigscript - no addresses", + script = ByteArray("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd41022" + "0181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), + addrs = [], + exception = "unsupported script", + )) + # Note the technically the pubkey is the second item on the + # stack, but since the address extraction intentionally only + # works with standard PkScripts, this should not return any + # addresses. + tests.append(test( + name = "valid sigscript to redeem p2pk - no addresses", + script = ByteArray("493046022100ddc69738bf2336318e4e041a5a77f305da87428ab1606f023260017854350ddc0" + "22100817af09d2eec36862d16009852b7e3a0f6dd76598290b7834e1453660367e07a014104cd4240c198e12" + "523b6f9cb9f5bed06de1ba37e96a1bbd13745fcf9d11c25b1dff9a519675d198804ba9962d3eca2d5937d58e5a75a71042d40388a4d307f887d"), + addrs = [], + reqSigs = 0, + exception = "unsupported script", + )) + # adapted from btc: + # tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 0 + # invalid public keys + tests.append(test( + name = "1 of 3 multisig with invalid pubkeys", + script = ByteArray("5141042200007353455857696b696c65616b73204361626c6567617465204261636b75700a0a6" + "361626c65676174652d3230313031323034313831312e377a0a0a446f41046e6c6f61642074686520666f6c6" + "c6f77696e67207472616e73616374696f6e732077697468205361746f736869204e616b616d6f746f2773206" + "46f776e6c6f61410420746f6f6c2077686963680a63616e20626520666f756e6420696e207472616e7361637" + "4696f6e2036633533636439383731313965663739376435616463636453ae"), + addrs = [], + exception = "isn't on secp256k1 curve", + )) + # adapted from btc: + # tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 44 + # invalid public keys + tests.append(test( + name = "1 of 3 multisig with invalid pubkeys 2", + script = ByteArray("514104633365633235396337346461636536666430383862343463656638630a6336366263313" + "9393663386239346133383131623336353631386665316539623162354104636163636539393361333938386" + "134363966636336643664616266640a323636336366613963663463303363363039633539336333653931666" + "56465373032392102323364643432643235363339643338613663663530616234636434340a00000053ae"), + addrs = [], + exception = "isn't on secp256k1 curve", + )) + tests.append(test( + name = "empty script", + script = ByteArray(b''), + addrs = [], + reqSigs = 0, + exception = "unsupported script", + )) + tests.append(test( + name = "script that does not parse", + script = ByteArray([opcode.OP_DATA_45]), + addrs = [], + reqSigs = 0, + exception = "unsupported script", + )) + + def checkAddrs(a, b, name): + if len(a) != len(b): + t.fail("extracted address length mismatch. expected %d, got %d" % (len(a), len(b))) + for av, bv in zip(a, b): + if av.scriptAddress() != bv.scriptAddress(): + self.fail("scriptAddress mismatch. expected %s, got %s (%s)" % + (av.scriptAddress().hex(), bv.scriptAddress().hex(), name)) + + for i, t in enumerate(tests): + try: + scriptClass, addrs, reqSigs = txscript.extractPkScriptAddrs(scriptVersion, t.script, mainnet) + except Exception as e: + if t.exception and t.exception in str(e): + continue + self.fail("extractPkScriptAddrs #%d (%s): %s" % (i, t.name, e)) + + self.assertEqual(scriptClass, t.scriptClass, t.name) + + self.assertEqual(reqSigs, t.reqSigs, t.name) + + checkAddrs(t.addrs, addrs, t.name) + def test_pay_to_addr_script(self): + """ + test_pay_to_addr_script ensures the PayToAddrScript function generates + the correct scripts for the various types of addresses. + """ + # 1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX + p2pkhMain = crypto.newAddressPubKeyHash(ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), + mainnet, crypto.STEcdsaSecp256k1) + + # Taken from transaction: + # b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d + p2shMain = crypto.newAddressScriptHashFromHash(ByteArray("e8c300c87986efa84c37c0519929019ef86eb5b4"), mainnet) + + # # disabled until Schnorr signatures implemented + # # mainnet p2pk 13CG6SJ3yHUXo4Cr2RY4THLLJrNFuG3gUg + # p2pkCompressedMain = crypto.newAddressPubKey(ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), + # mainnet) + + p2pkCompressed2Main = crypto.AddressSecpPubKey(ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), + mainnet) + + p2pkUncompressedMain = crypto.AddressSecpPubKey( + ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + mainnet, + ) + # Set the pubkey compressed. See golang TestPayToAddrScript in + # dcrd/tscript/standard_test.go + p2pkUncompressedMain.pubkeyFormat = crypto.PKFCompressed + + class BogusAddress(crypto.AddressPubKeyHash): + pass + + bogusAddress = ( + ByteArray(0x0000), + ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), + crypto.STEcdsaSecp256k1 + ) + + # Errors used in the tests below defined here for convenience and to + # keep the horizontal test size shorter. + class test: + def __init__(self, inAddr, expected, err): + self.inAddr = inAddr + self.expected = expected + self.err = err + tests = [ + # pay-to-pubkey-hash address on mainnet 0 + test( + p2pkhMain, + "DUP HASH160 DATA_20 0xe34cce70c86373273efcc54ce7d2a491bb4a0e8488 CHECKSIG", + False, + ), + # pay-to-script-hash address on mainnet 1 + test( + p2shMain, + "HASH160 DATA_20 0xe8c300c87986efa84c37c0519929019ef86eb5b4 EQUAL", + False, + ), + # disabled until Schnorr signatures implemented + # pay-to-pubkey address on mainnet. compressed key. 2 + # test( + # p2pkCompressedMain, + # "DATA_33 0x02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4 CHECKSIG", + # False, + # ), + # pay-to-pubkey address on mainnet. compressed key (other way). 3 + test( + p2pkCompressed2Main, + "DATA_33 0x03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65 CHECKSIG", + False, + ), + # pay-to-pubkey address on mainnet. for Decred this would + # be uncompressed, but standard for Decred is 33 byte + # compressed public keys. + test( + p2pkUncompressedMain, + "DATA_33 0x0311db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cac", + False, + ), + + # Unsupported address type. + test(bogusAddress, "", True), + ] + + for t in tests: + try: + pkScript = txscript.payToAddrScript(t.inAddr) + except Exception as e: + if not t.err: + self.fail("unexpected exception: %s" % e) + continue + + self.assertEqual(pkScript, parseShortForm(t.expected)) + + def test_script_class(self): + """ + test_script_class ensures all the scripts in scriptClassTests have the expected + class. + """ + scriptVersion = 0 + for test in scriptClassTests(): + script = parseShortForm(test.script) + scriptClass = txscript.getScriptClass(scriptVersion, script) + self.assertEqual(scriptClass, test.scriptClass, test.name) + + def test_calc_signature_hash_reference(self): + """ + test_calc_signature_hash_reference runs the reference signature hash calculation + tests in sighash.json. + """ + fileDir = os.path.dirname(os.path.realpath(__file__)) + path = os.path.join(fileDir, "test-data", "sighash.json") + with open(path, 'r') as f: + tests = json.loads(f.read()) + + scriptVersion = 0 + for i, test in enumerate(tests): + # raw transaction, script, input index, hash type, signature hash (result), expected error, comment (optional) + + # Skip comment lines. + if len(test) == 1: + continue + + if len(test) == 6: + txHex, scriptHex, vin, hashType, sigHashHex, err = test + elif len(test) == 7: + txHex, scriptHex, vin, hashType, sigHashHex, err, comment = test + else: + raise Exception("Test #%d: wrong length %d" % (i, len(test))) + + # Extract and parse the transaction from the test fields. + tx = msgtx.MsgTx.deserialize(ByteArray(txHex)) + + # Extract and parse the script from the test fields. + subScript = ByteArray(scriptHex) + scriptErr = txscript.checkScriptParses(scriptVersion, subScript) + if scriptErr: + self.fail("checkScriptParses failed with error %s" % scriptErr) + + # Extract and parse the signature hash from the test fields. + expectedHash = ByteArray(sigHashHex) + + # Calculate the signature hash and verify expected result. + try: + sigHash = txscript.calcSignatureHash(subScript, hashType, tx, vin, None) + except Exception as e: + if err == "OK": + self.fail("unexpected calcSignatureHash exception: %s" % e) + continue + + self.assertEqual(sigHash, expectedHash) + +class TestDcrdata(unittest.TestCase): + def client(self, **k): + return dcrdata.DcrdataClient("https://alpha.dcrdata.org", customPaths={ + "/tx/send", + "/insight/api/addr/{address}/utxo", + "insight/api/tx/send" + }, **k) + def test_websocket(self): + """ + "newblock": SigNewBlock, + "mempool": SigMempoolUpdate, + "ping": SigPingAndUserCount, + "newtxs": SigNewTxs, + "address": SigAddressTx, + "blockchainSync": SigSyncStatus, + """ + def emitter(o): + print("msg: %s" % repr(o)) + client = self.client(emitter=emitter) + client.subscribeAddresses("Dcur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx") + time.sleep(1) + client.close() + def test_get_block_header(self): + with TemporaryDirectory() as tempDir: + blockchain = dcrdata.DcrdataBlockchain(os.path.join(tempDir, "db.db"), mainnet, "https://alpha.dcrdata.org") + blockchain.connect() + blockchain.blockHeader("298e5cc3d985bfe7f81dc135f360abe089edd4396b86d2de66b0cef42b21d980") + def test_purchase_ticket(self): + from tinydecred.crypto.secp256k1 import curve as Curve + from tinydecred.crypto import rando + with TemporaryDirectory() as tempDir: + blockchain = dcrdata.DcrdataBlockchain(os.path.join(tempDir, "db.db"), testnet, "https://testnet.decred.org") + blockchain.connect() + def broadcast(txHex): + print("test skipping broadcast of transaction: %s" % txHex) + return True + blockchain.broadcast = broadcast + txs = {} + def getTx(txid): + return txs[txid] + blockchain.tx = getTx + addrs = [] + keys = {} + def newTxid(): + return crypto.hashH(rando.generateSeed(20)).hex() + def internal(): + privKey = Curve.generateKey() + pkHash = crypto.hash160(privKey.pub.serializeCompressed().b) + addr = crypto.AddressPubKeyHash(testnet.PubKeyHashAddrID, pkHash) + addrs.append(addr) + keys[addr.string()] = privKey + return addr.string() + def priv(addr): + return keys[addr] + + class KeySource: + def priv(self, *a): + return priv(*a) + def internal(self): + return internal() + + def utxosource(amt, filter): + nextVal = 10 + total = 0 + utxos = [] + + while total < amt: + atoms = int(nextVal*1e8) + privKey = Curve.generateKey() + pkHash = crypto.hash160(privKey.pub.serializeCompressed().b) + addr = crypto.AddressPubKeyHash(testnet.PubKeyHashAddrID, pkHash) + addrs.append(addr) + addrString = addr.string() + keys[addrString] = privKey + pkScript = txscript.makePayToAddrScript(addrString, testnet) + txid = newTxid() + utxos.append(dcrdata.UTXO( + address = addrString, + txid = txid, + vout = 0, + ts = int(time.time()), + scriptPubKey = pkScript, + amount = nextVal, + satoshis = atoms, + )) + tx = msgtx.MsgTx.new() + tx.addTxOut(msgtx.TxOut(value=atoms, pkScript=pkScript)) + txs[txid] = tx + total += atoms + nextVal *= 2 + return utxos, True + + poolPriv = Curve.generateKey() + pkHash = crypto.hash160(poolPriv.pub.serializeCompressed().b) + poolAddr = crypto.AddressPubKeyHash(testnet.PubKeyHashAddrID, pkHash) + scriptHash = crypto.hash160("some script. doesn't matter".encode()) + scriptAddr = crypto.AddressScriptHash(testnet.ScriptHashAddrID, scriptHash) + ticketPrice = int(blockchain.stakeDiff()["next"]*1e8) + class request: + minConf = 0 + expiry = 0 + spendLimit = ticketPrice*1.1 + poolAddress = poolAddr + votingAddress = scriptAddr + ticketFee = 0 + poolFees = 7.5 + count = 1 + txFee = 0 + ticket, spent, newUTXOs = blockchain.purchaseTickets(KeySource(), utxosource, request()) + +class TestStakePool(unittest.TestCase): + def setUp(self): + self.poolURL = "https://teststakepool.decred.org" + self.apiKey = "" + # signing address is needed to validate server-reported redeem script. + self.signingAddress = "" + if not self.apiKey or not self.signingAddress: + print(" no stake pool credentials provided. skipping stake pool test") + raise unittest.SkipTest + def stakePool(self): + stakePool = stakepool.StakePool(self.poolURL, self.apiKey) + stakePool.signingAddress = self.signingAddress + return stakePool + def test_get_purchase_info(self): + from tinydecred.pydecred import testnet + stakePool = self.stakePool() + pi = stakePool.getPurchaseInfo(testnet) + print(pi.__tojson__()) + def test_get_stats(self): + stakePool = self.stakePool() + stats = stakePool.getStats() + print(stats.__tojson__()) + def test_voting(self): + from tinydecred.pydecred import testnet + stakePool = self.stakePool() + pi = stakePool.getPurchaseInfo(testnet) + if pi.voteBits&(1 << 1) != 0: + nextVote = 1|(1 << 2) + else: + nextVote = 1|(1 << 1) + print("changing vote from %d to %d" % (pi.voteBits, nextVote)) + stakePool.setVoteBits(nextVote) + pi = stakePool.getPurchaseInfo(testnet) + self.assertEqual(pi.voteBits, nextVote) \ No newline at end of file diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 1c864c06..f13574fe 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -5,7 +5,7 @@ Based on dcrd txscript. """ -import unittest +import math from tinydecred.crypto.bytearray import ByteArray from tinydecred.pydecred.wire import wire, msgtx # A couple of usefule serialization functions. from tinydecred.crypto import opcode, crypto @@ -14,6 +14,7 @@ HASH_SIZE = 32 SHA256_SIZE = 32 BLAKE256_SIZE = 32 +MAX_UINT64 = 18446744073709551615 NonStandardTy = 0 # None of the recognized forms. PubKeyTy = 1 # Pay pubkey. @@ -60,6 +61,122 @@ MaxPubKeysPerMultiSig = 20 # Multisig can't have more sigs than this. MaxScriptElementSize = 2048 # Max bytes pushable to the stack. +# P2PKHPkScriptSize is the size of a transaction output script that +# pays to a compressed pubkey hash. It is calculated as: +# +# - OP_DUP +# - OP_HASH160 +# - OP_DATA_20 +# - 20 bytes pubkey hash +# - OP_EQUALVERIFY +# - OP_CHECKSIG +P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 + +# RedeemP2PKSigScriptSize is the worst case (largest) serialize size +# of a transaction input script that redeems a compressed P2PK output. +# It is calculated as: +# +# - OP_DATA_73 +# - 72 bytes DER signature + 1 byte sighash +RedeemP2PKSigScriptSize = 1 + 73 + +# P2SHPkScriptSize is the size of a transaction output script that +# pays to a script hash. It is calculated as: +# +# - OP_HASH160 +# - OP_DATA_20 +# - 20 bytes script hash +# - OP_EQUAL +P2SHPkScriptSize = 1 + 1 + 20 + 1 + +# Many of these constants were pulled from the dcrd, and are left as mixed case +# to maintain reference. + +# DefaultRelayFeePerKb is the default minimum relay fee policy for a mempool. +DefaultRelayFeePerKb = 1e4 + +# AtomsPerCent is the number of atomic units in one coin cent. +AtomsPerCent = 1e6 + +# AtomsPerCoin is the number of atomic units in one coin. +AtomsPerCoin = 1e8 + +# MaxAmount is the maximum transaction amount allowed in atoms. +# Decred - Changeme for release +MaxAmount = 21e6 * AtomsPerCoin + +opNonstake = opcode.OP_NOP10 + +# RedeemP2PKHSigScriptSize is the worst case (largest) serialize size +# of a transaction input script that redeems a compressed P2PKH output. +# It is calculated as: +# +# - OP_DATA_73 +# - 72 bytes DER signature + 1 byte sighash +# - OP_DATA_33 +# - 33 bytes serialized compressed pubkey +RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 + +# TicketCommitmentScriptSize is the size of a ticket purchase commitment +# script. It is calculated as: +# +# - OP_RETURN +# - OP_DATA_30 +# - 20 bytes P2SH/P2PKH +# - 8 byte amount +# - 2 byte fee range limits +TicketCommitmentScriptSize = 1 + 1 + 20 + 8 + 2 + +# generatedTxVersion is the version of the transaction being generated. +# It is defined as a constant here rather than using the wire.TxVersion +# constant since a change in the transaction version will potentially +# require changes to the generated transaction. Thus, using the wire +# constant for the generated transaction version could allow creation +# of invalid transactions for the updated version. +generatedTxVersion = 1 + +# MaxStackSize is the maximum combined height of stack and alt stack +# during execution. +MaxStackSize = 1024 + +# MaxScriptSize is the maximum allowed length of a raw script. +MaxScriptSize = 16384 + +# MaxDataCarrierSize is the maximum number of bytes allowed in pushed +# data to be considered a nulldata transaction. +MaxDataCarrierSize = 256 + +# consensusVersion = txscript.consensusVersion +consensusVersion = 0 + +# MaxInputsPerSStx is the maximum number of inputs allowed in an SStx. +MaxInputsPerSStx = 64 + +# MaxOutputsPerSStx is the maximum number of outputs allowed in an SStx; +# you need +1 for the tagged SStx output. +MaxOutputsPerSStx = MaxInputsPerSStx*2 + 1 + +# validSStxAddressOutPrefix is the valid prefix for a 30-byte +# minimum OP_RETURN push for a commitment for an SStx. +validSStxAddressOutMinPrefix = ByteArray([opcode.OP_RETURN, opcode.OP_DATA_30]) + +# MaxSingleBytePushLength is the largest maximum push for an +# SStx commitment or VoteBits push. +MaxSingleBytePushLength = 75 + +# SStxPKHMinOutSize is the minimum size of an OP_RETURN commitment output +# for an SStx tx. +# 20 bytes P2SH/P2PKH + 8 byte amount + 4 byte fee range limits +SStxPKHMinOutSize = 32 + +# SStxPKHMaxOutSize is the maximum size of an OP_RETURN commitment output +# for an SStx tx. +SStxPKHMaxOutSize = 77 + +# defaultTicketFeeLimits is the default byte string for the default +# fee limits imposed on a ticket. +defaultTicketFeeLimits = 0x5800 + # A couple of hashing functions from the crypto module. mac = crypto.mac hashH = crypto.hashH @@ -128,7 +245,7 @@ def __init__(self, version, script): self.version = version self.offset = 0 self.op = None - self.d = None + self.d = ByteArray(b'') self.err = None def next(self): """ @@ -203,7 +320,7 @@ def next(self): self.offset += 1 - op.length + dataLen self.op = op self.d = script[:dataLen] - return False + return True # The only remaining case is an opcode with length zero which is # impossible. @@ -246,6 +363,27 @@ def byteIndex(self): """ return self.offset +class Credit: + """ + Credit is the type representing a transaction output which was spent or + is still spendable by wallet. A UTXO is an unspent Credit, but not all + Credits are UTXOs. + """ + def __init__(self, op, blockMeta, amount, pkScript, received, fromCoinBase=False, hasExpiry=False): + self.op = op + self.blockMeta = blockMeta + self.amount = amount + self.pkScript = pkScript + self.received = received + self.fromCoinBase = fromCoinBase + self.hasExpiry = hasExpiry + +class ExtendedOutPoint: + def __init__(self, op, amt, pkScript): + self.op = op + self.amt = amt + self.pkScript = pkScript + def checkScriptParses(scriptVersion, script): """ checkScriptParses returns None when the script parses without error. @@ -281,7 +419,7 @@ def finalOpcodeData(scriptVersion, script): data = None tokenizer = ScriptTokenizer(scriptVersion, script) while tokenizer.next(): - data = tokenizer.data + data = tokenizer.data() if not tokenizer.err is None: return None return data @@ -362,8 +500,31 @@ def typeOfScript(scriptVersion, script): """ if scriptVersion != DefaultScriptVersion: return NonStandardTy - if isPubKeyHashScript(script): + elif isPubKeyHashScript(script): + return PubKeyHashTy + + elif isPubKeyScript(script): + return PubKeyTy + # elif isPubKeyAltScript(script): + # return PubkeyAltTy + elif isPubKeyHashScript(script): return PubKeyHashTy + # elif isPubKeyHashAltScript(script): + # return PubkeyHashAltTy + elif isScriptHashScript(script): + return ScriptHashTy + elif isMultisigScript(scriptVersion, script): + return MultiSigTy + elif isNullDataScript(scriptVersion, script): + return NullDataTy + elif isStakeSubmissionScript(scriptVersion, script): + return StakeSubmissionTy + elif isStakeGenScript(scriptVersion, script): + return StakeGenTy + elif isStakeRevocationScript(scriptVersion, script): + return StakeRevocationTy + elif isStakeChangeScript(scriptVersion, script): + return StakeSubChangeTy return NonStandardTy def isPubKeyHashScript(script): @@ -505,6 +666,103 @@ def extractStakePubKeyHash(script, stakeOpcode): return None return extractPubKeyHash(script[1:]) +def isStakeSubmissionScript(scriptVersion, script): + """ + isStakeSubmissionScript returns whether or not the passed script is a + supported stake submission script. + + NOTE: This function is only valid for version 0 scripts. It will always + return false for other script versions. + """ + # The only currently supported script version is 0. + if scriptVersion != 0: + return False + + # The only supported stake submission scripts are pay-to-pubkey-hash and + # pay-to-script-hash tagged with the stake submission opcode. + stakeOpcode = opcode.OP_SSTX + return (extractStakePubKeyHash(script, stakeOpcode) != None or + extractStakeScriptHash(script, stakeOpcode) != None) + +def isStakeGenScript(scriptVersion, script): + """ + isStakeGenScript returns whether or not the passed script is a supported + stake generation script. + + NOTE: This function is only valid for version 0 scripts. It will always + return false for other script versions. + """ + # The only currently supported script version is 0. + if scriptVersion != 0: + return False + + # The only supported stake generation scripts are pay-to-pubkey-hash and + # pay-to-script-hash tagged with the stake submission opcode. + stakeOpcode = opcode.OP_SSGEN + return (extractStakePubKeyHash(script, stakeOpcode) != None or + extractStakeScriptHash(script, stakeOpcode) != None) + +def isStakeRevocationScript(scriptVersion, script): + """ + isStakeRevocationScript returns whether or not the passed script is a + supported stake revocation script. + + NOTE: This function is only valid for version 0 scripts. It will always + return false for other script versions. + """ + # The only currently supported script version is 0. + if scriptVersion != 0: + return False + + # The only supported stake revocation scripts are pay-to-pubkey-hash and + # pay-to-script-hash tagged with the stake submission opcode. + stakeOpcode = opcode.OP_SSRTX + return (extractStakePubKeyHash(script, stakeOpcode) != None or + extractStakeScriptHash(script, stakeOpcode) != None) + +def isStakeChangeScript(scriptVersion, script): + """ + isStakeChangeScript returns whether or not the passed script is a supported + stake change script. + + NOTE: This function is only valid for version 0 scripts. It will always + return false for other script versions. + """ + # The only currently supported script version is 0. + if scriptVersion != 0: + return False + + # The only supported stake change scripts are pay-to-pubkey-hash and + # pay-to-script-hash tagged with the stake submission opcode. + stakeOpcode = opcode.OP_SSTXCHANGE + return (extractStakePubKeyHash(script, stakeOpcode) != None or + extractStakeScriptHash(script, stakeOpcode) != None) + +def getStakeOutSubclass(pkScript): + """ + getStakeOutSubclass extracts the subclass (P2PKH or P2SH) from a stake + output. + + NOTE: This function is only valid for version 0 scripts. Since the function + does not accept a script version, the results are undefined for other script + versions. + """ + scriptVersion = 0 + err = checkScriptParses(scriptVersion, pkScript) + if err != None: + raise err + + scriptClass = typeOfScript(scriptVersion, pkScript) + isStake = scriptClass in (StakeSubmissionTy, StakeGenTy, StakeRevocationTy, StakeSubChangeTy) + + subClass = 0 + if isStake: + subClass = typeOfScript(scriptVersion, pkScript[1:]) + else: + raise Exception("not a stake output") + + return subClass + class multiSigDetails(object): """ multiSigDetails houses details extracted from a standard multisig script. @@ -576,6 +834,142 @@ def extractMultisigScriptDetails(scriptVersion, script, extractPubKeys): return invalidMSDetails() return multiSigDetails(pubkeys, numPubkeys, requiredSigs, True) +def isMultisigScript(scriptVersion, script): + """ + isMultisigScript returns whether or not the passed script is a standard + multisig script. + + NOTE: This function is only valid for version 0 scripts. It will always + return false for other script versions. + """ + # Since this is only checking the form of the script, don't extract the + # public keys to avoid the allocation. + details = extractMultisigScriptDetails(scriptVersion, script, False) + return details.valid + +def isNullDataScript(scriptVersion, script): + """ + isNullDataScript returns whether or not the passed script is a standard + null data script. + + NOTE: This function is only valid for version 0 scripts. It will always + return false for other script versions. + """ + # The only currently supported script version is 0. + if scriptVersion != 0: + return False + + # A null script is of the form: + # OP_RETURN + # + # Thus, it can either be a single OP_RETURN or an OP_RETURN followed by a + # data push up to MaxDataCarrierSize bytes. + + # The script can't possibly be a null data script if it doesn't start + # with OP_RETURN. Fail fast to avoid more work below. + if len(script) < 1 or script[0] != opcode.OP_RETURN: + return False + + # Single OP_RETURN. + if len(script) == 1: + return True + + # OP_RETURN followed by data push up to MaxDataCarrierSize bytes. + tokenizer = ScriptTokenizer(scriptVersion, script[1:]) + + return (tokenizer.next() and tokenizer.done() and + (isSmallInt(tokenizer.opcode()) or tokenizer.opcode() <= opcode.OP_PUSHDATA4) and + len(tokenizer.data()) <= MaxDataCarrierSize) + +def checkSStx(tx): + """ + checkSStx returns an error if a transaction is not a stake submission + transaction. It does some simple validation steps to make sure the number of + inputs, number of outputs, and the input/output scripts are valid. + + SStx transactions are specified as below. + Inputs: + untagged output 1 [index 0] + untagged output 2 [index 1] + ... + untagged output MaxInputsPerSStx [index MaxInputsPerSStx-1] + + Outputs: + OP_SSTX tagged output [index 0] + OP_RETURN push of input 1's address for reward receiving [index 1] + OP_SSTXCHANGE tagged output for input 1 [index 2] + OP_RETURN push of input 2's address for reward receiving [index 3] + OP_SSTXCHANGE tagged output for input 2 [index 4] + ... + OP_RETURN push of input MaxInputsPerSStx's address for reward receiving + [index (MaxInputsPerSStx*2)-2] + OP_SSTXCHANGE tagged output [index (MaxInputsPerSStx*2)-1] + + The output OP_RETURN pushes should be of size 20 bytes (standard address). + """ + # Check to make sure there aren't too many inputs. + # CheckTransactionSanity already makes sure that number of inputs is + # greater than 0, so no need to check that. + if len(tx.txIn) > MaxInputsPerSStx: + raise Exception("SStx has too many inputs") + + # Check to make sure there aren't too many outputs. + if len(tx.txOut) > MaxOutputsPerSStx: + raise Exception("SStx has too many outputs") + + # Check to make sure there are some outputs. + if len(tx.txOut) == 0: + raise Exception("SStx has no outputs") + + # Check to make sure that all output scripts are the consensus version. + for idx, txOut in enumerate(tx.txOut): + if txOut.version != consensusVersion: + raise Exception("invalid script version found in txOut idx %d" % idx) + + # Ensure that the first output is tagged OP_SSTX. + if getScriptClass(tx.txOut[0].version, tx.txOut[0].pkScript) != StakeSubmissionTy: + raise Exception("First SStx output should have been OP_SSTX tagged, but it was not") + + # Ensure that the number of outputs is equal to the number of inputs + # + 1. + if (len(tx.txIn)*2 + 1) != len(tx.txOut): + raise Exception("The number of inputs in the SStx tx was not the number of outputs/2 - 1") + + # Ensure that the rest of the odd outputs are 28-byte OP_RETURN pushes that + # contain putative pubkeyhashes, and that the rest of the odd outputs are + # OP_SSTXCHANGE tagged. + for outTxIndex in range(1, len(tx.txOut)): + scrVersion = tx.txOut[outTxIndex].version + rawScript = tx.txOut[outTxIndex].pkScript + + # Check change outputs. + if outTxIndex%2 == 0 : + if getScriptClass(scrVersion, rawScript) != StakeSubChangeTy: + raise Exception("SStx output at output index %d was not an sstx change output", outTxIndex) + continue + + # Else (odd) check commitment outputs. The script should be a + # NullDataTy output. + if getScriptClass(scrVersion, rawScript) != NullDataTy: + raise Exception("SStx output at output index %d was not a NullData (OP_RETURN) push", outTxIndex) + + # The length of the output script should be between 32 and 77 bytes long. + if len(rawScript) < SStxPKHMinOutSize or len(rawScript) > SStxPKHMaxOutSize: + raise Exception("SStx output at output index %d was a NullData (OP_RETURN) push of the wrong size", outTxIndex) + + # The OP_RETURN output script prefix should conform to the standard. + outputScriptBuffer = rawScript.copy() + outputScriptPrefix = outputScriptBuffer[:2] + + minPush = validSStxAddressOutMinPrefix[1] + maxPush = validSStxAddressOutMinPrefix[1] + (MaxSingleBytePushLength - minPush) + pushLen = outputScriptPrefix[1] + pushLengthValid = (pushLen >= minPush) and (pushLen <= maxPush) + # The first byte should be OP_RETURN, while the second byte should be a + # valid push length. + if not (outputScriptPrefix[0] == validSStxAddressOutMinPrefix[0]) or not pushLengthValid: + raise Exception("sstx commitment at output idx %v had an invalid prefix", outTxIndex) + # asSmallInt returns the passed opcode, which must be true according to # isSmallInt(), as an integer. def asSmallInt(op): @@ -675,6 +1069,97 @@ def payToPubKeyScript(serializedPubKey): script += opcode.OP_CHECKSIG return script +def payToStakePKHScript(addr, stakeCode): + script = ByteArray(stakeCode) + script += opcode.OP_DUP + script += opcode.OP_HASH160 + script += addData(addr.scriptAddress()) + script += opcode.OP_EQUALVERIFY + script += opcode.OP_CHECKSIG + return script + +def payToStakeSHScript(addr, stakeCode): + script = ByteArray(stakeCode) + script += opcode.OP_HASH160 + script += addData(addr.scriptAddress()) + script += opcode.OP_EQUAL + return script + +def payToSStx(addr): + """ + payToSStx creates a new script to pay a transaction output to a script hash or + public key hash, but tags the output with OP_SSTX. For use in constructing + valid SStxs. + """ + # Only pay to pubkey hash and pay to script hash are + # supported. + scriptType = PubKeyHashTy + if isinstance(addr, crypto.AddressPubKeyHash): + if addr.sigType != crypto.STEcdsaSecp256k1: + raise Exception("unable to generate payment script for " + "unsupported digital signature algorithm") + elif isinstance(addr, crypto.AddressScriptHash): + scriptType = ScriptHashTy + else: + raise Exception("unable to generate payment script for " + "unsupported address type %s" % type(addr)) + + if scriptType == PubKeyHashTy: + return payToStakePKHScript(addr, opcode.OP_SSTX) + return payToStakeSHScript(addr, opcode.OP_SSTX) + +def generateSStxAddrPush(addr, amount, limits): + """ + generateSStxAddrPush generates an OP_RETURN push for SSGen payment addresses in + an SStx. + """ + # Only pay to pubkey hash and pay to script hash are + # supported. + scriptType = PubKeyHashTy + if isinstance(addr, crypto.AddressPubKeyHash): + if addr.sigType != crypto.STEcdsaSecp256k1: + raise Exception("unable to generate payment script for " + "unsupported digital signature algorithm") + elif isinstance(addr, crypto.AddressScriptHash): + scriptType = ScriptHashTy + else: + raise Exception("unable to generate payment script for unsupported address type %s" % type(addr)) + + # Concatenate the prefix, pubkeyhash, and amount. + adBytes = addr.scriptAddress() + adBytes += ByteArray(amount, length=8).littleEndian() + adBytes += ByteArray(limits, length=2).littleEndian() + + # Set the bit flag indicating pay to script hash. + if scriptType == ScriptHashTy: + adBytes[27] |= 1 << 7 + + script = ByteArray(opcode.OP_RETURN) + script += addData(adBytes) + return script + +def payToSStxChange(addr): + """ + payToSStxChange creates a new script to pay a transaction output to a + public key hash, but tags the output with OP_SSTXCHANGE. For use in constructing + valid SStxs. + """ + # Only pay to pubkey hash and pay to script hash are + # supported. + scriptType = PubKeyHashTy + if isinstance(addr, crypto.AddressPubKeyHash): + if addr.sigType != crypto.STEcdsaSecp256k1: + raise Exception("unable to generate payment script for " + "unsupported digital signature algorithm") + elif isinstance(addr, crypto.AddressScriptHash): + scriptType = ScriptHashTy + else: + raise Exception("unable to generate payment script for unsupported address type %s", type(addr)) + + if scriptType == PubKeyHashTy: + return payToStakePKHScript(addr, opcode.OP_SSTXCHANGE) + return payToStakeSHScript(addr, opcode.OP_SSTXCHANGE) + def decodeAddress(addr, net): """ DecodeAddress decodes the string encoding of an address and returns the @@ -1090,7 +1575,7 @@ def calcSignatureHash(script, hashType, tx, idx, cachedPrefix): value = txOut.value pkScript = txOut.pkScript if hashType&sigHashMask == SigHashSingle and txOutIdx != idx: - value = -1 + value = MAX_UINT64 pkScript = b'' prefixBuf += ByteArray(value, length=8).littleEndian() prefixBuf += ByteArray(txOut.version, length=2).littleEndian() @@ -1158,6 +1643,44 @@ def calcSignatureHash(script, hashType, tx, idx, cachedPrefix): h = hashH(sigHashBuf.bytes()) return h +def signP2PKHMsgTx(msgtx, prevOutputs, keysource, params): + """ + signP2PKHMsgTx sets the SignatureScript for every item in msgtx.TxIn. + It must be called every time a msgtx is changed. + Only P2PKH outputs are supported at this point. + """ + if len(prevOutputs) != len(msgtx.txIn): + raise Exception("Number of prevOutputs (%d) does not match number of tx inputs (%d)" % + len(prevOutputs), len(msgtx.TxIn)) + + for i, output in enumerate(prevOutputs): + # Errors don't matter here, as we only consider the + # case where len(addrs) == 1. + _, addrs, _ = extractPkScriptAddrs(0, output.pkScript, params) + if len(addrs) != 1: + continue + apkh = addrs[0] + if not isinstance(apkh, crypto.AddressPubKeyHash): + raise Exception("previous output address is not P2PKH") + + privKey = keysource.priv(apkh.string()) + sigscript = signatureScript(msgtx, i, output.pkScript, SigHashAll, privKey, True) + msgtx.txIn[i].signatureScript = sigscript + +def paysHighFees(totalInput, tx): + """ + paysHighFees checks whether the signed transaction pays insanely high fees. + Transactons are defined to have a high fee if they have pay a fee rate that + is 1000 time higher than the default fee. + """ + fee = totalInput - sum([op.value for op in tx.txOut]) + if fee <= 0: + # Impossible to determine + return False + + maxFee = calcMinRequiredTxRelayFee(1000*DefaultRelayFeePerKb, tx.serializeSize()) + return fee > maxFee + def sigHashPrefixSerializeSize(hashType, txIns, txOuts, signIdx): """ sigHashPrefixSerializeSize returns the number of bytes the passed parameters @@ -1405,921 +1928,443 @@ def signTxOutput(privKey, chainParams, tx, idx, pkScript, hashType, previousScri scriptClass == StakeGenTy or scriptClass == StakeRevocationTy) if isStakeType: - # scriptClass = getStakeOutSubclass(pkScript) - raise Exception("unimplemented") + scriptClass = getStakeOutSubclass(pkScript) if scriptClass == ScriptHashTy: raise Exception("ScriptHashTy signing unimplemented") # # TODO keep the sub addressed and pass down to merge. - # realSigScript, _, _, _ = sign(privKey, chainParams, tx, idx, sigScript, hashType, sigType) + realSigScript, _, _, _ = sign(privKey, chainParams, tx, idx, sigScript, hashType, sigType) - # # Append the p2sh script as the last push in the script. - # script = ByteArray(b'') - # script += realSigScript - # script += sigScript - # script += realSigScript - # script += addData(sigScript) + # Append the p2sh script as the last push in the script. + script = ByteArray(b'') + script += realSigScript + script += addData(sigScript) - # sigScript = script + sigScript = script # # TODO keep a copy of the script for merging. # Merge scripts. with any previous data, if any. mergedScript = mergeScripts(chainParams, tx, idx, pkScript, scriptClass, addresses, nrequired, sigScript, previousScript) return mergedScript -class TestTxScript(unittest.TestCase): - def test_var_int_serialize(self): - """ - TestVarIntSerializeSize ensures the serialize size for variable length - integers works as intended. - """ - tests = [ - (0, 1), # Single byte encoded - (0xfc, 1), # Max single byte encoded - (0xfd, 3), # Min 3-byte encoded - (0xffff, 3), # Max 3-byte encoded - (0x10000, 5), # Min 5-byte encoded - (0xffffffff, 5), # Max 5-byte encoded - (0x100000000, 9), # Min 9-byte encoded - (0xffffffffffffffff, 9), # Max 9-byte encoded - ] - - for i, (val, size) in enumerate(tests): - self.assertEqual(varIntSerializeSize(val), size, msg="test at index %d" % i) - def test_calc_signature_hash(self): - """ TestCalcSignatureHash does some rudimentary testing of msg hash calculation. """ - tx = msgtx.MsgTx.new() - for i in range(3): - txIn = msgtx.TxIn(msgtx.OutPoint( - txHash = hashH(ByteArray(i, length=1).bytes()), - idx = i, - tree = 0, - ), 0) - txIn.sequence = 0xFFFFFFFF - - tx.addTxIn(txIn) - for i in range(2): - txOut = msgtx.TxOut() - txOut.pkScript = ByteArray("51", length=1) - txOut.value = 0x0000FF00FF00FF00 - tx.addTxOut(txOut) - - want = ByteArray("4ce2cd042d64e35b36fdbd16aff0d38a5abebff0e5e8f6b6b31fcd4ac6957905") - script = ByteArray("51", length=1) - - msg1 = calcSignatureHash(script, SigHashAll, tx, 0, None) - - prefixHash = tx.hash() - msg2 = calcSignatureHash(script, SigHashAll, tx, 0, prefixHash) - - self.assertEqual(msg1, want) - - self.assertEqual(msg2, want) - - self.assertEqual(msg1, msg2) - - # Move the index and make sure that we get a whole new hash, despite - # using the same TxOuts. - msg3 = calcSignatureHash(script, SigHashAll, tx, 1, prefixHash) - - self.assertNotEqual(msg1, msg3) - def test_script_tokenizer(self): - """ - TestScriptTokenizer ensures a wide variety of behavior provided by the script - tokenizer performs as expected. - """ +def getP2PKHOpCode(pkScript): + """ + getP2PKHOpCode returns opNonstake for non-stake transactions, or + the stake op code tag for stake transactions. - # Add both positive and negative tests for OP_DATA_1 through OP_DATA_75. - tests = [] - for op in range(opcode.OP_DATA_1, opcode.OP_DATA_75): - data = ByteArray([1]*op) - tests.append(( - "OP_DATA_%d" % op, - ByteArray(op, length=1) + data, - ((op, data, 1 + op), ), - 1 + op, - None, - )) - - # Create test that provides one less byte than the data push requires. - tests.append(( - "short OP_DATA_%d" % op, - ByteArray(op) + data[1:], - None, - 0, - Exception, - )) - - # Add both positive and negative tests for OP_PUSHDATA{1,2,4}. - data = ByteArray([1]*76) - tests.extend([( - "OP_PUSHDATA1", - ByteArray(opcode.OP_PUSHDATA1) + ByteArray(0x4c) + ByteArray([0x01]*76), - ((opcode.OP_PUSHDATA1, data, 2 + len(data)),), - 2 + len(data), - None, - ), ( - "OP_PUSHDATA1 no data length", - ByteArray(opcode.OP_PUSHDATA1), - None, - 0, - Exception, - ), ( - "OP_PUSHDATA1 short data by 1 byte", - ByteArray(opcode.OP_PUSHDATA1) + ByteArray(0x4c) + ByteArray([0x01]*75), - None, - 0, - Exception, - ), ( - "OP_PUSHDATA2", - ByteArray(opcode.OP_PUSHDATA2) + ByteArray(0x4c00) + ByteArray([0x01]*76), - ((opcode.OP_PUSHDATA2, data, 3 + len(data)),), - 3 + len(data), - None, - ), ( - "OP_PUSHDATA2 no data length", - ByteArray(opcode.OP_PUSHDATA2), - None, - 0, - Exception, - ), ( - "OP_PUSHDATA2 short data by 1 byte", - ByteArray(opcode.OP_PUSHDATA2) + ByteArray(0x4c00) + ByteArray([0x01]*75), - None, - 0, - Exception, - ), ( - "OP_PUSHDATA4", - ByteArray(opcode.OP_PUSHDATA4) + ByteArray(0x4c000000) + ByteArray([0x01]*76), - ((opcode.OP_PUSHDATA4, data, 5 + len(data)),), - 5 + len(data), - None, - ), ( - "OP_PUSHDATA4 no data length", - ByteArray(opcode.OP_PUSHDATA4), - None, - 0, - Exception, - ), ( - "OP_PUSHDATA4 short data by 1 byte", - ByteArray(opcode.OP_PUSHDATA4) + ByteArray(0x4c000000) + ByteArray([0x01]*75), - None, - 0, - Exception, - )]) - - # Add tests for OP_0, and OP_1 through OP_16 (small integers/true/false). - opcodes = ByteArray(opcode.OP_0) - for op in range(opcode.OP_1, opcode.OP_16): - opcodes += op - for op in opcodes: - tests.append(( - "OP_%d" % op, - ByteArray(op), - ((op, None, 1),), - 1, - None, - )) - - # Add various positive and negative tests for multi-opcode scripts. - tests.extend([( - "pay-to-pubkey-hash", - ByteArray(opcode.OP_DUP) + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*20) + ByteArray(opcode.OP_EQUAL) + ByteArray(opcode.OP_CHECKSIG), - ( - (opcode.OP_DUP, None, 1), (opcode.OP_HASH160, None, 2), - (opcode.OP_DATA_20, ByteArray([0x01]*20), 23), - (opcode.OP_EQUAL, None, 24), (opcode.OP_CHECKSIG, None, 25), - ), - 25, - None, - ), ( - "almost pay-to-pubkey-hash (short data)", - ByteArray(opcode.OP_DUP) + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*17) + ByteArray(opcode.OP_EQUAL) + ByteArray(opcode.OP_CHECKSIG), - ( - (opcode.OP_DUP, None, 1), (opcode.OP_HASH160, None, 2), - ), - 2, - Exception, - ), ( - "almost pay-to-pubkey-hash (overlapped data)", - ByteArray(opcode.OP_DUP) + ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL) + ByteArray(opcode.OP_CHECKSIG), - ( - (opcode.OP_DUP, None, 1), (opcode.OP_HASH160, None, 2), - (opcode.OP_DATA_20, ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL), 23), - (opcode.OP_CHECKSIG, None, 24), - ), - 24, - None, - ), ( - "pay-to-script-hash", - ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*20) + ByteArray(opcode.OP_EQUAL), - ( - (opcode.OP_HASH160, None, 1), - (opcode.OP_DATA_20, ByteArray([0x01]*20), 22), - (opcode.OP_EQUAL, None, 23), - ), - 23, - None, - ), ( - "almost pay-to-script-hash (short data)", - ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*18) + ByteArray(opcode.OP_EQUAL), - ( - (opcode.OP_HASH160, None, 1), - ), - 1, - Exception, - ), ( - "almost pay-to-script-hash (overlapped data)", - ByteArray(opcode.OP_HASH160) + ByteArray(opcode.OP_DATA_20) + ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL), - ( - (opcode.OP_HASH160, None, 1), - (opcode.OP_DATA_20, ByteArray([0x01]*19) + ByteArray(opcode.OP_EQUAL), 22), - ), - 22, - None, - )]) - - scriptVersion = 0 - for test_name, test_script, test_expected, test_finalIdx, test_err in tests: - tokenizer = ScriptTokenizer(scriptVersion, test_script) - opcodeNum = 0 - while tokenizer.next(): - # Ensure Next never returns true when there is an error set. - self.assertIs(tokenizer.err, None, msg="%s: Next returned true when tokenizer has err: %r" % (test_name, tokenizer.err)) - - # Ensure the test data expects a token to be parsed. - op = tokenizer.opcode() - data = tokenizer.data - self.assertFalse(opcodeNum >= len(test_expected), msg="%s: unexpected token '%r' (data: '%s')" % (test_name, op, data)) - expected_op, expected_data, expected_index = test_expected[opcodeNum] - - # Ensure the opcode and data are the expected values. - self.assertEqual(op, expected_op, msg="%s: unexpected opcode -- got %d, want %d" % (test_name, op, expected_op)) - self.assertEqual(data, expected_data, msg="%s: unexpected data -- got %s, want %s" % (test_name, data, expected_data)) - - tokenizerIdx = tokenizer.offset - self.assertEqual(tokenizerIdx, expected_index, msg="%s: unexpected byte index -- got %d, want %d" % (test_name, tokenizerIdx, expected_index)) - - opcodeNum += 1 - - # Ensure the tokenizer claims it is done. This should be the case - # regardless of whether or not there was a parse error. - self.assertTrue(tokenizer.done(), msg="%s: tokenizer claims it is not done" % test_name) - - # Ensure the error is as expected. - if test_err is None: - self.assertIs(tokenizer.err, None, msg="%s: unexpected tokenizer err -- got %r, want None" % (test_name, tokenizer.err)) - else: - self.assertTrue(isinstance(tokenizer.err, test_err), msg="%s: unexpected tokenizer err -- got %r, want %r" % (test_name, tokenizer.err, test_err)) + Args: + pkScript (ByteArray): The pubkey script. - # Ensure the final index is the expected value. - tokenizerIdx = tokenizer.offset - self.assertEqual(tokenizerIdx, test_finalIdx, msg="%s: unexpected final byte index -- got %d, want %d" % (test_name, tokenizerIdx, test_finalIdx)) - def test_sign_tx(self): - """ - Based on dcrd TestSignTxOutput. - """ - # make key - # make script based on key. - # sign with magic pixie dust. - hashTypes = ( - SigHashAll, - # SigHashNone, - # SigHashSingle, - # SigHashAll | SigHashAnyOneCanPay, - # SigHashNone | SigHashAnyOneCanPay, - # SigHashSingle | SigHashAnyOneCanPay, - ) - signatureSuites = ( - crypto.STEcdsaSecp256k1, - # crypto.STEd25519, - # crypto.STSchnorrSecp256k1, - ) - - testValueIn = 12345 - tx = msgtx.MsgTx( - serType = wire.TxSerializeFull, - version = 1, - txIn = [ - msgtx.TxIn( - previousOutPoint = msgtx.OutPoint( - txHash = ByteArray(b''), - idx = 0, - tree = 0, - ), - sequence = 4294967295, - valueIn = testValueIn, - blockHeight = 78901, - blockIndex = 23456, - ), - msgtx.TxIn( - previousOutPoint = msgtx.OutPoint( - txHash = ByteArray(b''), - idx = 1, - tree = 0, - ), - sequence = 4294967295, - valueIn = testValueIn, - blockHeight = 78901, - blockIndex = 23456, - ), - msgtx.TxIn( - previousOutPoint = msgtx.OutPoint( - txHash = ByteArray(b''), - idx = 2, - tree = 0, - ), - sequence = 4294967295, - valueIn = testValueIn, - blockHeight = 78901, - blockIndex = 23456, - ), - ], - txOut = [ - msgtx.TxOut( - version = wire.DefaultPkScriptVersion, - value = 1, - ), - msgtx.TxOut( - version = wire.DefaultPkScriptVersion, - value = 2, - ), - msgtx.TxOut( - version = wire.DefaultPkScriptVersion, - value = 3, - ), - ], - lockTime = 0, - expiry = 0, - cachedHash = None, - ) - - # Since the script engine is not implmented, hard code the keys and - # check that the script signature is the same as produced by dcrd. - - # For compressed keys - tests = ( - ("b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", "47304402203220ddaee5e825376d3ae5a0e20c463a45808e066abc3c8c33a133446a4c9eb002200f2b0b534d5294d9ce5974975ab5af11696535c4c76cadaed1fa327d6d210e19012102e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1"), - ("a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", "473044022020eb42f1965c31987a4982bd8f654d86c1451418dd3ccc0a342faa98a384186b022021cd0dcd767e607df159dd25674469e1d172e66631593bf96023519d5c07c43101210224397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b"), - ("8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", "483045022100d71babc95de02df7be1e7b14c0f68fb5dcab500c8ef7cf8172b2ea8ad627533302202968ddc3b2f9ff07d3a736b04e74fa39663f028035b6d175de6a4ef90838b797012103255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d"), - ) - - # For uncompressed keys - # tests = ( - # ("b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", "483045022100e1bab52fe0b460c71e4a4226ada35ebbbff9959835fa26c70e2571ef2634a05b02200683f9bf8233ba89c5f9658041cc8edc56feef74cad238f060c3b04e0c4f1cb1014104e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1c4d3fa86c79cfe4f9d13f1c31fd75de316cdfe913b03c07252b1f02f7ee15c9c"), - # ("a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", "473044022029cf920fe059ca4d7e5d74060ed234ebcc7bca520dfed7238dc1e32a48d182a9022043141a443740815baf0caffc19ff7b948d41424832b4a9c6273be5beb15ed7ce01410424397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b422f7f8591e7a4030eddb635e753523bce3c6025fc4e97987adb385b08984e94"), - # ("8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", "473044022015f417f05573c3201f96f5ae706c0789539e638a4a57915dc077b8134c83f1ff022001afa12cebd5daa04d7a9d261d78d0fb910294d78c269fe0b2aabc2423282fe5014104255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d031fee342d455077982fe105e82added63ad667f0b616f3c2c17e1cc9205f3d1"), - # ) - - # Pay to Pubkey Hash (uncompressed) - # secp256k1 := chainec.Secp256k1 - from tinydecred.pydecred import mainnet - testingParams = mainnet - for hashType in hashTypes: - for suite in signatureSuites: - for idx in range(len(tx.txIn)): - # var keyDB, pkBytes []byte - # var key chainec.PrivateKey - # var pk chainec.PublicKey - kStr, sigStr = tests[idx] - - if suite == crypto.STEcdsaSecp256k1: - # k = Curve.generateKey(rand.Reader) - k = ByteArray(kStr) - privKey = crypto.privKeyFromBytes(k) - pkBytes = privKey.pub.serializeCompressed() - else: - raise Exception("test for signature suite %d not implemented" % suite) - - address = crypto.newAddressPubKeyHash(crypto.hash160(pkBytes.bytes()), testingParams, suite) - - pkScript = makePayToAddrScript(address.string(), testingParams) - - # chainParams, tx, idx, pkScript, hashType, kdb, sdb, previousScript, sigType - sigScript = signTxOutput(privKey, testingParams, tx, idx, pkScript, hashType, None, suite) - - self.assertEqual(sigScript, ByteArray(sigStr), msg="%d:%d:%d" % (hashType, idx, suite)) - return - def test_addresses(self): - from tinydecred.pydecred import mainnet, testnet - from base58 import b58decode - class test: - def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAddress=None, f=None, net=None): - self.name = name - self.addr = addr - self.saddr = saddr - self.encoded = encoded - self.valid = valid - self.scriptAddress = scriptAddress - self.f = f - self.net = net - - addrPKH = crypto.newAddressPubKeyHash - addrSH = crypto.newAddressScriptHash - addrSHH = crypto.newAddressScriptHashFromHash - addrPK = crypto.AddressSecpPubKey - - tests = [] - # Positive P2PKH tests. - tests.append(test( - name = "mainnet p2pkh", - addr = "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", - encoded = "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", - valid = True, - scriptAddress = ByteArray("2789d58cfa0957d206f025c2af056fc8a77cebb0"), - f = lambda: addrPKH( - ByteArray("2789d58cfa0957d206f025c2af056fc8a77cebb0"), - mainnet, - crypto.STEcdsaSecp256k1, - ), - net = mainnet, - )) - tests.append(test( - name = "mainnet p2pkh 2", - addr = "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9", - encoded = "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9", - valid = True, - scriptAddress = ByteArray("229ebac30efd6a69eec9c1a48e048b7c975c25f2"), - f = lambda: addrPKH( - ByteArray("229ebac30efd6a69eec9c1a48e048b7c975c25f2"), - mainnet, - crypto.STEcdsaSecp256k1, - ), - net = mainnet, - )) - tests.append(test( - name = "testnet p2pkh", - addr = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h", - encoded = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h", - valid = True, - scriptAddress = ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), - f = lambda: addrPKH( - ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), - testnet, - crypto.STEcdsaSecp256k1 - ), - net = testnet, - )) - - # Negative P2PKH tests. - tests.append(test( - name = "p2pkh wrong hash length", - addr = "", - valid = False, - f = lambda: addrPKH( - ByteArray("000ef030107fd26e0b6bf40512bca2ceb1dd80adaa"), - mainnet, - crypto.STEcdsaSecp256k1, - ), - )) - tests.append(test( - name = "p2pkh bad checksum", - addr = "TsmWaPM77WSyA3aiQ2Q1KnwGDVWvEkhip23", - valid = False, - net = testnet, - )) - - # Positive P2SH tests. - tests.append(test( - # Taken from transactions: - # output: 3c9018e8d5615c306d72397f8f5eef44308c98fb576a88e030c25456b4f3a7ac - # input: 837dea37ddc8b1e3ce646f1a656e79bbd8cc7f558ac56a169626d649ebe2a3ba. - name = "mainnet p2sh", - addr = "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", - encoded = "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", - valid = True, - scriptAddress = ByteArray("f0b4e85100aee1a996f22915eb3c3f764d53779a"), - f = lambda: addrSH( - ByteArray("512103aa43f0a6c15730d886cc1f0342046d20175483d90d7ccb657f90c489111d794c51ae"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - # Taken from transactions: - # output: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d - # input: (not yet redeemed at time test was written) - name = "mainnet p2sh 2", - addr = "DcqgK4N4Ccucu2Sq4VDAdu4wH4LASLhzLVp", - encoded = "DcqgK4N4Ccucu2Sq4VDAdu4wH4LASLhzLVp", - valid = True, - scriptAddress = ByteArray("c7da5095683436f4435fc4e7163dcafda1a2d007"), - f = lambda: addrSHH( - ByteArray("c7da5095683436f4435fc4e7163dcafda1a2d007"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - # Taken from bitcoind base58_keys_valid. - name = "testnet p2sh", - addr = "TccWLgcquqvwrfBocq5mcK5kBiyw8MvyvCi", - encoded = "TccWLgcquqvwrfBocq5mcK5kBiyw8MvyvCi", - valid = True, - scriptAddress = ByteArray("36c1ca10a8a6a4b5d4204ac970853979903aa284"), - f = lambda: addrSHH( - ByteArray("36c1ca10a8a6a4b5d4204ac970853979903aa284"), - testnet, - ), - net = testnet, - )) - - # Negative P2SH tests. - tests.append(test( - name = "p2sh wrong hash length", - addr = "", - valid = False, - f = lambda: addrSHH( - ByteArray("00f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), - mainnet, - ), - net = mainnet, - )) - - # Positive P2PK tests. - tests.append(test( - name = "mainnet p2pk compressed (0x02)", - addr = "DsT4FDqBKYG1Xr8aGrT1rKP3kiv6TZ5K5th", - encoded = "DsT4FDqBKYG1Xr8aGrT1rKP3kiv6TZ5K5th", - valid = True, - scriptAddress = ByteArray("028f53838b7639563f27c94845549a41e5146bcd52e7fef0ea6da143a02b0fe2ed"), - f = lambda: addrPK( - ByteArray("028f53838b7639563f27c94845549a41e5146bcd52e7fef0ea6da143a02b0fe2ed"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - name = "mainnet p2pk compressed (0x03)", - addr = "DsfiE2y23CGwKNxSGjbfPGeEW4xw1tamZdc", - encoded = "DsfiE2y23CGwKNxSGjbfPGeEW4xw1tamZdc", - valid = True, - scriptAddress = ByteArray("03e925aafc1edd44e7c7f1ea4fb7d265dc672f204c3d0c81930389c10b81fb75de"), - f = lambda: addrPK( - ByteArray("03e925aafc1edd44e7c7f1ea4fb7d265dc672f204c3d0c81930389c10b81fb75de"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - name = "mainnet p2pk uncompressed (0x04)", - addr = "DkM3EyZ546GghVSkvzb6J47PvGDyntqiDtFgipQhNj78Xm2mUYRpf", - encoded = "DsfFjaADsV8c5oHWx85ZqfxCZy74K8RFuhK", - valid = True, - saddr = "0264c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0", - scriptAddress = ByteArray("0464c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), - f = lambda: addrPK( - ByteArray("0464c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - name = "testnet p2pk compressed (0x02)", - addr = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", - encoded = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", - valid = True, - scriptAddress = ByteArray("026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e"), - f = lambda: addrPK( - ByteArray("026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e"), - testnet, - ), - net = testnet, - )) - tests.append(test( - name = "testnet p2pk compressed (0x03)", - addr = "TsWZ1EzypJfMwBKAEDYKuyHRGctqGAxMje2", - encoded = "TsWZ1EzypJfMwBKAEDYKuyHRGctqGAxMje2", - valid = True, - scriptAddress = ByteArray("030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af5"), - f = lambda: addrPK( - ByteArray("030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af5"), - testnet, - ), - net = testnet, - )) - tests.append(test( - name = "testnet p2pk uncompressed (0x04)", - addr = "TkKmMiY5iDh4U3KkSopYgkU1AzhAcQZiSoVhYhFymZHGMi9LM9Fdt", - encoded = "Tso9sQD3ALqRsmEkAm7KvPrkGbeG2Vun7Kv", - valid = True, - saddr = "026a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06e", - scriptAddress = ByteArray("046a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), - f = lambda: addrPK( - ByteArray("046a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), - testnet, - ), - net = testnet, - )) - - # Negative P2PK tests. - tests.append(test( - name = "mainnet p2pk hybrid (0x06)", - addr = "", - valid = False, - f = lambda: addrPK( - ByteArray("0664c44653d6567eff5753c5d24a682ddc2b2cadfe1b0c6433b16374dace6778f0b87ca4279b565d2130ce59f75bfbb2b88da794143d7cfd3e80808a1fa3203904"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - name = "mainnet p2pk hybrid (0x07)", - addr = "", - valid = False, - f = lambda: addrPK( - ByteArray("07348d8aeb4253ca52456fe5da94ab1263bfee16bb8192497f666389ca964f84798375129d7958843b14258b905dc94faed324dd8a9d67ffac8cc0a85be84bac5d"), - mainnet, - ), - net = mainnet, - )) - tests.append(test( - name = "testnet p2pk hybrid (0x06)", - addr = "", - valid = False, - f = lambda: addrPK( - ByteArray("066a40c403e74670c4de7656a09caa2353d4b383a9ce66eef51e1220eacf4be06ed548c8c16fb5eb9007cb94220b3bb89491d5a1fd2d77867fca64217acecf2244"), - testnet, - ), - net = testnet, - )) - tests.append(test( - name = "testnet p2pk hybrid (0x07)", - addr = "", - valid = False, - f = lambda: addrPK( - ByteArray("07edd40747de905a9becb14987a1a26c1adbd617c45e1583c142a635bfda9493dfa1c6d36735974965fe7b861e7f6fcc087dc7fe47380fa8bde0d9c322d53c0e89"), - testnet, - ), - net = testnet, - )) - - for test in tests: - # Decode addr and compare error against valid. - err = None - try: - decoded = decodeAddress(test.addr, test.net) - except Exception as e: - err = e - self.assertEqual(err == None, test.valid, "%s error: %s" % (test.name, err)) - - if err == None: - # Ensure the stringer returns the same address as theoriginal. - self.assertEqual(test.addr, decoded.string(), test.name) - - # Encode again and compare against the original. - encoded = decoded.address() - self.assertEqual(test.encoded, encoded) - - # Perform type-specific calculations. - if isinstance(decoded, crypto.AddressPubKeyHash): - d = ByteArray(b58decode(encoded)) - saddr = d[2 : 2+crypto.RIPEMD160_SIZE] - - elif isinstance(decoded, crypto.AddressScriptHash): - d = ByteArray(b58decode(encoded)) - saddr = d[2 : 2+crypto.RIPEMD160_SIZE] - - elif isinstance(decoded, crypto.AddressSecpPubKey): - # Ignore the error here since the script - # address is checked below. - try: - saddr = ByteArray(decoded.string()) - except Exception: - saddr = test.saddr - - elif isinstance(decoded, crypto.AddressEdwardsPubKey): - # Ignore the error here since the script - # address is checked below. - # saddr = ByteArray(decoded.String()) - self.fail("Edwards sigs unsupported") - - elif isinstance(decoded, crypto.AddressSecSchnorrPubKey): - # Ignore the error here since the script - # address is checked below. - # saddr = ByteArray(decoded.String()) - self.fail("Schnorr sigs unsupported") - - # Check script address, as well as the Hash160 method for P2PKH and - # P2SH addresses. - self.assertEqual(saddr, decoded.scriptAddress(), test.name) - - if isinstance(decoded, crypto.AddressPubKeyHash): - self.assertEqual(decoded.pkHash, saddr) - - if isinstance(decoded, crypto.AddressScriptHash): - self.assertEqual(decoded.hash160(), saddr) - - if not test.valid: - # If address is invalid, but a creation function exists, - # verify that it returns a nil addr and non-nil error. - if test.f != None: - try: - test.f() - self.fail("%s: address is invalid but creating new address succeeded" % test.name) - except Exception: - pass - continue - - # Valid test, compare address created with f against expected result. - try: - addr = test.f() - except Exception as e: - self.fail("%s: address is valid but creating new address failed with error %s", test.name, e) - self.assertEqual(addr.scriptAddress(), test.scriptAddress, test.name) - - def test_extract_script_addrs(self): - from tinydecred.pydecred import mainnet - scriptVersion = 0 - tests = [] - def pkAddr(b): - addr = crypto.AddressSecpPubKey(b, mainnet) - # force the format to compressed, as per golang tests. - addr.pubkeyFormat = crypto.PKFCompressed - return addr - - class test: - def __init__(self, name="", script=b'', addrs=None, reqSigs=-1, scriptClass=-1, exception=None): - self.name = name - self.script = script - self.addrs = addrs if addrs else [] - self.reqSigs = reqSigs - self.scriptClass = scriptClass - self.exception = exception - tests.append(test( - name = "standard p2pk with compressed pubkey (0x02)", - script = ByteArray("2102192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4ac"), - addrs = [pkAddr(ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"))], - reqSigs = 1, - scriptClass = PubKeyTy, - )) - tests.append(test( - name = "standard p2pk with uncompressed pubkey (0x04)", - script = ByteArray("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" - "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), - addrs = [ - pkAddr(ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482eca" - "d7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3")), - ], - reqSigs = 1, - scriptClass = PubKeyTy, - )) - tests.append(test( - name = "standard p2pk with compressed pubkey (0x03)", - script = ByteArray("2103b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65ac"), - addrs = [pkAddr(ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"))], - reqSigs = 1, - scriptClass = PubKeyTy, - )) - tests.append(test( - name = "2nd standard p2pk with uncompressed pubkey (0x04)", - script = ByteArray("4104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782" - "eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac"), - addrs = [ - pkAddr(ByteArray("04b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2" - "c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b")), - ], - reqSigs = 1, - scriptClass = PubKeyTy, - )) - tests.append(test( - name = "standard p2pkh", - script = ByteArray("76a914ad06dd6ddee55cbca9a9e3713bd7587509a3056488ac"), - addrs = [crypto.newAddressPubKeyHash(ByteArray("ad06dd6ddee55cbca9a9e3713bd7587509a30564"), mainnet, crypto.STEcdsaSecp256k1)], - reqSigs = 1, - scriptClass = PubKeyHashTy, - )) - tests.append(test( - name = "standard p2sh", - script = ByteArray("a91463bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb87"), - addrs = [crypto.newAddressScriptHashFromHash(ByteArray("63bcc565f9e68ee0189dd5cc67f1b0e5f02f45cb"), mainnet)], - reqSigs = 1, - scriptClass = ScriptHashTy, - )) - # from real tx 60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1, vout 0 - tests.append(test( - name = "standard 1 of 2 multisig", - script = ByteArray("514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a47" - "3e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4410461cbdcc5409fb4b4d42b51d3338" - "1354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af52ae"), - addrs = [ - pkAddr(ByteArray("04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a" - "1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4")), - pkAddr(ByteArray("0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34b" - "fa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af")), - ], - reqSigs = 1, - scriptClass = MultiSigTy, - )) - # from real tx d646f82bd5fbdb94a36872ce460f97662b80c3050ad3209bef9d1e398ea277ab, vin 1 - tests.append(test( - name = "standard 2 of 3 multisig", - script = ByteArray("524104cb9c3c222c5f7a7d3b9bd152f363a0b6d54c9eb312c4d4f9af1e8551b6c421a6a4ab0e2" - "9105f24de20ff463c1c91fcf3bf662cdde4783d4799f787cb7c08869b4104ccc588420deeebea22a7e900cc8" - "b68620d2212c374604e3487ca08f1ff3ae12bdc639514d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e" - "71e610b036aa24104ab47ad1939edcb3db65f7fedea62bbf781c5410d3f22a7a3a56ffefb2238af8627363bd" - "f2ed97c1f89784a1aecdb43384f11d2acc64443c7fc299cef0400421a53ae"), - addrs = [ - pkAddr(ByteArray("04cb9c3c222c5f7a7d3b9bd152f363a0b6d54c9eb312c4d4f9af" - "1e8551b6c421a6a4ab0e29105f24de20ff463c1c91fcf3bf662cdde4783d4799f787cb7c08869b")), - pkAddr(ByteArray("04ccc588420deeebea22a7e900cc8b68620d2212c374604e3487" - "ca08f1ff3ae12bdc639514d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e71e610b036aa2")), - pkAddr(ByteArray("04ab47ad1939edcb3db65f7fedea62bbf781c5410d3f22a7a3a5" - "6ffefb2238af8627363bdf2ed97c1f89784a1aecdb43384f11d2acc64443c7fc299cef0400421a")), - ], - reqSigs = 2, - scriptClass = MultiSigTy, - )) - - # The below are nonstandard script due to things such as - # invalid pubkeys, failure to parse, and not being of a - # standard form. - - tests.append(test( - name = "p2pk with uncompressed pk missing OP_CHECKSIG", - script = ByteArray("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" - "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), - addrs = [], - exception = "unsupported script", - )) - tests.append(test( - name = "valid signature from a sigscript - no addresses", - script = ByteArray("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd41022" - "0181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), - addrs = [], - exception = "unsupported script", - )) - # Note the technically the pubkey is the second item on the - # stack, but since the address extraction intentionally only - # works with standard PkScripts, this should not return any - # addresses. - tests.append(test( - name = "valid sigscript to redeem p2pk - no addresses", - script = ByteArray("493046022100ddc69738bf2336318e4e041a5a77f305da87428ab1606f023260017854350ddc0" - "22100817af09d2eec36862d16009852b7e3a0f6dd76598290b7834e1453660367e07a014104cd4240c198e12" - "523b6f9cb9f5bed06de1ba37e96a1bbd13745fcf9d11c25b1dff9a519675d198804ba9962d3eca2d5937d58e5a75a71042d40388a4d307f887d"), - addrs = [], - reqSigs = 0, - exception = "unsupported script", - )) - # adapted from btc: - # tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 0 - # invalid public keys - tests.append(test( - name = "1 of 3 multisig with invalid pubkeys", - script = ByteArray("5141042200007353455857696b696c65616b73204361626c6567617465204261636b75700a0a6" - "361626c65676174652d3230313031323034313831312e377a0a0a446f41046e6c6f61642074686520666f6c6" - "c6f77696e67207472616e73616374696f6e732077697468205361746f736869204e616b616d6f746f2773206" - "46f776e6c6f61410420746f6f6c2077686963680a63616e20626520666f756e6420696e207472616e7361637" - "4696f6e2036633533636439383731313965663739376435616463636453ae"), - addrs = [], - exception = "isn't on secp256k1 curve", - )) - # adapted from btc: - # tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 44 - # invalid public keys - tests.append(test( - name = "1 of 3 multisig with invalid pubkeys 2", - script = ByteArray("514104633365633235396337346461636536666430383862343463656638630a6336366263313" - "9393663386239346133383131623336353631386665316539623162354104636163636539393361333938386" - "134363966636336643664616266640a323636336366613963663463303363363039633539336333653931666" - "56465373032392102323364643432643235363339643338613663663530616234636434340a00000053ae"), - addrs = [], - exception = "isn't on secp256k1 curve", - )) - tests.append(test( - name = "empty script", - script = ByteArray(b''), - addrs = [], - reqSigs = 0, - exception = "unsupported script", - )) - tests.append(test( - name = "script that does not parse", - script = ByteArray([opcode.OP_DATA_45]), - addrs = [], - reqSigs = 0, - exception = "unsupported script", - )) - - def checkAddrs(a, b, name): - if len(a) != len(b): - t.fail("extracted address length mismatch. expected %d, got %d" % (len(a), len(b))) - for av, bv in zip(a, b): - if av.scriptAddress() != bv.scriptAddress(): - self.fail("scriptAddress mismatch. expected %s, got %s (%s)" % - (av.scriptAddress().hex(), bv.scriptAddress().hex(), name)) - - for i, t in enumerate(tests): - try: - scriptClass, addrs, reqSigs = extractPkScriptAddrs(scriptVersion, t.script, mainnet) - except Exception as e: - if t.exception and t.exception in str(e): - continue - self.fail("extractPkScriptAddrs #%d (%s): %s" % (i, t.name, e)) - - self.assertEqual(scriptClass, t.scriptClass, t.name) - - self.assertEqual(reqSigs, t.reqSigs, t.name) - - checkAddrs(t.addrs, addrs, t.name) + Returns: + int: The opcode tag for the script types parsed from the script. + """ + scriptClass = getScriptClass(DefaultScriptVersion, pkScript) + if scriptClass == NonStandardTy: + raise Exception("unknown script class") + if scriptClass == StakeSubmissionTy: + return opcode.OP_SSTX + elif scriptClass == StakeGenTy: + return opcode.OP_SSGEN + elif scriptClass == StakeRevocationTy: + return opcode.OP_SSRTX + elif scriptClass == StakeSubChangeTy: + return opcode.OP_SSTXCHANGE + return opNonstake + +def spendScriptSize(pkScript): + # Unspent credits are currently expected to be either P2PKH or + # P2PK, P2PKH/P2SH nested in a revocation/stakechange/vote output. + scriptClass = getScriptClass(DefaultScriptVersion, pkScript) + if scriptClass == PubKeyHashTy: + return RedeemP2PKHSigScriptSize + elif scriptClass == PubKeyTy: + return RedeemP2PKSigScriptSize + elif scriptClass in (StakeRevocationTy, StakeSubChangeTy, StakeGenTy): + scriptClass = getStakeOutSubclass(pkScript) + # For stake transactions we expect P2PKH and P2SH script class + # types only but ignore P2SH script type since it can pay + # to any script which the wallet may not recognize. + if scriptClass != PubKeyHashTy: + raise Exception("unexpected nested script class for credit: %d" % scriptClass) + return RedeemP2PKHSigScriptSize + raise Exception("unimplemented") + +def estimateInputSize(scriptSize): + """ + estimateInputSize returns the worst case serialize size estimate for a tx input + - 32 bytes previous tx + - 4 bytes output index + - 1 byte tree + - 8 bytes amount + - 4 bytes block height + - 4 bytes block index + - the compact int representation of the script size + - the supplied script size + - 4 bytes sequence + + Args: + scriptSize int: Byte-length of the script. + + Returns: + int: Estimated size of the byte-encoded transaction input. + """ + return 32 + 4 + 1 + 8 + 4 + 4 + wire.varIntSerializeSize(scriptSize) + scriptSize + 4 + +def estimateOutputSize(scriptSize): + """ + estimateOutputSize returns the worst case serialize size estimate for a tx output + - 8 bytes amount + - 2 bytes version + - the compact int representation of the script size + - the supplied script size + + Args: + scriptSize int: Byte-length of the script. + + Returns: + int: Estimated size of the byte-encoded transaction output. + """ + return 8 + 2 + wire.varIntSerializeSize(scriptSize) + scriptSize + +def sumOutputSerializeSizes(outputs): # outputs []*wire.TxOut) (serializeSize int) { + """ + sumOutputSerializeSizes sums up the serialized size of the supplied outputs. + + Args: + outputs list(TxOut): Transaction outputs. + + Returns: + int: Estimated size of the byte-encoded transaction outputs. + """ + serializeSize = 0 + for txOut in outputs: + serializeSize += txOut.serializeSize() + return serializeSize + +def estimateSerializeSize(scriptSizes, txOuts, changeScriptSize): + """ + estimateSerializeSize returns a worst case serialize size estimate for a + signed transaction that spends a number of outputs and contains each + transaction output from txOuts. The estimated size is incremented for an + additional change output if changeScriptSize is greater than 0. Passing 0 + does not add a change output. + + Args: + scriptSizes list(int): Pubkey script sizes + txOuts list(TxOut): Transaction outputs. + changeScriptSize int: Size of the change script. + + Returns: + int: Estimated size of the byte-encoded transaction outputs. + """ + # Generate and sum up the estimated sizes of the inputs. + txInsSize = 0 + for size in scriptSizes: + txInsSize += estimateInputSize(size) + + inputCount = len(scriptSizes) + outputCount = len(txOuts) + changeSize = 0 + if changeScriptSize > 0: + changeSize = estimateOutputSize(changeScriptSize) + outputCount += 1 + # 12 additional bytes are for version, locktime and expiry. + return (12 + (2 * wire.varIntSerializeSize(inputCount)) + + wire.varIntSerializeSize(outputCount) + + txInsSize + + sumOutputSerializeSizes(txOuts) + + changeSize) + +def calcMinRequiredTxRelayFee(relayFeePerKb, txSerializeSize): + """ + calcMinRequiredTxRelayFee returns the minimum transaction fee required for a + transaction with the passed serialized size to be accepted into the memory + pool and relayed. + + Args: + relayFeePerKb (float): The fee per kilobyte. + txSerializeSize int: (Size) of the byte-encoded transaction. + + Returns: + int: Fee in atoms. + """ + # Calculate the minimum fee for a transaction to be allowed into the + # mempool and relayed by scaling the base fee (which is the minimum + # free transaction relay fee). minTxRelayFee is in Atom/KB, so + # multiply by serializedSize (which is in bytes) and divide by 1000 to + # get minimum Atoms. + fee = relayFeePerKb * txSerializeSize / 1000 + + if fee == 0 and relayFeePerKb > 0: + fee = relayFeePerKb + + if fee < 0 or fee > MaxAmount: # dcrutil.MaxAmount: + fee = MaxAmount + return round(fee) + + +def isDustAmount(amount, scriptSize, relayFeePerKb): #amount dcrutil.Amount, scriptSize int, relayFeePerKb dcrutil.Amount) bool { + """ + isDustAmount determines whether a transaction output value and script length would + cause the output to be considered dust. Transactions with dust outputs are + not standard and are rejected by mempools with default policies. + + Args: + amount (int): Atoms. + scriptSize (int): Byte-size of the script. + relayFeePerKb (float): Fees paid per kilobyte. + + Returns: + bool: True if the amount is considered dust. + """ + # Calculate the total (estimated) cost to the network. This is + # calculated using the serialize size of the output plus the serial + # size of a transaction input which redeems it. The output is assumed + # to be compressed P2PKH as this is the most common script type. Use + # the average size of a compressed P2PKH redeem input (165) rather than + # the largest possible (txsizes.RedeemP2PKHInputSize). + totalSize = 8 + 2 + wire.varIntSerializeSize(scriptSize) + scriptSize + 165 + + # Dust is defined as an output value where the total cost to the network + # (output size + input size) is greater than 1/3 of the relay fee. + return amount*1000/(3*totalSize) < relayFeePerKb + +def isUnspendable(amount, pkScript): + """ + isUnspendable returns whether the passed public key script is unspendable, or + guaranteed to fail at execution. This allows inputs to be pruned instantly + when entering the UTXO set. In Decred, all zero value outputs are unspendable. + + NOTE: This function is only valid for version 0 scripts. Since the function + does not accept a script version, the results are undefined for other script + versions. + """ + # The script is unspendable if starts with OP_RETURN or is guaranteed to + # fail at execution due to being larger than the max allowed script size. + if (amount == 0 or len(pkScript) > MaxScriptSize or len(pkScript) > 0 and + pkScript[0] == opcode.OP_RETURN): + + return True + + # The script is unspendable if it is guaranteed to fail at execution. + scriptVersion = 0 + return checkScriptParses(scriptVersion, pkScript) != None + +def isDustOutput(output, relayFeePerKb): + """ + isDustOutput determines whether a transaction output is considered dust. + Transactions with dust outputs are not standard and are rejected by mempools + with default policies. + """ + # Unspendable outputs which solely carry data are not checked for dust. + if getScriptClass(output.version, output.pkScript) == NullDataTy: + return False + + # All other unspendable outputs are considered dust. + if isUnspendable(output.value, output.pkScript): + return True + + return isDustAmount(output.value, len(output.pkScript), relayFeePerKb) + +def estimateSerializeSizeFromScriptSizes(inputSizes, outputSizes, changeScriptSize): + """ + estimateSerializeSizeFromScriptSizes returns a worst case serialize size + estimate for a signed transaction that spends len(inputSizes) previous + outputs and pays to len(outputSizes) outputs with scripts of the provided + worst-case sizes. The estimated size is incremented for an additional + change output if changeScriptSize is greater than 0. Passing 0 does not + add a change output. + """ + # Generate and sum up the estimated sizes of the inputs. + txInsSize = 0 + for inputSize in inputSizes: + txInsSize += estimateInputSize(inputSize) + + # Generate and sum up the estimated sizes of the outputs. + txOutsSize = 0 + for outputSize in outputSizes: + txOutsSize += estimateOutputSize(outputSize) + + inputCount = len(inputSizes) + outputCount = len(outputSizes) + changeSize = 0 + if changeScriptSize > 0: + changeSize = estimateOutputSize(changeScriptSize) + outputCount += 1 + + # 12 additional bytes are for version, locktime and expiry. + return (12 + (2 * varIntSerializeSize(inputCount)) + + varIntSerializeSize(outputCount) + txInsSize + txOutsSize + changeSize) + +def stakePoolTicketFee(stakeDiff, relayFee, height, poolFee, subsidyCache, params): + """ + stakePoolTicketFee determines the stake pool ticket fee for a given ticket + from the passed percentage. Pool fee as a percentage is truncated from 0.01% + to 100.00%. This all must be done with integers. + See the included doc.go of this package for more information about the + calculation of this fee. + """ + # Shift the decimal two places, e.g. 1.00% + # to 100. This assumes that the proportion + # is already multiplied by 100 to give a + # percentage, thus making the entirety + # be a multiplication by 10000. + poolFeeAbs = math.floor(poolFee * 100.0) + poolFeeInt = int(poolFeeAbs) + + # Subsidy is fetched from the blockchain package, then + # pushed forward a number of adjustment periods for + # compensation in gradual subsidy decay. Recall that + # the average time to claiming 50% of the tickets as + # votes is the approximately the same as the ticket + # pool size (params.TicketPoolSize), so take the + # ceiling of the ticket pool size divided by the + # reduction interval. + adjs = int(math.ceil(params.TicketPoolSize / params.SubsidyReductionInterval)) + subsidy = subsidyCache.calcStakeVoteSubsidy(height) + for i in range(adjs): + subsidy *= 100 + subsidy = subsidy // 101 + + # The numerator is (p*10000*s*(v+z)) << 64. + shift = 64 + s = subsidy + v = int(stakeDiff) + z = int(relayFee) + num = poolFeeInt + num *= s + vPlusZ = v + z + num *= vPlusZ + num = num << shift + + # The denominator is 10000*(s+v). + # The extra 10000 above cancels out. + den = s + den += v + den *= 10000 + + # Divide and shift back. + num = num // den + num = num >> shift + + return num + +def sstxNullOutputAmounts(amounts, changeAmounts, amountTicket): + """ + sstxNullOutputAmounts takes an array of input amounts, change amounts, and a + ticket purchase amount, calculates the adjusted proportion from the purchase + amount, stores it in an array, then returns the array. That is, for any given + SStx, this function calculates the proportional outputs that any single user + should receive. + Returns: (1) Fees (2) Output Amounts + """ + lengthAmounts = len(amounts) + + if lengthAmounts != len(changeAmounts): + raise Exception("amounts was not equal in length to change amounts!") + + if amountTicket <= 0: + raise Exception("committed amount was too small!") + + contribAmounts = [] + total = 0 + + # Now we want to get the adjusted amounts. The algorithm is like this: + # 1 foreach amount + # 2 subtract change from input, store + # 3 add this amount to total + # 4 check total against the total committed amount + for i in range(lengthAmounts): + contrib = amounts[i] - changeAmounts[i] + if contrib < 0: + raise Exception("change at idx %d spent more coins than allowed (have: %r, spent: %r)" % (i, amounts[i], changeAmounts[i])) + total += contrib + contribAmounts.append(contrib) + + fees = total - amountTicket + + return fees, contribAmounts + +def makeTicket(params, inputPool, inputMain, addrVote, addrSubsidy, ticketCost, addrPool): + """ + makeTicket creates a ticket from a split transaction output. It can optionally + create a ticket that pays a fee to a pool if a pool input and pool address are + passed. + """ + + mtx = msgtx.MsgTx.new() + + if not addrPool or not inputPool: + raise Exception("solo tickets not supported") + + txIn = msgtx.TxIn(previousOutPoint=inputPool.op, valueIn=inputPool.amt) + mtx.addTxIn(txIn) + + txIn = msgtx.TxIn(previousOutPoint=inputMain.op, valueIn=inputMain.amt) + mtx.addTxIn(txIn) + + # Create a new script which pays to the provided address with an + # SStx tagged output. + if not addrVote: + raise Exception("no voting address provided") + + pkScript = payToSStx(addrVote) + + txOut = msgtx.TxOut( + value = ticketCost, + pkScript = pkScript, + ) + mtx.addTxOut(txOut) + + # Obtain the commitment amounts. + _, amountsCommitted = sstxNullOutputAmounts([inputPool.amt, inputMain.amt], [0, 0], ticketCost) + userSubsidyNullIdx = 1 + + # Zero value P2PKH addr. + zeroed = ByteArray(b'', length=20) + addrZeroed = crypto.newAddressPubKeyHash(zeroed, params, crypto.STEcdsaSecp256k1) + + # 2. Make an extra commitment to the pool. + limits = defaultTicketFeeLimits + pkScript = generateSStxAddrPush(addrPool, amountsCommitted[0], limits) + txout = msgtx.TxOut( + value = 0, + pkScript = pkScript, + ) + mtx.addTxOut(txout) + + # Create a new script which pays to the provided address with an + # SStx change tagged output. + pkScript = payToSStxChange(addrZeroed) + + txOut = msgtx.TxOut( + value = 0, + pkScript = pkScript, + ) + mtx.addTxOut(txOut) + + # 3. Create the commitment and change output paying to the user. + # + # Create an OP_RETURN push containing the pubkeyhash to send rewards to. + # Apply limits to revocations for fees while not allowing + # fees for votes. + pkScript = generateSStxAddrPush(addrSubsidy, amountsCommitted[userSubsidyNullIdx], limits) + txout = msgtx.TxOut( + value = 0, + pkScript = pkScript, + ) + mtx.addTxOut(txout) + + # Create a new script which pays to the provided address with an + # SStx change tagged output. + pkScript = payToSStxChange(addrZeroed) + txOut = msgtx.TxOut( + value = 0, + pkScript = pkScript, + ) + mtx.addTxOut(txOut) + + # Make sure we generated a valid SStx. + checkSStx(mtx) + + return mtx \ No newline at end of file diff --git a/pydecred/wire/msgtx.py b/pydecred/wire/msgtx.py index 52b8c643..714f6b9e 100644 --- a/pydecred/wire/msgtx.py +++ b/pydecred/wire/msgtx.py @@ -380,10 +380,10 @@ def __eq__(self, tx): self.lockTime == tx.lockTime and self.expiry == tx.expiry ) - def addTxIn(self, tx): - self.txIn.append(tx) - def addTxOut(self, tx): - self.txOut.append(tx) + def addTxIn(self, txin): + self.txIn.append(txin) + def addTxOut(self, txout): + self.txOut.append(txout) def hash(self): # chainhash.Hash { """ TxHash generates the hash for the transaction prefix. Since it does not diff --git a/util/http.py b/util/http.py new file mode 100644 index 00000000..67c2341c --- /dev/null +++ b/util/http.py @@ -0,0 +1,41 @@ +""" +Copyright (c) 2019, Brian Stafford +See LICENSE for details + +DcrdataClient.endpointList() for available enpoints. +""" +import urllib.request as urlrequest +from urllib.parse import urlencode +from tinydecred.util import tinyjson +from tinydecred.util.helpers import formatTraceback + +def get(uri, **kwargs): + return request(uri, **kwargs) + +def post(uri, data, **kwargs): + return request(uri, data, **kwargs) + +def request(uri, postData=None, headers=None, urlEncode=False): + try: + headers = headers if headers else {} + if postData: + if urlEncode: + # encode the data in url query string form, without ?. + encoded = urlencode(postData).encode("utf-8") + else: + # encode the data as json. + encoded = tinyjson.dump(postData).encode("utf-8") + req = urlrequest.Request(uri, headers=headers, data=encoded) + else: + req = urlrequest.Request(uri, headers=headers, method="GET") + raw = urlrequest.urlopen(req).read().decode() + try: + # try to decode the response as json, but fall back to just + # returning the string. + return tinyjson.load(raw) + except tinyjson.JSONDecodeError: + return raw + except Exception as e: + raise Exception("JSONError", "Failed to decode server response from path %s: %s : %s" % (uri, raw, formatTraceback(e))) + except Exception as e: + raise Exception("RequestError", "Error encountered in requesting path %s: %s" % (uri, formatTraceback(e))) \ No newline at end of file diff --git a/wallet.py b/wallet.py index e8dff775..829c074a 100644 --- a/wallet.py +++ b/wallet.py @@ -19,9 +19,9 @@ class KeySource(object): """ Implements the KeySource API from tinydecred.api. """ - def __init__(self, priv, change): + def __init__(self, priv, internal): self.priv = priv - self.change = change + self.internal = internal class Wallet(object): """ @@ -440,7 +440,7 @@ def sendToAddress(self, value, address, feeRate=None): acct = self.openAccount keysource = KeySource( priv = self.getKey, - change = acct.getChangeAddress, + internal = acct.getChangeAddress, ) tx, spentUTXOs, newUTXOs = self.blockchain.sendToAddress(value, address, keysource, self.getUTXOs, feeRate) acct.addMempoolTx(tx) From 5f12e35c858bc6b14019abdf8916afb65f6628ba Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 17 Sep 2019 11:40:54 -0500 Subject: [PATCH 03/12] vsp and ticket screens for gui. pay-to-stake-pubkey-hash script signing Updated interface to support ticket purchases via VSP. No solo voting and still needs revocation support, but signing stake-P2PKH outputs of all types is now supported. --- accounts.py | 40 +++- app.py | 9 +- config.py | 2 +- crypto/crypto.py | 25 -- pydecred/account.py | 269 +++++++++++++++++++++ pydecred/dcrdata.py | 201 +++++++++++----- pydecred/stakepool.py | 179 +++++++++++--- pydecred/tests.py | 78 +++++-- pydecred/txscript.py | 92 ++++++-- pydecred/wire/msgtx.py | 2 +- ui/qutilities.py | 162 ++----------- ui/screens.py | 514 ++++++++++++++++++++++++++++++++++++++++- ui/ui.py | 3 +- util/http.py | 5 +- util/tinyjson.py | 10 +- wallet.py | 69 +----- 16 files changed, 1294 insertions(+), 366 deletions(-) create mode 100644 pydecred/account.py diff --git a/accounts.py b/accounts.py index 38d99310..e5f404e2 100644 --- a/accounts.py +++ b/accounts.py @@ -9,7 +9,6 @@ The tinycrypto package relies heavily on the lower-level crypto modules. """ import unittest -import hashlib from tinydecred.util import tinyjson, helpers from tinydecred import api from tinydecred.pydecred import nets, constants as DCR @@ -235,8 +234,9 @@ def __tojson__(self): "balance": self.balance, } @staticmethod - def __fromjson__(obj): - acct = Account( + def __fromjson__(obj, cls=None): + cls = cls if cls else Account + acct = cls( obj["pubKeyEncrypted"], obj["privKeyEncrypted"], obj["name"], @@ -374,6 +374,30 @@ def resolveUTXOs(self, blockchainUTXOs): to. """ self.utxos = {u.key(): u for u in blockchainUTXOs} + def getUTXOs(self, requested, approve=None): + """ + Find confirmed and mature UTXOs, smallest first, that sum to the + requested amount, in atoms. + + Args: + requested (int): Required amount in atoms. + filter (func(UTXO) -> bool): Optional UTXO filtering function. + + Returns: + list(UTXO): A list of UTXOs. + bool: True if the UTXO sum is >= the requested amount. + """ + matches = [] + collected = 0 + pairs = [(u.satoshis, u) for u in self.utxoscan()] + for v, utxo in sorted(pairs, key=lambda p: p[0]): + if approve and not approve(utxo): + continue + matches.append(utxo) + collected += v + if collected >= requested: + break + return matches, collected >= requested def spendTxidVout(self, txid, vout): """ Spend the UTXO. The UTXO is removed from the watched list and returned. @@ -536,7 +560,7 @@ def addressesOfInterest(self): ext = self.externalAddresses for i in range(max(self.cursor - 10, 0), self.cursor+1): a.add(ext[i]) - return a + return list(a) def paymentAddress(self): """ Get the external address at the cursor. The cursor is not moved. @@ -800,7 +824,8 @@ def acctPublicKey(self, acct, net, pw): tinyjson.register(AccountManager) -def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams): + +def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams, constructor=None): """ Create a new account manager and a set of BIP0044 keys for creating accounts. The zeroth account is created for the provided network parameters. @@ -817,6 +842,7 @@ def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams): Returns: AccountManager: An initialized account manager. """ + constructor = constructor if constructor else Account # Ensure the private passphrase is not empty. if len(privPassphrase) == 0: @@ -899,12 +925,12 @@ def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams): acctPrivSLIP0044Enc = cryptoKeyPriv.encrypt(apes.encode()) # Derive the default account from the legacy coin type. - baseAccount = Account(acctPubLegacyEnc, acctPrivLegacyEnc, + baseAccount = constructor(acctPubLegacyEnc, acctPrivLegacyEnc, DEFAULT_ACCOUNT_NAME, CoinSymbols.decred, chainParams.Name) # Save the account row for the 0th account derived from the coin type # 42 key. - zerothAccount = Account(acctPubSLIP0044Enc, acctPrivSLIP0044Enc, + zerothAccount = constructor(acctPubSLIP0044Enc, acctPrivSLIP0044Enc, DEFAULT_ACCOUNT_NAME, CoinSymbols.decred, chainParams.Name) # Open the account. zerothAccount.open(cryptoKeyPriv) diff --git a/app.py b/app.py index 94e31268..6ab8a33c 100644 --- a/app.py +++ b/app.py @@ -13,7 +13,6 @@ from tinydecred.pydecred import constants as DCR from tinydecred.pydecred.dcrdata import DcrdataBlockchain from tinydecred.wallet import Wallet -from tinydecred.crypto import crypto from tinydecred.ui import screens, ui, qutilities as Q # the directory of the tinydecred package @@ -128,6 +127,8 @@ def __init__(self, qApp): self.sendScreen = screens.SendScreen(self) + self.confirmScreen = screens.ConfirmScreen(self) + self.sysTray.show() self.appWindow.show() @@ -382,6 +383,11 @@ def step2(pw, a, k): self.emitSignal(ui.DONE_SIGNAL) return False self.getPassword(step1, cb, a, k) + def confirm(self, msg, cb): + """ + Call the callback function only if the user confirms the prompt. + """ + self.appWindow.stack(self.confirmScreen.withPurpose(msg, cb)) def tryInitSync(self): """ If conditions are right, start syncing the wallet. @@ -428,6 +434,7 @@ def _setDCR(self, res): if not res: self.appWindow.showError("No dcrdata connection available.") return + self.emitSignal(ui.BLOCKCHAIN_CONNECTED) self.tryInitSync() def getButton(self, size, text, tracked=True): """ diff --git a/config.py b/config.py index de6bfd05..f9cb8732 100644 --- a/config.py +++ b/config.py @@ -126,7 +126,7 @@ def save(self): """ Save the file. """ - tinyjson.save(CONFIG_PATH, self.file) + tinyjson.save(CONFIG_PATH, self.file, indent=4, sort_keys=True) tinyConfig = TinyConfig() diff --git a/crypto/crypto.py b/crypto/crypto.py index 543e31af..6cb7bfad 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -460,31 +460,6 @@ def __init__(self, privVer, pubVer, key, pubKey, chainCode, parentFP, depth, chi self.depth = depth self.childNum = childNum self.isPrivate = isPrivate - # def __tojson__(self): - # return { - # "privVer": self.privVer, - # "pubVer": self.pubVer, - # "key": self.key, - # "pubKey": self.pubKey, - # "chainCode": self.chainCode, - # "parentFP": self.parentFP, - # "depth": self.depth, - # "childNum": self.childNum, - # "isPrivate": self.isPrivate, - # } - # @staticmethod - # def __fromjson__(obj): - # return ExtendedKey( - # privVer = obj["privVer"], - # pubVer = obj["pubVer"], - # key = obj["key"], - # pubKey = obj["pubKey"], - # chainCode = obj["chainCode"], - # parentFP = obj["parentFP"], - # depth = obj["depth"], - # childNum = obj["childNum"], - # isPrivate = obj["isPrivate"], - # ) def deriveCoinTypeKey(self, coinType): """ First two hardened child derivations in accordance with BIP0044. diff --git a/pydecred/account.py b/pydecred/account.py new file mode 100644 index 00000000..e0911c5f --- /dev/null +++ b/pydecred/account.py @@ -0,0 +1,269 @@ +""" +Copyright (c) 2019, Brian Stafford +See LICENSE for details + +The DecredAccount inherits from the tinydecred base Account and adds staking +support. +""" + +from tinydecred.accounts import Account, EXTERNAL_BRANCH +from tinydecred.util import tinyjson, helpers +from tinydecred.crypto.crypto import AddressSecpPubKey + +class KeySource(object): + """ + Implements the KeySource API from tinydecred.api. Must provide access to + internal addresses via the KeySource.internal method, and PrivateKeys for a + specified address via the KeySource.priv method. This implementation just + sets the passed functions to class properties with the required method + names. + """ + def __init__(self, priv, internal): + """ + Args: + priv (func): func(address : string) -> PrivateKey. Retrieves the + associated with the specified address. + internal (func): func() -> address : string. Get a new internal + address. + """ + self.priv = priv + self.internal = internal + +class TicketRequest: + """ + The TicketRequest is required to purchase tickets. + """ + def __init__(self, minConf, expiry, spendLimit, poolAddress, votingAddress, ticketFee, poolFees, count, txFee): + # minConf is just a placeholder for now. Account minconf is 0 until + # I add the ability to change it. + self.minConf = minConf + # expiry can be set to some reasonable block height. This may be + # important when approaching the end of a ticket window. + self.expiry = expiry + # Price is calculated purely from the ticket count, price, and fees, but + # cannot go over spendLimit. + self.spendLimit = spendLimit + # The VSP fee payment address. + self.poolAddress = poolAddress + # The P2SH voting address based on the 1-of-2 multi-sig script you share + # with the VSP. + self.votingAddress = votingAddress + # ticketFee is the transaction fee rate to pay the miner for the ticket. + # Set to zero to use wallet's network default fee rate. + self.ticketFee = ticketFee + # poolFees are set by the VSP. If you don't set these correctly, the + # VSP may not vote for you. + self.poolFees = poolFees + # How many tickets to buy. + self.count = count + # txFee is the transaction fee rate to pay the miner for the split + # transaction required to fund the ticket. + # Set to zero to use wallet's network default fee rate. + self.txFee = txFee + + +class TicketStats: + """ + TicketStats is basic information about the account's staking status. + """ + def __init__(self, count=0, value=0): + """ + Args: + count (int): How many tickets the account owns. No differentiation + is made between immature, live, missed, or expired tickets. + value (int): How much value is locked in the tickets counted in + count. + """ + self.count = count + self.value = value + +class DecredAccount(Account): + """ + DecredAccount is the Decred version of the base tinydecred Account. + Decred Account inherits Account, and adds the necessary functionality to + handle staking. + """ + def __init__(self, *a, **k): + """ + All constructor aruments are passed directly to the parent Account. + """ + super().__init__(*a, *k) + self.tickets = [] + self.stakeStats = TicketStats() + self.stakePools = [] + def __tojson__(self): + obj = super().__tojson__() + return helpers.recursiveUpdate(obj, { + "tickets": self.tickets, + "stakePools": self.stakePools, + }) + return obj + @staticmethod + def __fromjson__(obj): + acct = Account.__fromjson__(obj, cls=DecredAccount) + acct.tickets = obj["tickets"] + acct.stakePools = obj["stakePools"] + return acct + def updateStakeStats(self): + """ + Updates the stake stats object. + """ + ticketCount = 0 + ticketVal = 0 + for utxo in self.utxos.values(): + if utxo.isTicket(): + ticketCount += 1 + ticketVal += utxo.satoshis + self.tickets.append(utxo.txid) + self.stakeStats = TicketStats(ticketCount, ticketVal) + + def resolveUTXOs(self, blockchainUTXOs): + """ + resolveUTXOs is run once at the end of a sync. Using this opportunity + to hook into the sync to authorize the stake pool. + + Args: + blockchainUTXOs (list(obj)): A list of Python objects decoded from + dcrdata's JSON response from ...addr/utxo endpoint. + """ + super().resolveUTXOs(blockchainUTXOs) + self.updateStakeStats() + pool = self.stakePool() + if pool: + pool.authorize(self.votingAddress(), self.net) + def addUTXO(self, utxo): + """ + Add the UTXO. Update the stake stats if this is a ticket. + """ + super().addUTXO(utxo) + if utxo.isTicket(): + self.updateStakeStats() + def addTicketAddresses(self, a): + """ + Add the ticket voting addresses from each known stake pool. + + Args: + a (list(string)): The ticket addresses will be appended to this + list. + """ + for pool in self.stakePools: + if pool.purchaseInfo: + a.append(pool.purchaseInfo.ticketAddress) + return a + def allAddresses(self): + """ + Overload the base class to add the voting address. + """ + return self.addTicketAddresses(super().allAddresses()) + def addressesOfInterest(self): + """ + Overload the base class to add the voting address. + """ + return self.addTicketAddresses(super().addressesOfInterest()) + def votingKey(self): + """ + This may change, but for now, the voting key is from the zeroth + child of the zeroth child of the external branch. + """ + return self.privKey.child(EXTERNAL_BRANCH).child(0).child(0).privateKey() + def votingAddress(self): + """ + The voting address is the pubkey address (not pubkey-hash) for the + account. Tinydecred defines this as the zeroth child of the zeroth child + of the external branch key. + + Returns: + AddressSecpPubkey: The address object. + """ + return AddressSecpPubKey(self.votingKey().pub.serializeCompressed(), self.net).string() + def setPool(self, pool): + """ + Set the specified pool as the default. + + Args: + pool (stakepool.StakePool): The stake pool object. + """ + self.stakePools = [pool] + [p for p in self.stakePools if p.url != pool.url] + def hasPool(self): + """ + hasPool will return True if the wallet has at least one pool set. + """ + return self.stakePool() != None + def stakePool(self): + """ + stakePool is the default stakepool.StakePool for the account. + + Returns: + staekpool.StakePool: The default stake pool object. + """ + if self.stakePools: + return self.stakePools[0] + return None + def ticketStats(self): + """ + A getter for the stakeStats. + """ + return self.stakeStats + def sendToAddress(self, value, address, feeRate, blockchain): + """ + Send the value to the address. + + Args: + value int: The amount to send, in atoms. + address str: The base-58 encoded pubkey hash. + + Returns: + MsgTx: The newly created transaction on success, `False` on failure. + """ + keysource = KeySource( + priv = self.getPrivKeyForAddress, + internal = self.getChangeAddress, + ) + tx, spentUTXOs, newUTXOs = blockchain.sendToAddress(value, address, keysource, self.getUTXOs, feeRate) + self.addMempoolTx(tx) + self.spendUTXOs(spentUTXOs) + for utxo in newUTXOs: + self.addUTXO(utxo) + return tx + def purchaseTickets(self, qty, price, blockchain): + """ + purchaseTickets completes the purchase of the specified tickets. The + DecredAccount uses the blockchain to do the heavy lifting, but must + prepare the TicketRequest and KeySource and gather some other account- + related information. + """ + keysource = KeySource( + priv = self.getPrivKeyForAddress, + internal = self.getChangeAddress, + ) + pool = self.stakePool() + pi = pool.purchaseInfo + req = TicketRequest( + minConf = 0, + expiry = 0, + spendLimit = int(price*qty*1.1*1e8), # convert to atoms here + poolAddress = pi.poolAddress, + votingAddress = pi.ticketAddress, + ticketFee = 0, # use network default + poolFees = pi.poolFees, + count = qty, + txFee = 0, # use network default + ) + txs, spentUTXOs, newUTXOs = blockchain.purchaseTickets(keysource, self.getUTXOs, req) + if txs: + # Add the split transactions + self.addMempoolTx(txs[0]) + # Add all tickets + for tx in txs[1]: + self.addMempoolTx(tx) + # Store the txids. + self.tickets.extend([tx.txid() for tx in txs[1]]) + # Remove spent utxos from cache. + self.spendUTXOs(spentUTXOs) + # Add new UTXOs to set. These may be replaced with network-sourced + # UTXOs once the wallet receives an update from the BlockChain. + for utxo in newUTXOs: + self.addUTXO(utxo) + return txs[1] + +tinyjson.register(DecredAccount) \ No newline at end of file diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 8799364b..759c162c 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -339,13 +339,66 @@ def __init__(self, name, message): self.name = name self.message = message +class APIBlock: + def __init__(self, blockHash, height): + self.hash = blockHash + self.height = height + @staticmethod + def __fromjson__(obj): + return APIBlock(obj["hash"], obj["height"]) + def __tojson__(self): + return { + "hash": self.blockHash, + "height": self.blockHeight, + } + +tinyjson.register(APIBlock, tag="dcrdata.APIBlock") + +class TicketInfo: + def __init__(self, status, purchaseBlock, maturityHeight, expirationHeight, + lotteryBlock, vote, revocation): + self.status = status + self.purchaseBlock = purchaseBlock + self.maturityHeight = maturityHeight + self.expirationHeight = expirationHeight + self.lotteryBlock = lotteryBlock + self.vote = vote + self.revocation = revocation + @staticmethod + def parse(obj): + return TicketInfo( + status = obj["status"], + purchaseBlock = obj["purchase_block"], + maturityHeight = obj["maturity_height"], + expirationHeight = obj["expiration_height"], + lotteryBlock = obj["lottery_block"], + vote = obj["vote"], + revocation = obj["revocation"], + ) + @staticmethod + def __fromjson__(obj): + return TicketInfo.parse(obj) + def __tojson__(self): + return { + "status": self.status, + "purchase_block": self.purchaseBlock, + "maturity_height": self.maturityHeight, + "expiration_height": self.expirationHeight, + "lottery_block": self.lotteryBlock, + "vote": self.vote, + "revocation": self.revocation, + } + +tinyjson.register(TicketInfo, tag="dcr.TicketInfo") + class UTXO(object): """ The UTXO is part of the wallet API. BlockChains create and parse UTXO objects and fill fields as required by the Wallet. """ def __init__(self, address, txid, vout, ts=None, scriptPubKey=None, - height=-1, amount=0, satoshis=0, maturity=None): + height=-1, amount=0, satoshis=0, maturity=None, + scriptClass=None, tinfo=None): self.address = address self.txid = txid self.vout = vout @@ -355,6 +408,10 @@ def __init__(self, address, txid, vout, ts=None, scriptPubKey=None, self.amount = amount self.satoshis = satoshis self.maturity = maturity + self.scriptClass = scriptClass + if not scriptClass: + self.parseScriptClass() + self.tinfo = tinfo def __tojson__(self): return { @@ -367,28 +424,38 @@ def __tojson__(self): "amount": self.amount, "satoshis": self.satoshis, "maturity": self.maturity, + "scriptClass": self.scriptClass, + "tinfo": self.tinfo, } @staticmethod def __fromjson__(obj): return UTXO.parse(obj) @staticmethod def parse(obj): - return UTXO( + utxo = UTXO( address = obj["address"], txid = obj["txid"], vout = obj["vout"], ts = obj["ts"] if "ts" in obj else None, - scriptPubKey = obj["scriptPubKey"] if "scriptPubKey" in obj else None, + scriptPubKey = ByteArray(obj["scriptPubKey"]), height = obj["height"] if "height" in obj else -1, amount = obj["amount"] if "amount" in obj else 0, satoshis = obj["satoshis"] if "satoshis" in obj else 0, maturity = obj["maturity"] if "maturity" in obj else None, + scriptClass = obj["scriptClass"] if "scriptClass" in obj else None, + tinfo = obj["tinfo"] if "tinfo" in obj else None, ) + return utxo + def parseScriptClass(self): + if self.scriptPubKey: + self.scriptClass = txscript.getScriptClass(0, self.scriptPubKey) def confirm(self, block, tx, params): self.height = block.height self.maturity = block.height + params.CoinbaseMaturity if tx.looksLikeCoinbase() else None self.ts = block.timestamp def isSpendable(self, tipHeight): + if self.isTicket(): + return False if self.maturity: return self.maturity <= tipHeight return True @@ -397,7 +464,15 @@ def key(self): @staticmethod def makeKey(txid, vout): return txid + "#" + str(vout) -tinyjson.register(UTXO) + def setTicketInfo(self, apiTinfo): + self.tinfo = TicketInfo.parse(apiTinfo) + self.maturity = self.tinfo.maturityHeight + def isTicket(self): + return self.scriptClass == txscript.StakeSubmissionTy + def isLiveTicket(self): + return self.tinfo and self.tinfo.status in ("immature", "live") + +tinyjson.register(UTXO, tag="dcr.UTXO") def makeOutputs(pairs, chain): @@ -561,6 +636,14 @@ def processNewUTXO(self, utxo): if tx.looksLikeCoinbase(): # This is a coinbase or stakebase transaction. Set the maturity. utxo.maturity = utxo.height + self.params.CoinbaseMaturity + if utxo.isTicket(): + # Mempool tickets will be returned from the utxo endpoint, but + # the tinfo endpoint is an error until mined. + try: + rawTinfo = self.dcrdata.tx.tinfo(utxo.txid) + utxo.setTicketInfo(rawTinfo) + except: + utxo.tinfo = TicketInfo("mempool", None, -1, -1, None, 0, None) return utxo def UTXOs(self, addrs): """ @@ -716,7 +799,7 @@ def bestBlock(self): """ return self.dcrdata.block.best() def stakeDiff(self): - return self.dcrdata.stake.diff() + return self.dcrdata.stake.diff()["next"] def updateTip(self): """ Update the tip block. If the wallet is subscribed to block updates, @@ -819,6 +902,9 @@ def approveUTXO(self, utxo): # If the UTXO appears unconfirmed, see if it can be confirmed. if utxo.maturity and self.tip["height"] < utxo.maturity: return False + if utxo.isTicket(): + # Temporary until revocations implemented. + return False return True def confirmUTXO(self, utxo, block=None, tx=None): if not tx: @@ -869,7 +955,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf changeScriptSize = txscript.P2PKHPkScriptSize relayFeePerKb = feeRate * 1e3 if feeRate else self.relayFee() - for txout in outputs: + for (i, txout) in enumerate(outputs): checkOutput(txout, relayFeePerKb) signedSize = txscript.estimateSerializeSize([txscript.RedeemP2PKHSigScriptSize], outputs, changeScriptSize) @@ -942,8 +1028,8 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf pkScript = scripts[i] sigScript = txin.signatureScript scriptClass, addrs, numAddrs = txscript.extractPkScriptAddrs(0, pkScript, self.params) - privKey = keysource.priv(addrs[0].string()) - script = txscript.signTxOutput(privKey, self.params, newTx, i, pkScript, txscript.SigHashAll, sigScript, crypto.STEcdsaSecp256k1) + script = txscript.signTxOutput(self.params, newTx, i, pkScript, + txscript.SigHashAll, keysource, sigScript, crypto.STEcdsaSecp256k1) txin.signatureScript = script self.broadcast(newTx.txHex()) if change: @@ -961,17 +1047,19 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf def purchaseTickets(self, keysource, utxosource, req): """ + Based on dcrwallet (*Wallet).purchaseTickets. purchaseTickets indicates to the wallet that a ticket should be purchased using all currently available funds. The ticket address parameter in the request can be nil in which case the ticket address associated with the - wallet instance will be used. Also, when the spend limit in the request is - greater than or equal to 0, tickets that cost more than that limit will - return an error that not enough funds are available. + wallet instance will be used. Also, when the spend limit in the request + is greater than or equal to 0, tickets that cost more than that limit + will return an error that not enough funds are available. """ - # Ensure the minimum number of required confirmations is positive. self.updateTip() - if req.minConf < 0: - raise Exception("negative minconf") + # account minConf is zero for regular outputs for now. Need to make that + # adjustable. + # if req.minConf < 0: + # raise Exception("negative minconf") # Need a positive or zero expiry that is higher than the next block to # generate. @@ -995,27 +1083,41 @@ def purchaseTickets(self, keysource, utxosource, req): # address this better and prevent address burning. # Calculate the current ticket price. - ticketPrice = int(self.stakeDiff()["next"]*1e8) + ticketPrice = int(self.stakeDiff()*1e8) # Ensure the ticket price does not exceed the spend limit if set. if req.spendLimit > 0 and ticketPrice > req.spendLimit: raise Exception("ticket price %f above spend limit %f" % (ticketPrice, req.spendLimit)) - # Check the pool address from the request. If none exists in the - # request, try to get the global pool address. Then do the same for pool - # fees, but check sanity too. + # Check that pool fees is zero, which will result in invalid zero-valued + # outputs. + if req.poolFees == 0: + raise Exception("no pool fee specified") + + stakeSubmissionPkScriptSize = 0 + + # Check the pool address from the request. if not req.poolAddress: raise Exception("no pool address specified. solo voting not supported") - stakeSubmissionPkScriptSize = 0 + poolAddress = txscript.decodeAddress(req.poolAddress, self.params) + + # Check the passed address from the request. + if not req.votingAddress: + raise Exception("voting address not set in purchaseTickets request") + + # decode the string addresses. This is the P2SH multi-sig script + # address, not the wallets voting address, which is only one of the two + # pubkeys included in the redeem P2SH script. + votingAddress = txscript.decodeAddress(req.votingAddress, self.params) # The stake submission pkScript is tagged by an OP_SSTX. - if isinstance(req.votingAddress, crypto.AddressScriptHash): + if isinstance(votingAddress, crypto.AddressScriptHash): stakeSubmissionPkScriptSize = txscript.P2SHPkScriptSize + 1 - elif isinstance(req.votingAddress, crypto.AddressPubKeyHash) and req.votingAddress.sigType == crypto.STEcdsaSecp256k1: + elif isinstance(votingAddress, crypto.AddressPubKeyHash) and votingAddress.sigType == crypto.STEcdsaSecp256k1: stakeSubmissionPkScriptSize = txscript.P2PKHPkScriptSize + 1 else: - raise Exception("unsupported pool address type %s" % req.votingAddress.__class__.__name__) + raise Exception("unsupported voting address type %s" % votingAddress.__class__.__name__) ticketFeeIncrement = req.ticketFee if ticketFeeIncrement == 0: @@ -1053,6 +1155,8 @@ def purchaseTickets(self, keysource, utxosource, req): # immediately be consumed as tickets. splitTxAddr = keysource.internal() + print("splitTxAddr: %s" % splitTxAddr) + # TODO: Don't reuse addresses # TODO: Consider wrapping. see dcrwallet implementation. splitPkScript = txscript.makePayToAddrScript(splitTxAddr, self.params) @@ -1089,27 +1193,8 @@ def purchaseTickets(self, keysource, utxosource, req): # sendOutputs takes the fee rate in atoms/byte splitTx, splitSpent, internalOutputs = self.sendOutputs(splitOuts, keysource, utxosource, int(txFeeIncrement/1000)) - # // After tickets are created and published, watch for future - # // relevant transactions - # var watchOutPoints []wire.OutPoint - # defer func() { - # err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { - # return w.watchFutureAddresses(ctx, tx) - # }) - # if err != nil { - # log.Errorf("Failed to watch for future addresses after ticket "+ - # "purchases: %v", err) - # } - # if len(watchOutPoints) > 0 { - # err := n.LoadTxFilter(ctx, false, nil, watchOutPoints) - # if err != nil { - # log.Errorf("Failed to watch outpoints: %v", err) - # } - # } - # }() - # Generate the tickets individually. - ticketHashes = [] + tickets = [] for i in range(req.count): # Generate the extended outpoints that we need to use for ticket @@ -1140,20 +1225,11 @@ def purchaseTickets(self, keysource, utxosource, req): pkScript = txOut.pkScript, ) - # If the user hasn't specified a voting address - # to delegate voting to, just use an address from - # this wallet. Check the passed address from the - # request first, then check the ticket address - # stored from the configuation. Finally, generate - # an address. - if not req.votingAddress: - raise Exception("voting address not set in purchaseTickets request") - addrSubsidy = txscript.decodeAddress(keysource.internal(), self.params) # Generate the ticket msgTx and sign it. - ticket = txscript.makeTicket(self.params, eopPool, eop, req.votingAddress, - addrSubsidy, ticketPrice, req.poolAddress) + ticket = txscript.makeTicket(self.params, eopPool, eop, votingAddress, + addrSubsidy, ticketPrice, poolAddress) forSigning = [] eopPoolCredit = txscript.Credit( op = eopPool.op, @@ -1190,7 +1266,18 @@ def purchaseTickets(self, keysource, utxosource, req): raise Exception("high fees detected") self.broadcast(ticket.txHex()) - ticketHash = ticket.hash() - ticketHashes.append(ticketHash) - log.info("published ticket purchase %s" % ticketHash) - return (splitTx, ticket), splitSpent, internalOutputs + tickets.append(ticket) + log.info("published ticket %s" % ticket.txid()) + + # Add a UTXO to the internal outputs list. + txOut = ticket.txOut[0] + internalOutputs.append(UTXO( + address = votingAddress.string(), + txid = ticket.txid(), + vout = 0, # sstx is output 0 + ts = int(time.time()), + scriptPubKey = txOut.pkScript, + amount = txOut.value*1e-8, + satoshis = txOut.value, + )) + return (splitTx, tickets), splitSpent, internalOutputs diff --git a/pydecred/stakepool.py b/pydecred/stakepool.py index d141940b..5468ca1f 100644 --- a/pydecred/stakepool.py +++ b/pydecred/stakepool.py @@ -4,17 +4,34 @@ DcrdataClient.endpointList() for available enpoints. """ -import unittest from tinydecred.util import http, tinyjson from tinydecred.pydecred import txscript -from tinydecred.crypto import crypto, opcode +from tinydecred.crypto import crypto from tinydecred.crypto.bytearray import ByteArray def resultIsSuccess(res): - return res and isinstance(res, object) and "status" in res and res["status"] == "success" + """ + JSON-decoded stake pool responses have a common base structure that enables + a universal success check. + + Args: + res (object): The freshly-decoded-from-JSON response. + + Returns: + bool: True if result fields indicate success. + """ + return res and isinstance(res, object) and "status" in res and res["status"] == "success" class PurchaseInfo(object): + """ + The PurchaseInfo models the response from the 'getpurchaseinfo' endpoint. + This information is required for validating the pool and creating tickets. + """ def __init__(self, pi): + """ + Args: + pi (object): The response from the 'getpurchaseinfo' request. + """ get = lambda k, default=None: pi[k] if k in pi else default self.poolAddress = get("PoolAddress") self.poolFees = get("PoolFees") @@ -39,7 +56,14 @@ def __fromjson__(obj): tinyjson.register(PurchaseInfo) class PoolStats(object): + """ + PoolStats models the response from the 'stats' endpoint. + """ def __init__(self, stats): + """ + Args: + stats (object): The response from the 'stats' request. + """ get = lambda k, default=None: stats[k] if k in stats else default self.allMempoolTix = get("AllMempoolTix") self.apiVersionsSupported = get("APIVersionsSupported") @@ -95,13 +119,31 @@ def __fromjson__(obj): tinyjson.register(PoolStats) class StakePool(object): + """ + A StakePool is a voting service provider, uniquely defined by it's URL. The + StakePool class has methods for interacting with the VSP API. StakePool is + JSON-serializable if used with tinyjson, so can be stored as part of an + Account in the wallet. + """ def __init__(self, url, apiKey): + """ + Args: + url (string): The stake pool URL. + apiKey (string): The API key assigned to the VSP account during + registration. + """ self.url = url + # The network parameters are not JSON-serialized, so must be set during + # a call to StakePool.authorize before using the StakePool. + self.net = None + # The signingAddress (also called a votingAddress in other contexts) is + # the P2SH 1-of-2 multi-sig address that spends SSTX outputs. self.signingAddress = None self.apiKey = apiKey self.lastConnection = 0 self.purchaseInfo = None self.stats = None + self.err = None def __tojson__(self): return { "url": self.url, @@ -115,48 +157,131 @@ def __fromjson__(obj): sp.purchaseInfo = obj["purchaseInfo"] sp.stats = obj["stats"] return sp + @staticmethod + def providers(net): + """ + A static method to get the current Decred VSP list. + + Args: + net (string): The network name. + + Returns: + list(object): The vsp list. + """ + vsps = http.get("https://api.decred.org/?c=gsd") + network = "testnet" if net.Name == "testnet3" else net.Name + return [vsp for vsp in vsps.values() if vsp["Network"] == network] def apiPath(self, command): + """ + The full URL for the specified command. + + Args: + command (string): The API endpoint specifier. + + Returns: + string: The full URL. + """ return "%s/api/v2/%s" % (self.url, command) def headers(self): + """ + Make the API request headers. + + Returns: + object: The headers as a Python object. + """ return {"Authorization": "Bearer %s" % self.apiKey} - def setAddress(self, address): - data = { "UserPubKeyAddr": address } - res = http.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) - if resultIsSuccess(res): - self.signingAddress = address - else: - raise Exception("unexpected response from 'address': %s" % repr(res)) - def getPurchaseInfo(self, net): + def validate(self, addr): + """ + Validate performs some checks that the PurchaseInfo provided by the + stake pool API is valid for this given voting address. Exception is + raised on failure to validate. + + Args: + addr (string): The base58-encoded pubkey address that the wallet + uses to vote. + """ + pi = self.purchaseInfo + redeemScript = ByteArray(pi.script) + scriptAddr = crypto.AddressScriptHash.fromScript(self.net.ScriptHashAddrID, redeemScript) + if scriptAddr.string() != pi.ticketAddress: + raise Exception("ticket address mismatch. %s != %s" % (pi.ticketAddress, scriptAddr.string())) + # extract addresses + scriptType, addrs, numSigs = txscript.extractPkScriptAddrs(0, redeemScript, self.net) + if numSigs != 1: + raise Exception("expected 2 required signatures, found 2") + found = False + signAddr = txscript.decodeAddress(addr, self.net) + for addr in addrs: + if addr.string() == signAddr.string(): + found = True + break + if not found: + raise Exception("signing pubkey not found in redeem script") + def authorize(self, address, net): + """ + Authorize the stake pool for the provided address and network. Exception + is raised on failure to authorize. + + Args: + address (string): The base58-encoded pubkey address that the wallet + uses to vote. + net (object): The network parameters. + """ + # An error is returned if the address is already set + # {'status': 'error', 'code': 6, 'message': 'address error - address already submitted'} + # First try to get the purchase info directly. + self.net = net + try: + self.getPurchaseInfo() + self.validate(address) + except Exception as e: + if "code" not in self.err or self.err["code"] != 9: + # code 9 is address not set + raise e + # address is not set + data = { "UserPubKeyAddr": address } + res = http.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) + if resultIsSuccess(res): + self.getPurchaseInfo() + self.validate(address) + else: + raise Exception("unexpected response from 'address': %s" % repr(res)) + def getPurchaseInfo(self): + """ + Get the purchase info from the stake pool API. + + Returns: + PurchaseInfo: The PurchaseInfo object. + """ + # An error is returned if the address isn't yet set + # {'status': 'error', 'code': 9, 'message': 'purchaseinfo error - no address submitted', 'data': None} res = http.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) if resultIsSuccess(res): pi = PurchaseInfo(res["data"]) # check the script hash - redeemScript = ByteArray(pi.script) - scriptAddr = crypto.AddressScriptHash.fromScript(net.ScriptHashAddrID, redeemScript) - if scriptAddr.string() != pi.ticketAddress: - raise Exception("ticket address mismatch. %s != %s" % (pi.ticketAddress, scriptAddr.string())) - # extract addresses - scriptType, addrs, numSigs = txscript.extractPkScriptAddrs(0, redeemScript, net) - if numSigs != 1: - raise Exception("expected 2 required signatures, found 2") - found = False - signAddr = txscript.decodeAddress(self.signingAddress, net) - for addr in addrs: - if addr.string() == signAddr.string(): - found = True - break - if not found: - raise Exception("signing pubkey not found in redeem script") self.purchaseInfo = pi return self.purchaseInfo + self.err = res raise Exception("unexpected response from 'getpurchaseinfo': %r" % (res, )) def getStats(self): + """ + Get the stats from the stake pool API. + + Returns: + Poolstats: The PoolStats object. + """ res = http.get(self.apiPath("stats"), headers=self.headers()) if resultIsSuccess(res): self.stats = PoolStats(res["data"]) return self.stats raise Exception("unexpected response from 'stats': %s" % repr(res)) def setVoteBits(self, voteBits): + """ + Set the vote preference on the StakePool. + + Returns: + bool: True on success. Exception raised on error. + """ data = { "VoteBits": voteBits } res = http.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) if resultIsSuccess(res): diff --git a/pydecred/tests.py b/pydecred/tests.py index fc1ffa39..a5af0eeb 100644 --- a/pydecred/tests.py +++ b/pydecred/tests.py @@ -1128,8 +1128,6 @@ def test_sign_tx(self): # ) # Pay to Pubkey Hash (uncompressed) - # secp256k1 := chainec.Secp256k1 - from tinydecred.pydecred import mainnet testingParams = mainnet for hashType in hashTypes: for suite in signatureSuites: @@ -1151,13 +1149,61 @@ def test_sign_tx(self): pkScript = txscript.makePayToAddrScript(address.string(), testingParams) - # chainParams, tx, idx, pkScript, hashType, kdb, sdb, previousScript, sigType - sigScript = txscript.signTxOutput(privKey, testingParams, tx, idx, pkScript, hashType, None, suite) + class keysource: + @staticmethod + def priv(addr): + return privKey + + sigScript = txscript.signTxOutput(testingParams, tx, idx, pkScript, hashType, keysource, None, suite) self.assertEqual(sigScript, ByteArray(sigStr), msg="%d:%d:%d" % (hashType, idx, suite)) return + def test_sign_stake_p2pkh_outputs(self): + from tinydecred.crypto.secp256k1 import curve as Curve + from tinydecred.crypto import rando + txIn = msgtx.TxIn( + previousOutPoint = msgtx.OutPoint( + txHash = ByteArray(rando.generateSeed(32)), + idx = 0, + tree = 0, + ), + sequence = 4294967295, + valueIn = 1, + blockHeight = 78901, + blockIndex = 23456, + ) + tx = msgtx.MsgTx( + serType = wire.TxSerializeFull, + version = 1, + txIn = [ + txIn, + ], + txOut = [ + msgtx.TxOut( + version = wire.DefaultPkScriptVersion, + value = 1, + ), + ], + lockTime = 0, + expiry = 0, + cachedHash = None, + ) + + privKey = Curve.generateKey() + pkHash = crypto.hash160(privKey.pub.serializeCompressed().b) + addr = crypto.AddressPubKeyHash(mainnet.PubKeyHashAddrID, pkHash) + class keysource: + @staticmethod + def priv(addr): + return privKey + for opCode in (opcode.OP_SSGEN, opcode.OP_SSRTX, opcode.OP_SSTX): + pkScript = txscript.payToStakePKHScript(addr, opcode.OP_SSTX) + # Just looking to raise an exception for now. + txscript.signTxOutput(mainnet, tx, 0, pkScript, + txscript.SigHashAll, keysource, None, crypto.STEcdsaSecp256k1) + + def test_addresses(self): - from tinydecred.pydecred import mainnet, testnet from base58 import b58decode class test: def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAddress=None, f=None, net=None): @@ -1928,16 +1974,16 @@ def utxosource(amt, filter): poolAddr = crypto.AddressPubKeyHash(testnet.PubKeyHashAddrID, pkHash) scriptHash = crypto.hash160("some script. doesn't matter".encode()) scriptAddr = crypto.AddressScriptHash(testnet.ScriptHashAddrID, scriptHash) - ticketPrice = int(blockchain.stakeDiff()["next"]*1e8) + ticketPrice = int(blockchain.stakeDiff()*1e8) class request: minConf = 0 expiry = 0 - spendLimit = ticketPrice*1.1 - poolAddress = poolAddr - votingAddress = scriptAddr + spendLimit = ticketPrice*2*1.1 + poolAddress = poolAddr.string() + votingAddress = scriptAddr.string() ticketFee = 0 poolFees = 7.5 - count = 1 + count = 2 txFee = 0 ticket, spent, newUTXOs = blockchain.purchaseTickets(KeySource(), utxosource, request()) @@ -1952,26 +1998,24 @@ def setUp(self): raise unittest.SkipTest def stakePool(self): stakePool = stakepool.StakePool(self.poolURL, self.apiKey) - stakePool.signingAddress = self.signingAddress + stakePool.authorize(self.signingAddress, testnet) return stakePool - def test_get_purchase_info(self): - from tinydecred.pydecred import testnet + def test_get_purchase_info(self): stakePool = self.stakePool() - pi = stakePool.getPurchaseInfo(testnet) + pi = stakePool.getPurchaseInfo() print(pi.__tojson__()) def test_get_stats(self): stakePool = self.stakePool() stats = stakePool.getStats() print(stats.__tojson__()) def test_voting(self): - from tinydecred.pydecred import testnet stakePool = self.stakePool() - pi = stakePool.getPurchaseInfo(testnet) + pi = stakePool.getPurchaseInfo() if pi.voteBits&(1 << 1) != 0: nextVote = 1|(1 << 2) else: nextVote = 1|(1 << 1) print("changing vote from %d to %d" % (pi.voteBits, nextVote)) stakePool.setVoteBits(nextVote) - pi = stakePool.getPurchaseInfo(testnet) + pi = stakePool.getPurchaseInfo() self.assertEqual(pi.voteBits, nextVote) \ No newline at end of file diff --git a/pydecred/txscript.py b/pydecred/txscript.py index f13574fe..888b3ebb 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -1798,10 +1798,10 @@ def extractPkScriptAddrs(version, pkScript, chainParams): # pay-to-pubkey-hash and pay-to-script-hash are allowed. pkHash = extractStakePubKeyHash(pkScript, opcode.OP_SSTX) if pkHash: - return StakeSubmissionTy, pubKeyHashToAddrs(hash, chainParams), 1 + return StakeSubmissionTy, pubKeyHashToAddrs(pkHash, chainParams), 1 scriptHash = extractStakeScriptHash(pkScript, opcode.OP_SSTX) if scriptHash: - return StakeSubmissionTy, scriptHashToAddrs(hash, chainParams), 1 + return StakeSubmissionTy, scriptHashToAddrs(scriptHash, chainParams), 1 # Check for stake generation script. Only stake-generation-tagged # pay-to-pubkey-hash and pay-to-script-hash are allowed. @@ -1833,17 +1833,83 @@ def extractPkScriptAddrs(version, pkScript, chainParams): # EVERYTHING AFTER TIHS IS UN-IMPLEMENTED raise Exception("unsupported script") -def sign(privKey, chainParams, tx, idx, subScript, hashType, sigType): +def sign(chainParams, tx, idx, subScript, hashType, keysource, sigType): scriptClass, addresses, nrequired = extractPkScriptAddrs(DefaultScriptVersion, subScript, chainParams) - if scriptClass == PubKeyHashTy: - # look up key for address - # key = acct.getPrivKeyForAddress(addresses[0]) + subClass = scriptClass + isStakeType = (scriptClass == StakeSubmissionTy or + scriptClass == StakeSubChangeTy or + scriptClass == StakeGenTy or + scriptClass == StakeRevocationTy) + if isStakeType: + subClass = getStakeOutSubclass(subScript) + + if scriptClass == PubKeyTy: + raise Exception("P2PK signature scripts not implemented") + # privKey = keysource.priv(addresses[0].string()) + # script = p2pkSignatureScript(tx, idx, subScript, hashType, key) + # return script, scriptClass, addresses, nrequired, nil + + elif scriptClass == PubkeyAltTy: + raise Exception("alt signatures not implemented") + # privKey = keysource.priv(addresses[0].string()) + # script = p2pkSignatureScriptAlt(tx, idx, subScript, hashType, key, sigType) + # return script, scriptClass, addresses, nrequired, nil + + elif scriptClass == PubKeyHashTy: + privKey = keysource.priv(addresses[0].string()) script = signatureScript(tx, idx, subScript, hashType, privKey, True) - else: - raise Exception("un-implemented script class") + return script, scriptClass, addresses, nrequired + + elif scriptClass == PubkeyHashAltTy: + raise Exception("alt signatures not implemented") + # look up key for address + # privKey = keysource.priv(addresses[0].string()) + # script = signatureScriptAlt(tx, idx, subScript, hashType, key, compressed, sigType) + # return script, scriptClass, addresses, nrequired + + elif scriptClass == ScriptHashTy: + raise Exception("script-hash script signing not implemented") + # script = keysource.script(addresses[0]) + # return script, scriptClass, addresses, nrequired + + elif scriptClass == MultiSigTy: + raise Exception("spending multi-sig script not implemented") + # script = signMultiSig(tx, idx, subScript, hashType, addresses, nrequired, kdb) + # return script, scriptClass, addresses, nrequired + + elif scriptClass == StakeSubmissionTy: + return handleStakeOutSign(tx, idx, subScript, hashType, keysource, addresses, scriptClass, subClass, nrequired) + + elif scriptClass == StakeGenTy: + return handleStakeOutSign(tx, idx, subScript, hashType, keysource, addresses, scriptClass, subClass, nrequired) + + elif scriptClass == StakeRevocationTy: + return handleStakeOutSign(tx, idx, subScript, hashType, keysource, addresses, scriptClass, subClass, nrequired) - return script, scriptClass, addresses, nrequired + elif scriptClass == StakeSubChangeTy: + return handleStakeOutSign(tx, idx, subScript, hashType, keysource, addresses, scriptClass, subClass, nrequired) + + elif scriptClass == NullDataTy: + raise Exception("can't sign NULLDATA transactions") + + raise Exception("can't sign unknown transactions") + +def handleStakeOutSign(tx, idx, subScript, hashType, keysource, addresses, scriptClass, subClass, nrequired): + """ + # handleStakeOutSign is a convenience function for reducing code clutter in + # sign. It handles the signing of stake outputs. + """ + if subClass == PubKeyHashTy: + privKey = keysource.priv(addresses[0].string()) + txscript = signatureScript(tx, idx, subScript, hashType, privKey, True) + return txscript, scriptClass, addresses, nrequired + elif subClass == ScriptHashTy: + # This will be needed in order to enable voting. + raise Exception("script-hash script signing not implemented") + # script = keysource.script(addresses[0].string()) + # return script, scriptClass, addresses, nrequired + raise Exception("unknown subclass for stake output to sign") def mergeScripts(chainParams, tx, idx, pkScript, scriptClass, addresses, nRequired, sigScript, prevScript): """ @@ -1906,7 +1972,7 @@ def mergeScripts(chainParams, tx, idx, pkScript, scriptClass, addresses, nRequir return sigScript return prevScript -def signTxOutput(privKey, chainParams, tx, idx, pkScript, hashType, previousScript, sigType): +def signTxOutput(chainParams, tx, idx, pkScript, hashType, keysource, previousScript, sigType): """ signTxOutput signs output idx of the given tx to resolve the script given in pkScript with a signature type of hashType. Any keys required will be @@ -1921,7 +1987,7 @@ def signTxOutput(privKey, chainParams, tx, idx, pkScript, hashType, previousScri versions. """ - sigScript, scriptClass, addresses, nrequired = sign(privKey, chainParams, tx, idx, pkScript, hashType, sigType) + sigScript, scriptClass, addresses, nrequired = sign(chainParams, tx, idx, pkScript, hashType, keysource, sigType) isStakeType = (scriptClass == StakeSubmissionTy or scriptClass == StakeSubChangeTy or @@ -1987,7 +2053,7 @@ def spendScriptSize(pkScript): if scriptClass != PubKeyHashTy: raise Exception("unexpected nested script class for credit: %d" % scriptClass) return RedeemP2PKHSigScriptSize - raise Exception("unimplemented") + raise Exception("unimplemented: %s : %r" % (scriptClass, scriptClass)) def estimateInputSize(scriptSize): """ @@ -2367,4 +2433,4 @@ def makeTicket(params, inputPool, inputMain, addrVote, addrSubsidy, ticketCost, # Make sure we generated a valid SStx. checkSStx(mtx) - return mtx \ No newline at end of file + return mtx diff --git a/pydecred/wire/msgtx.py b/pydecred/wire/msgtx.py index 714f6b9e..53b641b1 100644 --- a/pydecred/wire/msgtx.py +++ b/pydecred/wire/msgtx.py @@ -1373,5 +1373,5 @@ def test_tx_from_hex(self): print(repr(tx.lockTime)) print(repr(tx.expiry)) v = sum(txout.value for txout in tx.txOut) - print("--total sent: %.2f" % (v*1e-8,)) + print("total sent: %.2f" % (v*1e-8,)) print(tx.txHex()) diff --git a/ui/qutilities.py b/ui/qutilities.py index e2e4113a..6cb46288 100644 --- a/ui/qutilities.py +++ b/ui/qutilities.py @@ -8,6 +8,8 @@ from tinydecred.util import helpers from PyQt5 import QtCore, QtWidgets, QtGui +log = helpers.getLogger("QUTIL") # , logLvl=0) + # Some colors, QT_WHITE = QtGui.QColor("white") WHITE_PALETTE = QtGui.QPalette(QT_WHITE) @@ -86,15 +88,16 @@ def run(self): """ QThread method. Runs the func. """ - # print("--SmartThread starting with %s" % self.func.__name__) - self.returns = self.func(*self.args, **self.kwargs) - + try: + self.returns = self.func(*self.args, **self.kwargs) + except Exception as e: + log.error("exception encountered in QThread: %s" % helpers.formatTraceback(e)) + self.returns = False def callitback(self): """ QThread Slot connected to the connect Signal. Send the value returned from `func` to the callback function. """ - # print("--SmartThread finishing with %s" % self.callback.__name__) self.callback(self.returns) @@ -317,9 +320,9 @@ def makeWidget(widgetClass, layoutDirection="vertical", parent=None): layout.setAlignment(ALIGN_TOP | ALIGN_LEFT) return widget, layout -def makeSeries(layoutDirection, *widgets, align=None): +def makeSeries(layoutDirection, *widgets, align=None, widget=QtWidgets.QWidget): align = align if align else QtCore.Qt.Alignment() - wgt, lyt = makeWidget(QtWidgets.QWidget, layoutDirection) + wgt, lyt = makeWidget(widget, layoutDirection) for w in widgets: if w == STRETCH: lyt.addStretch(1) @@ -352,6 +355,18 @@ def setBackgroundColor(widget, color): p.setColor(QtGui.QPalette.Window, QtGui.QColor(color)) widget.setPalette(p) +def addDropShadow(wgt): + """ + Add a white background and a drop shadow for the given widget. + """ + effect = QtWidgets.QGraphicsDropShadowEffect() + effect.setBlurRadius(7) + effect.setXOffset(0) + effect.setYOffset(1) + effect.setColor(QtGui.QColor("#777777")) + setBackgroundColor(wgt, "white") + wgt.setGraphicsEffect(effect) + def makeLabel(s, fontSize, a=ALIGN_CENTER, **k): """ Create a QLabel and set the font size and alignment. @@ -385,6 +400,7 @@ def setProperties(lbl, color=None, fontSize=None, fontFamily=None): if fontFamily: font.setFamily(fontFamily) lbl.setFont(font) + return lbl def pad(wgt, t, r, b, l): """ @@ -395,134 +411,6 @@ def pad(wgt, t, r, b, l): w.setContentsMargins(l, t, r, b) return w - -class QSimpleTable(QtWidgets.QTableWidget): - """ - QSimpleTable is a simple table layout with reasonable default settings. - """ - def __init__(self, parent, *args, iconStacking=None, singleHeader=False, fontWeight=None, maxHeight=None, **kwargs): - super(QSimpleTable, self).__init__(parent) - self.singleHeader = singleHeader - self.iconStacking = iconStacking if iconStacking else QtWidgets.QStyleOptionViewItem.Left - self.headerFont = QtWidgets.QTableWidgetItem().font() - self.maxHeight = maxHeight - self.headerFont.setPixelSize(14) - self.headerFont.setWeight(fontWeight if fontWeight else QtGui.QFont.DemiBold) - self.setWordWrap(False) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setProperty("table-type","plain") - self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - self.verticalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - self.horizontalHeader().setStretchLastSection(True) - self.verticalHeader().hide() - self.horizontalHeader().hide() - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - if not maxHeight: - self.wheelEvent = lambda e: None - self.setRowCount(1) - self.resizeSelf() - self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - if len(args): - self.setHeaders(*args) - self.offset = 1 - else: - self.offset = 0 - - def viewOptions(self): - """ - This will stack icons and text vertically? - """ - option = QtWidgets.QTableWidget.viewOptions(self) - option.decorationAlignment = QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter - option.decorationPosition = self.iconStacking - return option - - def setHeaders(self, *headers): - """Set the headers""" - self.setColumnCount(len(headers)) - font = self.headerFont - for i, header in enumerate(headers): - if header: - item = QtWidgets.QTableWidgetItem(header) - else: - item = QtWidgets.QTableWidgetItem() - item.setTextAlignment(ALIGN_CENTER) - item.setFont(font) - self.setItem(0, i, item) - - def clearTable(self, fromRow=None, resize=True): - """ - Clear all but the header row. - """ - startRow = self.offset - if fromRow: - fromRow += self.offset # To account for the headers - if fromRow < self.rowCount(): - startRow = fromRow - else: - if resize: - self.resizeSelf() - return - for i in reversed(range(startRow, self.rowCount())): - self.removeRow(i) - if resize: - self.resizeSelf() - - def insertStuff(self, row, col, text=None, icon=None, rowSpan=1, colSpan=1, alignment=None, font=None, resize=True): - """ - Insert the widget, offsetting row by 1 to account for the headers - text could also be an QtGui.QIcon - """ - actualRow = row+self.offset - alignment = alignment if alignment else ALIGN_CENTER - if self.rowCount() < actualRow+rowSpan: - self.setRowCount(actualRow+rowSpan) - if self.columnCount() < col+colSpan: - self.setColumnCount(col+colSpan) - if self.singleHeader and self.columnCount() < col+1: - self.setColumnCount(col+1) - self.setSpan(0, 0, 1, col+1) - if isinstance(text, str): - text = QtWidgets.QTableWidgetItem(text) - text.setTextAlignment(alignment) - elif not text: - text = QtWidgets.QTableWidgetItem() - if isinstance(icon, QtGui.QIcon): - text.setIcon(icon) - if isinstance(text, QtWidgets.QWidget): - self.setCellWidget(actualRow, col, text) - else: - if font: - text.setFont(font) - self.setItem(actualRow, col, text) - if rowSpan > 1 or colSpan > 1: - self.setSpan(actualRow, col, rowSpan, colSpan) - if resize: - self.resizeSelf() - return text - - def getItem(self, row, col): - """ - Return the item at the given position, offsetting the row by 1 to account for the headers, or None if no item available - """ - return self.item(row+1, col) - - def resizeSelf(self): - rows = self.rowCount() - # scrollBarHeight = self.horizontalScrollBar().height() - # headerHeight = self.horizontalHeader().height() - rowTotalHeight = 0 - for i in range(rows): - rowTotalHeight += self.verticalHeader().sectionSize(i) - if self.maxHeight and rowTotalHeight > self.maxHeight: - self.setFixedHeight(self.maxHeight) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - else: - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setFixedHeight(rowTotalHeight) - - def clearLayout(layout, delete=False): """ Clears all items from the given layout. Optionally deletes the parent widget @@ -570,7 +458,7 @@ def layoutWidgets(layout): QPushButton[button-style-class=light]{ background-color:white; border-radius:3px; - border-color:#777777; + border-color:#aaaaaa; border-width:1px; border-style:solid; color:#333333; @@ -642,9 +530,9 @@ def layoutWidgets(layout): } QLineEdit { padding: 7px; - font-size: 16px; + font-size: 14px; line-height: 34px; - border: 1px solid #777777; + border: 1px solid #aaaaaa; border-radius: 2px; } QLineEdit:focus { diff --git a/ui/screens.py b/ui/screens.py index 2a50da5d..62c27851 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -4,12 +4,14 @@ """ import os import time +import random from PyQt5 import QtGui, QtCore, QtWidgets from tinydecred import config from tinydecred.ui import qutilities as Q, ui from tinydecred.wallet import Wallet from tinydecred.util import helpers from tinydecred.pydecred import constants as DCR +from tinydecred.pydecred.stakepool import StakePool UI_DIR = os.path.dirname(os.path.realpath(__file__)) log = helpers.getLogger("APPUI") # , logLvl=0) @@ -71,7 +73,10 @@ def __init__(self, app): self.padX = self.targetPadding if availPadX >= self.targetPadding else availPadX self.setGeometry( screenGeo.x() + screenGeo.width() - self.w - self.padX, - screenGeo.y() + screenGeo.height() - self.h, self.w, self.h) + screenGeo.y() + screenGeo.height() - self.h, + self.w, + self.h + ) self.successSig.connect(self.showSuccess_) self.showSuccess = lambda s: self.successSig.emit(s) @@ -95,18 +100,18 @@ def __init__(self, app): app.registerSignal(ui.WORKING_SIGNAL, lambda: self.working.setVisible(True)) app.registerSignal(ui.DONE_SIGNAL, lambda: self.working.setVisible(False)) - # If enabled by a Screen instance, the user can navigate directly to - # the home screen. - self.homeIcon = ClickyLabel(self.homeClicked) - self.homeIcon.setPixmap(pixmapFromSvg("home.svg", 20, 20)) - menuLayout.addWidget(Q.pad(self.homeIcon, 3, 3, 3, 3)) - # If enabled by a Screen instance, the user can navigate back to the # previous screen. self.backIcon = ClickyLabel(self.backClicked) self.backIcon.setPixmap(pixmapFromSvg("back.svg", 20, 20)) menuLayout.addWidget(Q.pad(self.backIcon, 3, 3, 3, 3)) + # If enabled by a Screen instance, the user can navigate directly to + # the home screen. + self.homeIcon = ClickyLabel(self.homeClicked) + self.homeIcon.setPixmap(pixmapFromSvg("home.svg", 20, 20)) + menuLayout.addWidget(Q.pad(self.homeIcon, 3, 3, 3, 3)) + # Separate the left and right sub-menus. menuLayout.addStretch(1) @@ -149,6 +154,8 @@ def stack(self, w): w.setVisible(True) self.setIcons(w) self.setVisible(True) + if hasattr(w, "stacked"): + w.stacked() def pop_(self, screen=None): """ Pop the top screen from the stack. If a Screen instance is provided, @@ -186,6 +193,8 @@ def setHomeScreen(self, home): home.setVisible(True) home.runAnimation(FADE_IN_ANIMATION) self.layout.addWidget(home) + if hasattr(home, "inserted"): + home.inserted() self.setIcons(home) def setIcons(self, top): """ @@ -363,9 +372,13 @@ def __init__(self, app): # The TinyDialog won't allow popping of the bottom screen anyway. self.isPoppable = False self.canGoHome = False + self.ticketStats = None + self.balance = None + self.stakeScreen = StakingScreen(app) # Update the home screen when the balance signal is received. app.registerSignal(ui.BALANCE_SIGNAL, self.balanceUpdated) + app.registerSignal(ui.SYNC_SIGNAL, self.setTicketStats) layout = self.layout layout.setAlignment(Q.ALIGN_LEFT) @@ -383,6 +396,8 @@ def __init__(self, app): self.availBalance = ClickyLabel(self.balanceClicked, "0.00 spendable") Q.setProperties(self.availBalance, fontSize=15) + self.statsLbl = Q.makeLabel("", 15) + tot, totLyt = Q.makeSeries(Q.HORIZONTAL, self.totalBalance, self.totalUnit, @@ -391,6 +406,7 @@ def __init__(self, app): bals, balsLyt = Q.makeSeries(Q.VERTICAL, tot, self.availBalance, + self.statsLbl, align=Q.ALIGN_RIGHT, ) @@ -438,6 +454,13 @@ def __init__(self, app): spend.clicked.connect(self.spendClicked) optsLyt.addWidget(spend, 0, 0, Q.ALIGN_LEFT) + # Open staking window. Button is initally hidden until sync is complete. + self.stakeBttn = btn = app.getButton(SMALL, "Staking") + btn.setVisible(False) + btn.setMinimumWidth(110) + btn.clicked.connect(self.openStaking) + optsLyt.addWidget(btn, 0, 1, Q.ALIGN_RIGHT) + # Navigate to the settings screen. # settings = app.getButton(SMALL, "Settings") # settings.setMinimumWidth(110) @@ -453,8 +476,8 @@ def newAddressClicked(self): """ app = self.app def addr(wallet): - return app.wallet.getNewAddress() - app.withUnlockedWallet(addr, self.setNewAddress) + return wallet.getNewAddress() + app.withUnlockedWallet(addr, self.setNewAddress) def setNewAddress(self, address): """ Callback for newAddressClicked. Sets the displayed address. @@ -483,12 +506,31 @@ def balanceUpdated(self, bal): self.totalBalance.setText("{0:,.2f}".format(dcr)) self.totalBalance.setToolTip("%.8f" % dcr) self.availBalance.setText("%s spendable" % availStr.rstrip('0').rstrip('.')) + self.balance = bal + if self.ticketStats: + self.setTicketStats() + def setTicketStats(self): + """ + Set the staking statistics. + """ + acct = self.app.wallet.selectedAccount + balance = self.balance + stats = acct.ticketStats() + if stats and balance and balance.total > 0: + self.stakeBttn.setVisible(True) + self.statsLbl.setText("%s%% staked" % helpers.formatNumber(stats.value/balance.total*100)) + self.ticketStats = stats def spendClicked(self, e=None): """ Display a form to send funds to an address. A Qt Slot, but any event parameter is ignored. """ self.app.appWindow.stack(self.app.sendScreen) + def openStaking(self, e=None): + """ + Display the staking window. + """ + self.app.appWindow.stack(self.stakeScreen) def settingsClicked(self, e): log.debug("settings clicked") @@ -953,6 +995,453 @@ def walletCreationComplete(self, wallet): else: app.appWindow.showError("failed to create wallet") +class StakingScreen(Screen): + """ + A screen with a simple form for entering a mnemnic seed from which to + generate a wallet. + """ + def __init__(self, app): + """ + Args: + app (TinyDecred): The TinyDecred application instance. + """ + super().__init__(app) + self.isPoppable = True + self.canGoHome = True + self.layout.setSpacing(20) + self.poolScreen = PoolScreen(app, self.poolAuthed) + self.balance = None + self.wgt.setContentsMargins(5, 5, 5, 5) + self.wgt.setMinimumWidth(400) + self.blockchain = app.dcrdata + + # Register for a few key signals. + self.app.registerSignal(ui.BLOCKCHAIN_CONNECTED, self.blockchainConnected) + self.app.registerSignal(ui.BALANCE_SIGNAL, self.balanceSet) + self.app.registerSignal(ui.SYNC_SIGNAL, self.setStats) + + # ticket price is a single row reading `Ticket Price: XX.YY DCR`. + lbl = Q.makeLabel("Ticket Price: ", 16) + self.lastPrice = None + self.lastPriceStamp = 0 + self.ticketPrice = Q.makeLabel("--.--", 24, fontFamily="Roboto-Bold") + unit = Q.makeLabel("DCR", 16) + priceWgt, _ = Q.makeSeries(Q.HORIZONTAL, lbl, self.ticketPrice, unit) + self.layout.addWidget(priceWgt) + + # Current holdings is a single row that reads `Currently staking X + # tickets worth YY.ZZ DCR` + lbl = Q.makeLabel("Currently staking", 14) + self.ticketCount = Q.makeLabel("", 18, fontFamily="Roboto-Bold") + lbl2 = Q.makeLabel("tickets worth", 14) + self.ticketValue = Q.makeLabel("", 18, fontFamily="Roboto-Bold") + unit = Q.makeLabel("DCR", 14) + wgt, _ = Q.makeSeries(Q.HORIZONTAL, lbl, self.ticketCount, lbl2, self.ticketValue, unit) + self.layout.addWidget(wgt) + + # Affordability. A row that reads `You can afford X tickets` + lbl = Q.makeLabel("You can afford ", 14) + self.affordLbl = Q.makeLabel(" ", 17, fontFamily="Roboto-Bold") + lbl2 = Q.makeLabel("tickets", 14) + affordWgt, _ = Q.makeSeries(Q.HORIZONTAL, lbl, self.affordLbl, lbl2) + affordWgt.setContentsMargins(0, 0, 0, 30) + self.layout.addWidget(affordWgt) + + # The actual purchase form. A box with a drop shadow that contains a + # single row reading `Purchase [ ] tickets [Buy Now]`. + lbl = Q.makeLabel("Purchase", 16) + lbl2 = Q.makeLabel("tickets", 16) + lbl2.setContentsMargins(0, 0, 30, 0) + qty = self.ticketQty = QtWidgets.QLineEdit() + qty.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]*"))) + qty.setFixedWidth(40) + font = lbl2.font() + font.setPixelSize(18) + qty.setFont(font) + qty.returnPressed.connect(self.buyClicked) + btn = app.getButton(SMALL, "Buy Now") + btn.clicked.connect(self.buyClicked) + purchaseWgt, lyt = Q.makeSeries(Q.HORIZONTAL, lbl, qty, lbl2, Q.STRETCH, btn) + lyt.setContentsMargins(10, 10, 10, 10) + Q.addDropShadow(purchaseWgt) + self.layout.addWidget(purchaseWgt) + + def stacked(self): + """ + stacked is called on screens when stacked by the TinyDialog. + """ + acct = self.app.wallet.selectedAccount + if not acct.hasPool(): + self.app.appWindow.pop(self) + self.app.appWindow.stack(self.poolScreen) + + def setStats(self): + """ + Get the current ticket stats and update the display. + """ + acct = self.app.wallet.selectedAccount + stats = acct.ticketStats() + self.ticketCount.setText(str(stats.count)) + self.ticketValue.setText("%.2f" % (stats.value/1e8)) + + def blockchainConnected(self): + """ + Connected to the BLOCKCHAIN_CONNECTED signal. Updates the current + ticket price. + """ + self.app.makeThread(getTicketPrice, self.ticketPriceCB, self.blockchain) + + def ticketPriceCB(self, ticketPrice): + """ + Sets the current ticket price and updates the display. + + Args: + ticketPrice (float): The ticket price, in DCR. + """ + if not ticketPrice: + return + self.lastPrice = ticketPrice + self.lastPriceStamp = int(time.time()) + self.ticketPrice.setText("%.2f" % ticketPrice) + self.ticketPrice.setToolTip("%.8f" % ticketPrice) + self.setBuyStats() + + def balanceSet(self, balance): + """ + Connected to the BALANCE_SIGNAL signal. Sets the balance and updates + the display. + + Args: + balance (account.Balance): The current account balance. + """ + self.balance = balance + self.setBuyStats() + + def setBuyStats(self): + """ + Update the display of the current affordability stats. + """ + if self.balance and self.lastPrice: + self.affordLbl.setText(str(int(self.balance.available/1e8/self.lastPrice))) + + def buyClicked(self, e=None): + """ + Connected to the "Buy Now" button clicked signal. Initializes the ticket + purchase routine. + """ + qtyStr = self.ticketQty.text() + if not qtyStr or qtyStr == "": + self.app.appWindow.showError("can't purchase zero tickets") + return + qty = int(qtyStr) + if qty > self.balance.available/1e8/self.lastPrice: + self.app.appWindow.showError("can't afford %d tickets" % qty) + def step(): + self.app.withUnlockedWallet(self.buyTickets, self.ticketsBought, qty) + self.app.confirm("Are you sure you want to purchase %d ticket(s) for %.2f DCR? " + "Once purchased, these funds will be locked until your tickets vote or expire." + % (qty, qty*self.lastPrice), + step) + def buyTickets(self, wallet, qty): + """ + The second step in the sequence for a ticket purchase. Defer the hard + work to the open Account. + + Args: + wallet (Wallet): The open wallet. + qty (int): The number of tickets to purchase. + + Returns: + list(msgtx.MsgTx): The purchased tickets. + """ + tip = self.blockchain.tip["height"] + acct = wallet.openAccount + txs = acct.purchaseTickets(qty, self.lastPrice, self.blockchain) + if txs: + wallet.signals.balance(acct.calcBalance(tip)) + self.app.home() + self.app.appWindow.showSuccess("bought %s tickets" % qty) + wallet.save() + return txs + def ticketsBought(self, res): + """ + The final callback from a ticket purchase. If res evaluates True, it + should be a list of purchased tickets. + """ + if not res: + self.app.appWindow.showError("error purchasing tickets") + self.app.home() + def poolAuthed(self, res): + """ + The callback from the PoolScreen when a pool is added. If res evaluates + True, the pool was successfully authorized. + """ + if not res: + # The pool screen handles error notifications. + self.app.home() + window = self.app.appWindow + window.pop(self.poolScreen) + window.stack(self) + +class PoolScreen(Screen): + def __init__(self, app, callback): + """ + Args: + app (TinyDecred): The TinyDecred application instance. + callback (function): A function to call when a pool is succesfully + validated. + """ + super().__init__(app) + self.isPoppable = True + self.canGoHome = True + self.callback = callback + self.pools = [] + self.poolIdx = -1 + self.app.registerSignal(ui.BLOCKCHAIN_CONNECTED, self.getPools) + self.wgt.setMinimumWidth(400) + self.wgt.setContentsMargins(15, 0, 15, 0) + + # After the header, there are two rows that make up the form. The first + # row is a QLineEdit and a button that takes the pool URL. The second + # row is a slightly larger QLineEdit for the API key. + lbl = Q.makeLabel("Add a voting service provider", 16) + wgt, _ = Q.makeSeries(Q.HORIZONTAL, lbl, Q.STRETCH) + self.layout.addWidget(wgt) + self.poolIp = edit = QtWidgets.QLineEdit() + edit.setPlaceholderText("e.g. https://anothervsp.com") + edit.returnPressed.connect(self.authPool) + btn = app.getButton(SMALL, "Add") + btn.clicked.connect(self.authPool) + wgt, _ = Q.makeSeries(Q.HORIZONTAL, self.poolIp, btn) + wgt.setContentsMargins(0, 0, 0, 5) + self.layout.addWidget(wgt) + self.keyIp = edit = QtWidgets.QLineEdit() + edit.setPlaceholderText("API key") + self.keyIp.setContentsMargins(0, 0, 0, 30) + self.layout.addWidget(edit) + edit.returnPressed.connect(self.authPool) + + # A separate header for the pick-a-VSP section. + l = Q.ALIGN_LEFT + lbl = Q.makeLabel("Don't have a VSP yet? Heres one.", 15, a=l) + self.layout.addWidget(lbl) + + # Display info for a randomly chosen pool (with some filtering), and a + # couple of links to aid in selecting a VSP.. + self.poolUrl = Q.makeLabel("", 16, a=l, fontFamily="Roboto-Medium") + self.poolUrl.setOpenExternalLinks(True) + self.poolUrl.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse); + scoreLbl = Q.makeLabel("score:", 14) + self.score = Q.makeLabel("", 14) + feeLbl = Q.makeLabel("fee:", 14) + self.fee = Q.makeLabel("", 14) + usersLbl = Q.makeLabel("users:", 14) + self.users = Q.makeLabel("", 14) + stats, _ = Q.makeSeries( Q.HORIZONTAL, + scoreLbl, self.score, Q.STRETCH, + feeLbl, self.fee, Q.STRETCH, + usersLbl, self.users + ) + poolWgt, lyt = Q.makeSeries(Q.VERTICAL, self.poolUrl, stats) + lyt.setContentsMargins(10, 10, 10, 10) + lyt.setSpacing(10) + Q.addDropShadow(poolWgt) + Q.addHoverColor(poolWgt, "#f5ffff") + poolWgt.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + poolWgt.mouseReleaseEvent = self.poolClicked + self.layout.addWidget(poolWgt) + + # A button to select a different pool and a link to the master list on + # decred.org. + btn1 = app.getButton(TINY, "show another") + btn1.clicked.connect(self.randomizePool) + btn2 = app.getButton(TINY, "see all") + btn2.clicked.connect(self.showAll) + wgt, _ = Q.makeSeries(Q.HORIZONTAL, btn1, Q.STRETCH, btn2) + self.layout.addWidget(wgt) + + def getPools(self): + """ + Get the current master list of VSPs from decred.org. + """ + net = self.app.dcrdata.params + def getPools(): + try: + return StakePool.providers(net) + except Exception as e: + log.error("error retrieving stake pools: %s" % e) + return False + self.app.makeThread(getPools, self.setPools) + + def setPools(self, pools): + """ + Cache the list of stake pools from decred.org, and pick one to display. + + Args: + pools (list(object)): The freshly-decoded-from-JSON stake pools. + """ + if not pools: + return + self.pools = pools + tNow = int(time.time()) + # only save pools updated within the last day + self.pools = [p for p in pools if tNow - p["LastUpdated"] < 86400] + # sort by performance + self.pools.sort(key=self.scorePool, reverse=True) + self.randomizePool() + + def randomizePool(self, e=None): + """ + Randomly select a pool from the best performing half, where performance + is based purely on voting record, e.g. voted/(voted+missed). The sorting + and some initial filtering was already performed in setPools. + """ + count = len(self.pools)//2+1 + pools = self.pools[:count] + lastIdx = self.poolIdx + if count == 1: + self.poolIdx = 0 + else: + # pick random elements until the index changes + while self.poolIdx == lastIdx: + self.poolIdx = random.randint(0, count-1) + pool = pools[self.poolIdx] + self.poolUrl.setText(pool["URL"]) + self.score.setText("%.1f%%" % self.scorePool(pool)) + self.fee.setText("%.1f%%" % pool["PoolFees"]) + self.users.setText(str(pool["UserCountActive"])) + + def scorePool(self, pool): + """ + Get the pools performance score, as a float percentage. + """ + return pool["Voted"]/(pool["Voted"]+pool["Missed"])*100 + + def authPool(self): + """ + Connected to the "Add" button clicked signal. Attempts to authorize the + user-specified pool and API key. + """ + url = self.poolIp.text() + app = self.app + err = app.appWindow.showError + if not url: + err("empty address") + return + if not url.startswith("http://") and not url.startswith("https://"): + err("invalid pool address: %s" % url) + return + apiKey = self.keyIp.text() + if not apiKey: + err("empty API key") + return + pool = StakePool(url, apiKey) + def votingAddr(wallet): + try: + addr = wallet.openAccount.votingAddress() + wallet.openAccount.setPool(pool) + wallet.save() + return pool, addr, wallet.openAccount.net + except Exception as e: + log.error("error getting voting address: %s" % e) + err("failed to get voting address") + return None + app.withUnlockedWallet(votingAddr, self.continueAuth) + def continueAuth(self, res): + """ + Follows authPool in the pool authorization process. Send the wallet + voting address to the pool and authorize the response. + """ + if not res: + self.callback(False) + return + pool, addr, net = res + app = self.app + def setAddr(): + try: + pool.authorize(addr, net) + app.appWindow.showSuccess("pool authorized") + self.callback(True) + except Exception as e: + # log.error("failed to set pool address: %s" % e) + # self.app.appWindow.showError("failed to set address") + # might be okay. + log.error("failed to authorize stake pool: %s" % e) + app.appWindow.showError("pool authorization failed") + self.callback(False) + + self.app.waitThread(setAddr, None) + def showAll(self, e=None): + """ + Connected to the "see all" button clicked signal. Open the full + decred.org VSP list in the browser. + """ + QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://decred.org/vsp/")) + def poolClicked(self, e=None): + """ + Callback from the clicked signal on the try-this-pool widget. Opens the + pools homepage in the users browser. + """ + url = self.poolUrl.text() + self.poolIp.setText(url) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) + + +class ConfirmScreen(Screen): + """ + A screen that displays a custom prompt and calls a callback function + conditionally on user affirmation. The two available buttons say "ok" and + "no". Clicking "ok" triggers the callback. Clicking "no" simply pops this + Screen. + """ + def __init__(self, app): + """ + Args: + app (TinyDecred): The TinyDecred application instance. + """ + super().__init__(app) + self.isPoppable = True + self.canGoHome = False + self.callback = None + + self.prompt = Q.makeLabel("", 16) + self.prompt.setWordWrap(True) + self.layout.addWidget(self.prompt) + stop = app.getButton(SMALL, "no") + stop.clicked.connect(self.stopClicked) + go = app.getButton(SMALL, "ok") + go.clicked.connect(self.goClicked) + wgt, _ = Q.makeSeries(Q.HORIZONTAL, Q.STRETCH, stop, Q.STRETCH, go, Q.STRETCH) + wgt.setContentsMargins(0, 20, 0, 0) + self.layout.addWidget(wgt) + def withPurpose(self, prompt, callback): + """ + Set the prompts and callback and return self. + + Args: + prompt (string): The prompt for the users. + callback (function): The function to call when the user clicks "ok". + + Returns: + ConfirmScreen: This instance. Useful for using a patter like + app.appWindow.stack(confirmScreen.withPurpose("go ahead?", callbackFunc)) + """ + self.prompt.setText(prompt) + self.callback = callback + return self + def stopClicked(self, e=None): + """ + The user has clicked "no". Just pop this screen. + """ + self.app.appWindow.pop(self) + def goClicked(self, e=None): + """ + The user has clicked the "ok" button. Pop self and call the callback. + """ + self.app.appWindow.pop(self) + if self.callback: + self.callback() + class Spinner(QtWidgets.QLabel): """ A waiting/loading spinner. @@ -1000,3 +1489,10 @@ def paintEvent(self, e): painter.setRenderHints(QtGui.QPainter.HighQualityAntialiasing) painter.setPen(self.getPen()) painter.drawEllipse(*self.rect) + +def getTicketPrice(blockchain): + try: + return blockchain.stakeDiff() + except Exception as e: + log.error("error fetching ticket price: %s" % e) + return False \ No newline at end of file diff --git a/ui/ui.py b/ui/ui.py index 55d59af2..4f889a39 100644 --- a/ui/ui.py +++ b/ui/ui.py @@ -15,4 +15,5 @@ BALANCE_SIGNAL = "balance_signal" SYNC_SIGNAL = "sync_signal" WORKING_SIGNAL = "working_signal" -DONE_SIGNAL = "done_signal" \ No newline at end of file +DONE_SIGNAL = "done_signal" +BLOCKCHAIN_CONNECTED = "blockchain_connected" \ No newline at end of file diff --git a/util/http.py b/util/http.py index 67c2341c..96bc6822 100644 --- a/util/http.py +++ b/util/http.py @@ -7,7 +7,9 @@ import urllib.request as urlrequest from urllib.parse import urlencode from tinydecred.util import tinyjson -from tinydecred.util.helpers import formatTraceback +from tinydecred.util.helpers import formatTraceback, getLogger + +log = getLogger("HTTP") def get(uri, **kwargs): return request(uri, **kwargs) @@ -38,4 +40,5 @@ def request(uri, postData=None, headers=None, urlEncode=False): except Exception as e: raise Exception("JSONError", "Failed to decode server response from path %s: %s : %s" % (uri, raw, formatTraceback(e))) except Exception as e: + log.error("error retrieving %s request for %s" % ("POST" if postData else "GET", uri, e)) raise Exception("RequestError", "Error encountered in requesting path %s: %s" % (uri, formatTraceback(e))) \ No newline at end of file diff --git a/util/tinyjson.py b/util/tinyjson.py index eff4f32d..bf4bae45 100644 --- a/util/tinyjson.py +++ b/util/tinyjson.py @@ -11,7 +11,7 @@ def clsKey(cls): return cls.__qualname__ -def register(cls): +def register(cls, tag=None): """ Registered types will be checked for compliance with the JSONMarshaller. When an object of a registered type is dump'ed, it's __tojson__ method @@ -22,7 +22,8 @@ def register(cls): """ if not hasattr(cls, "__fromjson__") or not hasattr(cls, "__tojson__"): raise KeyError("register: registered types must have a __fromjson__ method") - k = clsKey(cls) + k = tag if tag else clsKey(cls) + cls.__jsontag__ = k if k in _types: raise Exception("tinyjson: mutliple attempts to register class %s" % k) _types[k] = cls @@ -59,10 +60,9 @@ class Encoder(json.JSONEncoder): probably a dict. """ def default(self, obj): - ck = clsKey(obj.__class__) - if ck in _types: + if hasattr(obj.__class__, "__jsontag__"): encoded = obj.__tojson__() - encoded["_jt_"] = ck + encoded["_jt_"] = obj.__class__.__jsontag__ return encoded # Let the base class default method raise the TypeError return json.JSONEncoder.default(self, obj) diff --git a/wallet.py b/wallet.py index 829c074a..e83e7381 100644 --- a/wallet.py +++ b/wallet.py @@ -4,25 +4,17 @@ See LICENSE for details """ import os -import unittest from threading import Lock as Mutex from tinydecred.util import tinyjson, helpers from tinydecred.crypto import crypto, mnemonic from tinydecred.pydecred import txscript +from tinydecred.pydecred.account import DecredAccount from tinydecred.accounts import createNewAccountManager log = helpers.getLogger("WLLT") # , logLvl=0) VERSION = "0.0.1" -class KeySource(object): - """ - Implements the KeySource API from tinydecred.api. - """ - def __init__(self, priv, internal): - self.priv = priv - self.internal = internal - class Wallet(object): """ Wallet is a wallet. An application would use a Wallet to create and @@ -60,9 +52,7 @@ def __init__(self): # The fileKey is a hash generated with the user's password as an input. # The fileKey hash is used to AES encrypt and decrypt the wallet file. self.fileKey = None - # An object implementing the BlockChain API. Eventually should be moved - # from wallet in favor of a common interface that wraps a full, spv, or - # light node. + # An object implementing the BlockChain API. self.blockchain = None # The best block. self.users = 0 @@ -110,7 +100,7 @@ def create(path, password, chain, userSeed = None): pw = password.encode() # Create the keys and coin type account, using the seed, the public # password, private password and blockchain params. - wallet.acctManager = createNewAccountManager(seed, b'', pw, chain) + wallet.acctManager = createNewAccountManager(seed, b'', pw, chain, constructor=DecredAccount) wallet.fileKey = crypto.SecretKey(pw) wallet.selectedAccount = wallet.acctManager.openAccount(0, password) wallet.close() @@ -297,42 +287,6 @@ def balance(self): Balance: The current account's Balance object. """ return self.selectedAccount.balance - def getUTXOs(self, requested, approve=None): - """ - Find confirmed and mature UTXOs, smallest first, that sum to the - requested amount, in atoms. - - Args: - requested (int): Required amount in atoms. - filter (func(UTXO) -> bool): Optional UTXO filtering function. - - Returns: - list(UTXO): A list of UTXOs. - bool: True if the UTXO sum is >= the requested amount. - """ - matches = [] - acct = self.openAccount - collected = 0 - pairs = [(u.satoshis, u) for u in acct.utxoscan()] - for v, utxo in sorted(pairs, key=lambda p: p[0]): - if approve and not approve(utxo): - continue - matches.append(utxo) - collected += v - if collected >= requested: - break - return matches, collected >= requested - def getKey(self, addr): - """ - Get the private key for the provided address. - - Args: - addr (str): The base-58 encoded address. - - Returns: - secp256k1.PrivateKey: The private key structure for the address. - """ - return self.openAccount.getPrivKeyForAddress(addr) def blockSignal(self, sig): """ Process a new block from the explorer. @@ -438,22 +392,9 @@ def sendToAddress(self, value, address, feeRate=None): failure. """ acct = self.openAccount - keysource = KeySource( - priv = self.getKey, - internal = acct.getChangeAddress, - ) - tx, spentUTXOs, newUTXOs = self.blockchain.sendToAddress(value, address, keysource, self.getUTXOs, feeRate) - acct.addMempoolTx(tx) - acct.spendUTXOs(spentUTXOs) - for utxo in newUTXOs: - acct.addUTXO(utxo) + tx = acct.sendToAddress(value, address, feeRate, self.blockchain) self.signals.balance(acct.calcBalance(self.blockchain.tip["height"])) self.save() return tx -tinyjson.register(Wallet) - - -class TestWallet(unittest.TestCase): - def test_tx_to_outputs(self): - pass +tinyjson.register(Wallet) \ No newline at end of file From 8e456e6fa5e1a7e2934fa0093a8aeb66a27db92b Mon Sep 17 00:00:00 2001 From: buck54321 Date: Sun, 6 Oct 2019 12:58:10 -0500 Subject: [PATCH 04/12] fix syncing and gap address handling --- accounts.py | 289 ++++++++++++++++++++++-------- examples/create_testnet_wallet.py | 2 +- pydecred/README.md | 3 +- pydecred/account.py | 134 ++++++++++++-- pydecred/dcrdata.py | 35 +++- pydecred/tests.py | 7 +- ui/screens.py | 4 +- util/database.py | 12 +- wallet.py | 91 +--------- 9 files changed, 380 insertions(+), 197 deletions(-) diff --git a/accounts.py b/accounts.py index e5f404e2..2a531fab 100644 --- a/accounts.py +++ b/accounts.py @@ -26,6 +26,12 @@ CrazyAddress = "CRAZYADDRESS" +def filterCrazyAddress(addrs): + return [a for a in addrs if a != CrazyAddress] + +# DefaultGapLimit is the default unused address gap limit defined by BIP0044. +DefaultGapLimit = 20 + log = helpers.getLogger("TCRYP") # , logLvl=0) class CoinSymbols: @@ -198,11 +204,18 @@ def __init__(self, pubKeyEncrypted, privKeyEncrypted, name, coinID, netID): self.netID = netID self.net = None setNetwork(self) - self.lastExternalIndex = -1 - self.lastInternalIndex = -1 + # For external addresses, the cursor can sit on the last seen address, + # so start the lastSeen at the 0th external address. This is necessary + # because the currentAddress method grabs the address at the current + # cursor position, rather than the next. + self.lastSeenExt = 0 + # For internal addresses, the cursor can sit below zero, since the + # addresses are always retrieved with with nextInternalAddress. + self.lastSeenInt = -1 self.externalAddresses = [] self.internalAddresses = [] - self.cursor = 0 + self.cursorExt = 0 + self.cursorInt = 0 self.balance = Balance() # Map a txid to a MsgTx for a transaction suspected of being in # mempool. @@ -217,21 +230,22 @@ def __init__(self, pubKeyEncrypted, privKeyEncrypted, name, coinID, netID): self.privKey = None # The private extended key. self.extPub = None # The external branch public extended key. self.intPub = None # The internal branch public extended key. + self.gapLimit = DefaultGapLimit def __tojson__(self): return { "pubKeyEncrypted": self.pubKeyEncrypted, "privKeyEncrypted": self.privKeyEncrypted, - "lastExternalIndex": self.lastExternalIndex, - "lastInternalIndex": self.lastInternalIndex, "name": self.name, "coinID": self.coinID, "netID": self.netID, "externalAddresses": self.externalAddresses, "internalAddresses": self.internalAddresses, - "cursor": self.cursor, + "cursorExt": self.cursorExt, + "cursorInt": self.cursorInt, "txs": self.txs, "utxos": self.utxos, "balance": self.balance, + "gapLimit": self.gapLimit, } @staticmethod def __fromjson__(obj, cls=None): @@ -243,14 +257,16 @@ def __fromjson__(obj, cls=None): obj["coinID"], obj["netID"], ) - acct.lastExternalIndex = obj["lastExternalIndex"] - acct.lastInternalIndex = obj["lastInternalIndex"] acct.externalAddresses = obj["externalAddresses"] acct.internalAddresses = obj["internalAddresses"] - acct.cursor = obj["cursor"] + acct.cursorExt = obj["cursorExt"] + acct.cursorInt = obj["cursorInt"] acct.txs = obj["txs"] acct.utxos = obj["utxos"] acct.balance = obj["balance"] + acct.gapLimit = obj["gapLimit"] + acct.lastSeenExt = acct.lastSeen(acct.externalAddresses) + acct.lastSeenInt = acct.lastSeen(acct.internalAddresses) setNetwork(acct) return acct def addrTxs(self, addr): @@ -432,6 +448,21 @@ def addTxid(self, addr, txid): txids = self.txs[addr] if txid not in txids: txids.append(txid) + try: + extIdx = self.externalAddresses.index(addr) + if extIdx > self.lastSeenExt: + diff = extIdx - self.lastSeenExt + self.lastSeenExt = extIdx + self.cursorExt = max(0, self.cursorExt-diff) + except ValueError: + try: + intIdx = self.internalAddresses.index(addr) + if intIdx > self.lastSeenInt: + diff = intIdx - self.lastSeenInt + self.lastSeenInt = intIdx + self.cursorInt = max(0, self.cursorInt-diff) + except ValueError: + raise Exception("attempting to add transaction %s for unknown address %s" % (txid, addr)) def confirmTx(self, tx, blockHeight): """ Confirm a transaction. Sets height for any unconfirmed UTXOs in the @@ -472,37 +503,91 @@ def calcBalance(self, tipHeight): self.balance.total = tot self.balance.available = avail return self.balance - def generateNextPaymentAddress(self): + def nextBranchAddress(self, branchKey, branchAddrs): """ Generate a new address and add it to the list of external addresses. Does not move the cursor. + Args: + branchKey (ExtendedKey): The branch extended public key. + branchAddrs (list(string)): The current list of branch addresses. + Returns: str: Base-58 encoded address. """ - if len(self.externalAddresses) != self.lastExternalIndex + 1: - raise Exception("index-address length mismatch") - idx = self.lastExternalIndex + 1 - try: - addr = self.extPub.deriveChildAddress(idx, self.net) - except crypto.ParameterRangeError: - log.warning("crazy address generated") - addr = CrazyAddress - self.externalAddresses.append(addr) - self.lastExternalIndex = idx + for i in range(3): + try: + addr = branchKey.deriveChildAddress(len(branchAddrs), self.net) + branchAddrs.append(addr) + return addr + except crypto.ParameterRangeError: + log.warning("crazy address generated") + addr = CrazyAddress + branchAddrs.append(addr) + continue + raise Exception("failed to generate new address") + def nextExternalAddress(self): + """ + Return a new external address by advancing the cursor. + + Returns: + addr (str): Base-58 encoded address. + """ + extAddrs = self.externalAddresses + addr = CrazyAddress + # Though unlikely, if an out or range key is generated, the account will + # generate an additional address + while addr == CrazyAddress: + # gap policy is to wrap. Wrapping brings the cursor back to index 1, + # since the index zero is the last seen address. + self.cursorExt += 1 + if self.cursorExt > self.gapLimit: + self.cursorExt = 1 + idx = self.lastSeenExt + self.cursorExt + while len(extAddrs) < idx + 1: + self.nextBranchAddress(self.extPub, extAddrs) + addr = extAddrs[idx] return addr - def getNextPaymentAddress(self): + def nextInternalAddress(self): """ - Get the next address after the cursor and move the cursor. + Return a new internal address Returns: str: Base-58 encoded address. """ - self.cursor += 1 - while self.cursor >= len(self.externalAddresses): - self.generateNextPaymentAddress() - return self.externalAddresses[self.cursor] - def generateGapAddresses(self, gap): + intAddrs = self.internalAddresses + addr = CrazyAddress + # Though unlikely, if an out or range key is generated, the account will + # generate an additional address + while addr == CrazyAddress: + # gap policy is to wrap. Wrapping brings the cursor back to index 1, + # since the index zero is the last seen address. + self.cursorInt += 1 + if self.cursorInt > self.gapLimit: + self.cursorInt = 1 + idx = self.lastSeenInt + self.cursorInt + while len(intAddrs) < idx + 1: + self.nextBranchAddress(self.intPub, intAddrs) + addr = intAddrs[idx] + return addr + def lastSeen(self, addrs): + """ + Find the index of the last seen address in the list of addresses. + The last seen address is taken as the last address for which there is an + entry in the self.txs dict. + + Args: + addrs (list(string)): The list of addresses. + + Returns: + int: The highest index of all seen addresses in the list. + """ + lastSeen = -1 + for i, addr in enumerate(addrs): + if addr in self.txs: + lastSeen = i + return lastSeen + def generateGapAddresses(self): """ Generate addresses up to gap addresses after the cursor. Do not move the cursor. @@ -512,33 +597,17 @@ def generateGapAddresses(self, gap): """ if self.extPub is None: log.warning("attempting to generate gap addresses on a closed account") - highest = 0 - for addr in self.txs: - try: - highest = max(highest, self.externalAddresses.index(addr)) - except ValueError: # Not found - continue - tip = highest + gap - while len(self.externalAddresses) < tip: - self.generateNextPaymentAddress() - def getChangeAddress(self): - """ - Return a new change address. - - Returns: - str: Base-58 encoded address. - """ - if len(self.internalAddresses) != self.lastInternalIndex + 1: - raise Exception("index-address length mismatch while generating change address") - idx = self.lastInternalIndex + 1 - try: - addr = self.intPub.deriveChildAddress(idx, self.net) - except crypto.ParameterRangeError: - log.warning("crazy address generated") - addr = CrazyAddress - self.internalAddresses.append(addr) - self.lastInternalIndex = idx - return addr + newAddrs = [] + extAddrs, intAddrs = self.externalAddresses, self.internalAddresses + minExtLen = self.lastSeenExt + self.gapLimit + 1 + while len(extAddrs) < minExtLen: + self.nextBranchAddress(self.extPub, extAddrs) + newAddrs.append(extAddrs[len(extAddrs)-1]) + minIntLen = self.lastSeenInt + self.gapLimit + 1 + while len(intAddrs) < minIntLen: + self.nextBranchAddress(self.intPub, self.internalAddresses) + newAddrs.append(intAddrs[len(intAddrs)-1]) + return newAddrs def allAddresses(self): """ Get the list of all known addresses for this account. @@ -546,8 +615,8 @@ def allAddresses(self): Returns: list(str): A list of base-58 encoded addresses. """ - return self.internalAddresses + self.externalAddresses - def addressesOfInterest(self): + return filterCrazyAddress(self.internalAddresses + self.externalAddresses) + def watchAddrs(self): """ Scan and get the list of all known addresses for this account. @@ -555,20 +624,22 @@ def addressesOfInterest(self): list(str): A list of base-58 encoded addresses. """ a = set() - for utxo in self.utxoscan(): - a.add(utxo.address) - ext = self.externalAddresses - for i in range(max(self.cursor - 10, 0), self.cursor+1): - a.add(ext[i]) - return list(a) - def paymentAddress(self): + a = a.union((utxo.address for utxo in self.utxoscan())) + a = a.union(self.externalAddresses) + a = a.union((a for a in self.internalAddresses if a not in self.txs)) + return filterCrazyAddress(a) + def unseenAddrs(self): + return filterCrazyAddress( + [a for a in self.internalAddresses if a not in self.txs] + + [a for a in self.externalAddresses if a not in self.txs]) + def currentAddress(self): """ Get the external address at the cursor. The cursor is not moved. Returns: str: Base-58 encoded address. """ - return self.externalAddresses[self.cursor] + return self.externalAddresses[self.lastSeenExt+self.cursorExt] def privateExtendedKey(self, pw): """ Decode the private extended key for the account using the provided @@ -935,7 +1006,7 @@ def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams, co # Open the account. zerothAccount.open(cryptoKeyPriv) # Create the first payment address. - zerothAccount.generateNextPaymentAddress() + zerothAccount.generateGapAddresses() # Close the account to zero the key. zerothAccount.close() @@ -1025,7 +1096,7 @@ def test_accounts(self): self.assertEqual(addr1, addr2) acct = am.openAccount(0, pw) for n in range(20): - acct.getNextPaymentAddress() + acct.nextExternalAddress() v = 5 satoshis = v*1e8 txid = "abcdefghijkl" @@ -1062,13 +1133,6 @@ def test_newmaster(self): Test extended key derivation. ''' kpriv = newMaster(testSeed, nets.mainnet) - # --extKey: f2418d00085be520c6449ddb94b25fe28a1944b5604193bd65f299168796f862 - # --kpub: 0317a47499fb2ef0ff8dc6133f577cd44a5f3e53d2835ae15359dbe80c41f70c9b - # --kpub_branch0: 02dfed559fddafdb8f0041cdd25c4f9576f71b0e504ce61837421c8713f74fb33c - # --kpub_branch0_child1: 03745417792d529c66980afe36f364bee6f85a967bae117bc4d316b77e7325f50c - # --kpriv_branch0: 6469a8eb3ed6611cc9ee4019d44ec545f3174f756cc41f9867500efdda742dd9 - # --kpriv_branch0_child1: fb8efe52b3e4f31bc12916cbcbfc0e84ef5ebfbceb7197b8103e8009c3a74328 - self.assertEqual(kpriv.key.hex(), "f2418d00085be520c6449ddb94b25fe28a1944b5604193bd65f299168796f862") kpub = kpriv.neuter() self.assertEqual(kpub.key.hex(), "0317a47499fb2ef0ff8dc6133f577cd44a5f3e53d2835ae15359dbe80c41f70c9b") @@ -1091,7 +1155,78 @@ def test_change_addresses(self): # acct = acctManager.account(0) acct = acctManager.openAccount(0, pw) for i in range(10): - acct.getChangeAddress() - -if __name__ == "__main__": - pass + acct.nextInternalAddress() + def test_gap_handling(self): + internalAddrs = [ + "DskHpgbEb6hqkuHchHhtyojpehFToEtjQSo", + "Dsm4oCLnLraGDedfU5unareezTNT75kPbRb", + "DsdN6A9bWhKKJ7PAGdcDLxQrYPKEnjnDv2N", + "Dsifz8eRHvQrfaPXgHHLMDHZopFJq2pBPU9", + "DsmmzJiTTmpafdt2xx7LGk8ZW8cdAKe53Zx", + "DsVB47P4N23PK5C1RyaJdqkQDzVuCDKGQbj", + "DsouVzScdUUswvtCAv6nxRzi2MeufpWnELD", + "DsSoquT5SiDPfksgnveLv3r524k1v8RamYm", + "DsbVDrcfhbdLy4YaSmThmWN47xhxT6FC8XB", + "DsoSrtGYKruQLbvhA92xeJ6eeuAR1GGJVQA", + ] + + externalAddrs = [ + "DsmP6rBEou9Qr7jaHnFz8jTfrUxngWqrKBw", + "DseZQKiWhN3ceBwDJEgGhmwKD3fMbwF4ugf", + "DsVxujP11N72PJ46wgU9sewptWztYUy3L7o", + "DsYa4UBo379cRMCTpDLxYVfpMvMNBdAGrGS", + "DsVSEmQozEnsZ9B3D4Xn4H7kEedDyREgc18", + "DsifDp8p9mRocNj7XNNhGAsYtfWhccc2cry", + "DsV78j9aF8NBwegbcpPkHYy9cnPM39jWXZm", + "DsoLa9Rt1L6qAVT9gSNE5F5XSDLGoppMdwC", + "DsXojqzUTnyRciPDuCFFoKyvQFd6nQMn7Gb", + "DsWp4nShu8WxefgoPej1rNv4gfwy5AoULfV", + ] + + global DefaultGapLimit + gapLimit = DefaultGapLimit = 5 + pw = "abc".encode() + am = createNewAccountManager(testSeed, bytearray(0), pw, nets.mainnet) + account = am.openAccount(0, pw) + account.gapLimit = gapLimit + listsAreEqual = lambda a, b: len(a) == len(b) and all(x == y for x,y in zip(a,b)) + self.assertTrue(listsAreEqual(account.internalAddresses, internalAddrs[:gapLimit])) + # The external branch starts with the "last seen" at the zeroth address, so + # has one additional address to start. + self.assertTrue(listsAreEqual(account.externalAddresses, externalAddrs[:gapLimit+1])) + + account.addTxid(internalAddrs[0], "somerandomtxid") + newAddrs = account.generateGapAddresses() + self.assertEqual(len(newAddrs), 1) + self.assertEqual(newAddrs[0], internalAddrs[5]) + + # The zeroth external address is considered "seen", so this should not + # change anything. + account.addTxid(externalAddrs[0], "somerandomtxid") + newAddrs = account.generateGapAddresses() + self.assertEqual(len(newAddrs), 0) + + # Mark the 1st address as seen. + account.addTxid(externalAddrs[1], "somerandomtxid") + newAddrs = account.generateGapAddresses() + self.assertEqual(len(newAddrs), 1) + self.assertEqual(externalAddrs[1], account.currentAddress()) + + # cursor should be at index 0, last seen 1, max index 6, so calling + # nextExternalAddress 5 time should put the cursor at index 6, which is + # the gap limit. + for i in range(5): + account.nextExternalAddress() + self.assertEqual(account.currentAddress(), externalAddrs[6]) + + # one more should wrap the cursor back to 1, not zero, so the current + # address is lastSeenExt(=1) + cursor(=1) = 2 + a1 = account.nextExternalAddress() + self.assertEqual(account.currentAddress(), externalAddrs[2]) + self.assertEqual(a1, account.currentAddress()) + + # Sanity check that internal addresses are wrapping too. + for i in range(20): + account.nextInternalAddress() + addrs = account.internalAddresses + self.assertEqual(addrs[len(addrs)-1], internalAddrs[5]) diff --git a/examples/create_testnet_wallet.py b/examples/create_testnet_wallet.py index 54126962..267c2268 100644 --- a/examples/create_testnet_wallet.py +++ b/examples/create_testnet_wallet.py @@ -19,4 +19,4 @@ # Print the seed words and an address. print("Mnemonic seed\n-------------") print(" ".join(mnemonicSeed)) -print("Receive DCR at %s" % wallet.paymentAddress()) +print("Receive DCR at %s" % wallet.currentAddress()) diff --git a/pydecred/README.md b/pydecred/README.md index 97ba2f2a..68d87f08 100644 --- a/pydecred/README.md +++ b/pydecred/README.md @@ -30,8 +30,7 @@ print(json.dumps(bestBlock, indent=4, sort_keys=True)) Because the available dcrdata endpoints can change with new versions, the interface is generated by pulling a list of endpoints from the API itself. The acquired list does not include some endpoints, particularly the Insight API -endpoints. Additional paths can be provided to the constructor in a list of -paths as the `customPaths` parameter. +endpoints. You can print an endpoint guide to the console with `client.endpointGuide()`, diff --git a/pydecred/account.py b/pydecred/account.py index e0911c5f..8cbf8468 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -6,9 +6,17 @@ support. """ -from tinydecred.accounts import Account, EXTERNAL_BRANCH +from tinydecred.accounts import Account from tinydecred.util import tinyjson, helpers from tinydecred.crypto.crypto import AddressSecpPubKey +from tinydecred.pydecred import txscript + +log = helpers.getLogger("DCRACCT") + +# In addition to the standard internal and external branches, we'll have a third +# branch. This should help accomodate upcoming changes to dcrstakepool. See also +# https://github.com/decred/dcrstakepool/pull/514 +STAKE_BRANCH = 2 class KeySource(object): """ @@ -91,6 +99,8 @@ def __init__(self, *a, **k): self.tickets = [] self.stakeStats = TicketStats() self.stakePools = [] + self.blockchain = None + self.signals = None def __tojson__(self): obj = super().__tojson__() return helpers.recursiveUpdate(obj, { @@ -155,17 +165,16 @@ def allAddresses(self): Overload the base class to add the voting address. """ return self.addTicketAddresses(super().allAddresses()) - def addressesOfInterest(self): + def watchAddrs(self): """ Overload the base class to add the voting address. """ - return self.addTicketAddresses(super().addressesOfInterest()) + return self.addTicketAddresses(super().watchAddrs()) def votingKey(self): """ - This may change, but for now, the voting key is from the zeroth - child of the zeroth child of the external branch. + For now, the voting key is the zeroth child """ - return self.privKey.child(EXTERNAL_BRANCH).child(0).child(0).privateKey() + return self.privKey.child(STAKE_BRANCH).child(0).privateKey() def votingAddress(self): """ The voting address is the pubkey address (not pubkey-hash) for the @@ -204,7 +213,62 @@ def ticketStats(self): A getter for the stakeStats. """ return self.stakeStats - def sendToAddress(self, value, address, feeRate, blockchain): + def blockSignal(self, sig): + """ + Process a new block from the explorer. + + Arg: + sig (obj or string): The block explorer's json-decoded block + notification. + """ + block = sig["message"]["block"] + for newTx in block["Tx"]: + txid = newTx["TxID"] + # only grab the tx if its a transaction we care about. + if self.caresAboutTxid(txid): + tx = self.blockchain.tx(txid) + self.confirmTx(tx, self.blockchain.tipHeight) + # "Spendable" balance can change as utxo's mature, so update the + # balance at every block. + self.signals.balance(self.calcBalance(self.blockchain.tipHeight)) + def addressSignal(self, addr, txid): + """ + Process an address notification from the block explorer. + + Args: + addr (obj or string): The block explorer's json-decoded address + notification's address. + txid (obj or string): The block explorer's json-decoded address + notification's txid. + """ + tx = self.blockchain.tx(txid) + self.addTxid(addr, tx.txid()) + + matches = False + # scan the inputs for any spends. + for txin in tx.txIn: + op = txin.previousOutPoint + # spendTxidVout is a no-op if output is unknown + match = self.spendTxidVout(op.txid(), op.index) + if match: + matches += 1 + # scan the outputs for any new UTXOs + for vout, txout in enumerate(tx.txOut): + try: + _, addresses, _ = txscript.extractPkScriptAddrs(0, txout.pkScript, self.net) + except Exception: + # log.debug("unsupported script %s" % txout.pkScript.hex()) + continue + # convert the Address objects to strings. + if addr in (a.string() for a in addresses): + utxo = self.blockchain.txVout(txid, vout) + utxo.address = addr + self.addUTXO(utxo) + matches += 1 + if matches: + # signal the balance update + self.signals.balance(self.calcBalance(self.blockchain.tip["height"])) + def sendToAddress(self, value, address, feeRate): """ Send the value to the address. @@ -217,15 +281,15 @@ def sendToAddress(self, value, address, feeRate, blockchain): """ keysource = KeySource( priv = self.getPrivKeyForAddress, - internal = self.getChangeAddress, + internal = self.nextInternalAddress, ) - tx, spentUTXOs, newUTXOs = blockchain.sendToAddress(value, address, keysource, self.getUTXOs, feeRate) + tx, spentUTXOs, newUTXOs = self.blockchain.sendToAddress(value, address, keysource, self.getUTXOs, feeRate) self.addMempoolTx(tx) self.spendUTXOs(spentUTXOs) for utxo in newUTXOs: self.addUTXO(utxo) return tx - def purchaseTickets(self, qty, price, blockchain): + def purchaseTickets(self, qty, price): """ purchaseTickets completes the purchase of the specified tickets. The DecredAccount uses the blockchain to do the heavy lifting, but must @@ -234,7 +298,7 @@ def purchaseTickets(self, qty, price, blockchain): """ keysource = KeySource( priv = self.getPrivKeyForAddress, - internal = self.getChangeAddress, + internal = self.nextInternalAddress, ) pool = self.stakePool() pi = pool.purchaseInfo @@ -249,7 +313,7 @@ def purchaseTickets(self, qty, price, blockchain): count = qty, txFee = 0, # use network default ) - txs, spentUTXOs, newUTXOs = blockchain.purchaseTickets(keysource, self.getUTXOs, req) + txs, spentUTXOs, newUTXOs = self.blockchain.purchaseTickets(keysource, self.getUTXOs, req) if txs: # Add the split transactions self.addMempoolTx(txs[0]) @@ -265,5 +329,51 @@ def purchaseTickets(self, qty, price, blockchain): for utxo in newUTXOs: self.addUTXO(utxo) return txs[1] + def sync(self, blockchain, signals): + """ + Synchronize the UTXO set with the server. This should be the first + action after the account is opened or changed. + """ + self.blockchain = blockchain + self.signals = signals + signals.balance(self.balance) + self.generateGapAddresses() + + # First, look at addresses that have been generated but not seen. Run in + # loop until the gap limit is reached. + requestedTxs = 0 + addrs = self.unseenAddrs() + while addrs: + for addr in addrs: + for txid in blockchain.txsForAddr(addr): + requestedTxs += 1 + self.addTxid(addr, txid) + addrs = self.generateGapAddresses() + log.info("%d address transactions sets fetched" % requestedTxs) + + # start with a search for all known addresses + addresses = self.allAddresses() + + # Until the server stops returning UTXOs, keep requesting more addresses + # to check. + while True: + # Update the account with known UTXOs. + blockchainUTXOs = blockchain.UTXOs(addresses) + if not blockchainUTXOs: + break + self.resolveUTXOs(blockchainUTXOs) + addresses = self.generateGapAddresses() + if not addresses: + break + + # Subscribe to block and address updates. + blockchain.addressReceiver = self.addressSignal + blockchain.subscribeBlocks(self.blockSignal) + watchAddresses = self.watchAddrs() + if watchAddresses: + blockchain.subscribeAddresses(watchAddresses) + # Signal the new balance. + signals.balance(self.calcBalance(self.blockchain.tip["height"])) + return True tinyjson.register(DecredAccount) \ No newline at end of file diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 759c162c..f591a529 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -91,6 +91,14 @@ def getSocketURIs(uri): ps = fmt.format(prot, uri.netloc, "ps") return ws, ps +# To Do: Get the full list here. +InsightPaths = [ + "/tx/send", + "/insight/api/addr/{address}/utxo", + "/insight/api/addr/{address}/txs", + "insight/api/tx/send" +] + class DcrdataClient(object): """ DcrdataClient represents the base node. The only argument to the @@ -99,7 +107,7 @@ class DcrdataClient(object): timeFmt = "%Y-%m-%d %H:%M:%S" rfc3339Z = "%Y-%m-%dT%H:%M:%SZ" - def __init__(self, baseURI, customPaths=None, emitter=None): + def __init__(self, baseURI, emitter=None): """ Build the DcrdataPath tree. """ @@ -113,10 +121,9 @@ def __init__(self, baseURI, customPaths=None, emitter=None): atexit.register(self.close) root = self.root = DcrdataPath() self.listEntries = [] - customPaths = customPaths if customPaths else [] # /list returns a json list of enpoints with parameters in template format, base/A/{param}/B endpoints = http.get(self.baseApi + "/list", headers=GET_HEADERS) - endpoints += customPaths + endpoints += InsightPaths def getParam(part): if part.startswith('{') and part.endswith('}'): @@ -581,11 +588,6 @@ def connect(self): """ self.dcrdata = DcrdataClient( self.datapath, - customPaths=( - "/tx/send", - "/insight/api/addr/{address}/utxo", - "insight/api/tx/send" - ), emitter=self.pubsubSignal, ) self.updateTip() @@ -663,6 +665,17 @@ def UTXOs(self, addrs): ads = addrs[start:end] utxos += [self.processNewUTXO(u) for u in get(ads)] return utxos + def txsForAddr(self, addr): + """ + Get the transaction IDs for the provided address. + + Args: + addrs (string): Base-58 encoded address + """ + addrInfo = self.dcrdata.insight.api.addr.txs(addr) + if "transactions" not in addrInfo: + return [] + return addrInfo["transactions"] def txVout(self, txid, vout): """ Get a UTXO from the outpoint. The UTXO will not have the address set. @@ -889,8 +902,12 @@ def pubsubSignal(self, sig): elif sigType == "subscribeResp": # should check for error. pass + elif sigType == "ping": + # nothing to do here right now. May want to implement a + # auto-reconnect using this signal. + pass else: - raise Exception("unknown signal") + raise Exception("unknown signal %s" % repr(sigType)) except Exception as e: log.error("failed to process pubsub message: %s" % formatTraceback(e)) def changeScript(self, changeAddress): diff --git a/pydecred/tests.py b/pydecred/tests.py index a5af0eeb..37d247f8 100644 --- a/pydecred/tests.py +++ b/pydecred/tests.py @@ -1879,11 +1879,7 @@ def test_calc_signature_hash_reference(self): class TestDcrdata(unittest.TestCase): def client(self, **k): - return dcrdata.DcrdataClient("https://alpha.dcrdata.org", customPaths={ - "/tx/send", - "/insight/api/addr/{address}/utxo", - "insight/api/tx/send" - }, **k) + return dcrdata.DcrdataClient("https://alpha.dcrdata.org", **k) def test_websocket(self): """ "newblock": SigNewBlock, @@ -1904,6 +1900,7 @@ def test_get_block_header(self): blockchain = dcrdata.DcrdataBlockchain(os.path.join(tempDir, "db.db"), mainnet, "https://alpha.dcrdata.org") blockchain.connect() blockchain.blockHeader("298e5cc3d985bfe7f81dc135f360abe089edd4396b86d2de66b0cef42b21d980") + blockchain.close() def test_purchase_ticket(self): from tinydecred.crypto.secp256k1 import curve as Curve from tinydecred.crypto import rando diff --git a/ui/screens.py b/ui/screens.py index 62c27851..0caa6ff2 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -490,7 +490,7 @@ def showEvent(self, e): """ app = self.app if app.wallet: - address = app.wallet.paymentAddress() + address = app.wallet.currentAddress() self.address.setText(address) def balanceClicked(self): """ @@ -1156,7 +1156,7 @@ def buyTickets(self, wallet, qty): """ tip = self.blockchain.tip["height"] acct = wallet.openAccount - txs = acct.purchaseTickets(qty, self.lastPrice, self.blockchain) + txs = acct.purchaseTickets(qty, self.lastPrice) if txs: wallet.signals.balance(acct.calcBalance(tip)) self.app.home() diff --git a/util/database.py b/util/database.py index 2042678a..92e759e8 100644 --- a/util/database.py +++ b/util/database.py @@ -52,18 +52,20 @@ def __init__(self, database, name, datatypes, unique): self.existsQuery = KVExists.format(tablename=name) # = "SELECT EXISTS(SELECT * FROM kvtable WHERE k = ?);" self.deleteQuery = KVDelete.format(tablename=name) # = "DELETE FROM kvtable WHERE k = ?;" self.countQuery = KVCount.format(tablename=name) # = "SELECT COUNT(*) FROM kvtable;" - self.tid = None + self.conn = None def __enter__(self): """ Create a new connection for a every requesting thread. """ - tid = threadID() - if self.tid != tid: - self.conn = self.database.openDB() - self.tid = tid + if self.conn: + self.conn.close() + self.conn = self.database.openDB() self.open() return self def __exit__(self, xType, xVal, xTB): + if self.conn: + self.conn.close() + self.conn = None pass def __setitem__(self, k, v): cursor = self.conn.cursor() diff --git a/wallet.py b/wallet.py index e83e7381..e9f95d58 100644 --- a/wallet.py +++ b/wallet.py @@ -7,7 +7,6 @@ from threading import Lock as Mutex from tinydecred.util import tinyjson, helpers from tinydecred.crypto import crypto, mnemonic -from tinydecred.pydecred import txscript from tinydecred.pydecred.account import DecredAccount from tinydecred.accounts import createNewAccountManager @@ -266,19 +265,19 @@ def getNewAddress(self): Returns: str: The next unused external address. """ - a = self.selectedAccount.getNextPaymentAddress() + a = self.selectedAccount.nextExternalAddress() if self.blockchain: self.blockchain.subscribeAddresses(a) self.save() return a - def paymentAddress(self): + def currentAddress(self): """ Gets the payment address at the cursor. Returns: str: The current external address. """ - return self.selectedAccount.paymentAddress() + return self.selectedAccount.currentAddress() def balance(self): """ Get the balance of the currently selected account. @@ -287,65 +286,6 @@ def balance(self): Balance: The current account's Balance object. """ return self.selectedAccount.balance - def blockSignal(self, sig): - """ - Process a new block from the explorer. - - Arg: - sig (obj or string): The block explorer's json-decoded block - notification. - """ - block = sig["message"]["block"] - acct = self.selectedAccount - for newTx in block["Tx"]: - txid = newTx["TxID"] - # Only grab the tx if it's a transaction we care about. - if acct.caresAboutTxid(txid): - tx = self.blockchain.tx(txid) - acct.confirmTx(tx, self.blockchain.tipHeight) - # "Spendable" balance can change as UTXOs mature, so update the - # balance at every block. - self.signals.balance(acct.calcBalance(self.blockchain.tipHeight)) - def addressSignal(self, addr, txid): - """ - Process an address notification from the block explorer. - - Args: - addr (obj or string): The block explorer's json-decoded address - notification's address. - txid (obj or string): The block explorer's json-decoded address - notification's txid. - """ - acct = self.selectedAccount - - tx = self.blockchain.tx(txid) - acct.addTxid(addr, tx.txid()) - - matches = False - # Scan the inputs for any spends. - for txin in tx.txIn: - op = txin.previousOutPoint - # spendTxidVout is a no-op if output is unknown. - match = acct.spendTxidVout(op.txid(), op.index) - if match: - matches += 1 - # Scan the outputs for any new UTXOs. - for vout, txout in enumerate(tx.txOut): - try: - _, addresses, _ = txscript.extractPkScriptAddrs(0, txout.pkScript, acct.net) - except Exception: - # log.debug("unsupported script %s" % txout.pkScript.hex()) - continue - # Convert the Address objects to strings. - if addr in (a.string() for a in addresses): - log.debug("found new utxo for %s" % addr) - utxo = self.blockchain.txVout(txid, vout) - utxo.address = addr - acct.addUTXO(utxo) - matches += 1 - if matches: - # Signal the balance update. - self.signals.balance(acct.calcBalance(self.blockchain.tip["height"])) def sync(self): """ Synchronize the UTXO set with the server. This should be the first @@ -356,27 +296,10 @@ def sync(self): """ acctManager = self.acctManager acct = acctManager.account(0) - gapPolicy = 5 - acct.generateGapAddresses(gapPolicy) - watchAddresses = set() - - # Send the initial balance. - self.signals.balance(acct.balance) - addresses = acct.allAddresses() - - # Update the account with known UTXOs. - chain = self.blockchain - blockchainUTXOs = chain.UTXOs(addresses) - acct.resolveUTXOs(blockchainUTXOs) - # Subscribe to block and address updates. - chain.subscribeBlocks(self.blockSignal) - watchAddresses = acct.addressesOfInterest() - if watchAddresses: - chain.subscribeAddresses(watchAddresses, self.addressSignal) - # Signal the new balance. - b = acct.calcBalance(self.blockchain.tip["height"]) - self.signals.balance(b) + # send the initial balance. This was the balance the last time the + # wallet was saved, but may be innacurate until sync in complete. + acct.sync(self.blockchain, self.signals) self.save() return True def sendToAddress(self, value, address, feeRate=None): @@ -392,7 +315,7 @@ def sendToAddress(self, value, address, feeRate=None): failure. """ acct = self.openAccount - tx = acct.sendToAddress(value, address, feeRate, self.blockchain) + tx = acct.sendToAddress(value, address, feeRate) self.signals.balance(acct.calcBalance(self.blockchain.tip["height"])) self.save() return tx From cab42a6df00dedaaf20956859b6d19c5cd092687 Mon Sep 17 00:00:00 2001 From: buck54321 Date: Wed, 13 Nov 2019 11:31:36 -0600 Subject: [PATCH 05/12] paginate vsp accounts screen --- accounts.py | 46 +++--- pydecred/account.py | 6 +- pydecred/dcrdata.py | 126 +++++++++-------- pydecred/stakepool.py | 22 +-- ui/qutilities.py | 70 +++++++--- ui/screens.py | 317 +++++++++++++++++++++++++++++++----------- 6 files changed, 384 insertions(+), 203 deletions(-) diff --git a/accounts.py b/accounts.py index 2a531fab..6b4b4ac5 100644 --- a/accounts.py +++ b/accounts.py @@ -204,10 +204,10 @@ def __init__(self, pubKeyEncrypted, privKeyEncrypted, name, coinID, netID): self.netID = netID self.net = None setNetwork(self) - # For external addresses, the cursor can sit on the last seen address, + # For external addresses, the cursor can sit on the last seen address, # so start the lastSeen at the 0th external address. This is necessary - # because the currentAddress method grabs the address at the current - # cursor position, rather than the next. + # because the currentAddress method grabs the address at the current + # cursor position, rather than the next. self.lastSeenExt = 0 # For internal addresses, the cursor can sit below zero, since the # addresses are always retrieved with with nextInternalAddress. @@ -448,21 +448,19 @@ def addTxid(self, addr, txid): txids = self.txs[addr] if txid not in txids: txids.append(txid) - try: + # Advance the cursors as necessary. + if addr in self.externalAddresses: extIdx = self.externalAddresses.index(addr) if extIdx > self.lastSeenExt: diff = extIdx - self.lastSeenExt self.lastSeenExt = extIdx self.cursorExt = max(0, self.cursorExt-diff) - except ValueError: - try: - intIdx = self.internalAddresses.index(addr) - if intIdx > self.lastSeenInt: - diff = intIdx - self.lastSeenInt - self.lastSeenInt = intIdx - self.cursorInt = max(0, self.cursorInt-diff) - except ValueError: - raise Exception("attempting to add transaction %s for unknown address %s" % (txid, addr)) + elif addr in self.internalAddresses: + intIdx = self.internalAddresses.index(addr) + if intIdx > self.lastSeenInt: + diff = intIdx - self.lastSeenInt + self.lastSeenInt = intIdx + self.cursorInt = max(0, self.cursorInt-diff) def confirmTx(self, tx, blockHeight): """ Confirm a transaction. Sets height for any unconfirmed UTXOs in the @@ -535,11 +533,11 @@ def nextExternalAddress(self): """ extAddrs = self.externalAddresses addr = CrazyAddress - # Though unlikely, if an out or range key is generated, the account will + # Though unlikely, if an out or range key is generated, the account will # generate an additional address while addr == CrazyAddress: - # gap policy is to wrap. Wrapping brings the cursor back to index 1, - # since the index zero is the last seen address. + # gap policy is to wrap. Wrapping brings the cursor back to index 1, + # since the index zero is the last seen address. self.cursorExt += 1 if self.cursorExt > self.gapLimit: self.cursorExt = 1 @@ -557,11 +555,11 @@ def nextInternalAddress(self): """ intAddrs = self.internalAddresses addr = CrazyAddress - # Though unlikely, if an out or range key is generated, the account will + # Though unlikely, if an out or range key is generated, the account will # generate an additional address while addr == CrazyAddress: - # gap policy is to wrap. Wrapping brings the cursor back to index 1, - # since the index zero is the last seen address. + # gap policy is to wrap. Wrapping brings the cursor back to index 1, + # since the index zero is the last seen address. self.cursorInt += 1 if self.cursorInt > self.gapLimit: self.cursorInt = 1 @@ -572,7 +570,7 @@ def nextInternalAddress(self): return addr def lastSeen(self, addrs): """ - Find the index of the last seen address in the list of addresses. + Find the index of the last seen address in the list of addresses. The last seen address is taken as the last address for which there is an entry in the self.txs dict. @@ -630,7 +628,7 @@ def watchAddrs(self): return filterCrazyAddress(a) def unseenAddrs(self): return filterCrazyAddress( - [a for a in self.internalAddresses if a not in self.txs] + + [a for a in self.internalAddresses if a not in self.txs] + [a for a in self.externalAddresses if a not in self.txs]) def currentAddress(self): """ @@ -1191,7 +1189,7 @@ def test_gap_handling(self): account.gapLimit = gapLimit listsAreEqual = lambda a, b: len(a) == len(b) and all(x == y for x,y in zip(a,b)) self.assertTrue(listsAreEqual(account.internalAddresses, internalAddrs[:gapLimit])) - # The external branch starts with the "last seen" at the zeroth address, so + # The external branch starts with the "last seen" at the zeroth address, so # has one additional address to start. self.assertTrue(listsAreEqual(account.externalAddresses, externalAddrs[:gapLimit+1])) @@ -1200,7 +1198,7 @@ def test_gap_handling(self): self.assertEqual(len(newAddrs), 1) self.assertEqual(newAddrs[0], internalAddrs[5]) - # The zeroth external address is considered "seen", so this should not + # The zeroth external address is considered "seen", so this should not # change anything. account.addTxid(externalAddrs[0], "somerandomtxid") newAddrs = account.generateGapAddresses() @@ -1212,7 +1210,7 @@ def test_gap_handling(self): self.assertEqual(len(newAddrs), 1) self.assertEqual(externalAddrs[1], account.currentAddress()) - # cursor should be at index 0, last seen 1, max index 6, so calling + # cursor should be at index 0, last seen 1, max index 6, so calling # nextExternalAddress 5 time should put the cursor at index 6, which is # the gap limit. for i in range(5): diff --git a/pydecred/account.py b/pydecred/account.py index 8cbf8468..d6692e56 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -10,6 +10,7 @@ from tinydecred.util import tinyjson, helpers from tinydecred.crypto.crypto import AddressSecpPubKey from tinydecred.pydecred import txscript +from tinydecred.pydecred.stakepool import StakePool log = helpers.getLogger("DCRACCT") @@ -192,7 +193,8 @@ def setPool(self, pool): Args: pool (stakepool.StakePool): The stake pool object. """ - self.stakePools = [pool] + [p for p in self.stakePools if p.url != pool.url] + assert isinstance(pool, StakePool) + self.stakePools = [pool] + [p for p in self.stakePools if p.apiKey != pool.apiKey] def hasPool(self): """ hasPool will return True if the wallet has at least one pool set. @@ -349,7 +351,7 @@ def sync(self, blockchain, signals): requestedTxs += 1 self.addTxid(addr, txid) addrs = self.generateGapAddresses() - log.info("%d address transactions sets fetched" % requestedTxs) + log.debug("%d address transactions sets fetched" % requestedTxs) # start with a search for all known addresses addresses = self.allAddresses() diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index f591a529..59c40360 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -70,7 +70,7 @@ def getCallsignPath(self, *args, **kwargs): return uri if all([x in kwargs for x in argList]): return template % tuple(kwargs[x] for x in argList) - raise DcrDataException("ArgumentError", "Supplied arguments, %r, do not match any of the know call signatures, %r." % + raise DcrDataException("ArgumentError", "Supplied arguments, %r, do not match any of the know call signatures, %r." % (args if args else kwargs, [argList for argList, _ in self.callSigns])) def __getattr__(self, key): if key in self.subpaths: @@ -91,7 +91,7 @@ def getSocketURIs(uri): ps = fmt.format(prot, uri.netloc, "ps") return ws, ps -# To Do: Get the full list here. +# To Do: Get the full list here. InsightPaths = [ "/tx/send", "/insight/api/addr/{address}/utxo", @@ -109,7 +109,7 @@ class DcrdataClient(object): def __init__(self, baseURI, emitter=None): """ - Build the DcrdataPath tree. + Build the DcrdataPath tree. """ self.baseURI = baseURI.rstrip('/').rstrip("/api") self.baseApi = self.baseURI + "/api" @@ -167,7 +167,7 @@ def endpointList(self): return [entry[1] for entry in self.listEntries] def endpointGuide(self): """ - Print on endpoint per line. + Print on endpoint per line. Each line shows a translation from Python notation to a URL. """ print("\n".join(["%s -> %s" % entry for entry in self.listEntries])) @@ -239,7 +239,7 @@ class WebsocketClient(object): def __init__(self, path, emitter=None, exitObject=None, decoder=None, encoder=None): """ See python `socketserver documentation `_. for inherited attributes and methods. - + Parameters ---------- path: string @@ -308,7 +308,7 @@ def listenLoop(self): except OSError as e: if(e.errno == 9): #OSError: [Errno 9] Bad file descriptor - pass # socket was closed + pass # socket was closed break if stringBuffer == "": # server probably closed socket break @@ -324,11 +324,11 @@ def listenLoop(self): if self.exitObject: self.emitter(self.exitObject) self.handlinBidness = False - def send(self, msg): + def send(self, msg): if not self.socket: log.error("no socket") - return - try: + return + try: self.socket.send(self.encoder(msg)) except Exception as e: log.error("Error while sending websocket message: %s" % formatTraceback(e)) @@ -362,7 +362,7 @@ def __tojson__(self): tinyjson.register(APIBlock, tag="dcrdata.APIBlock") class TicketInfo: - def __init__(self, status, purchaseBlock, maturityHeight, expirationHeight, + def __init__(self, status, purchaseBlock, maturityHeight, expirationHeight, lotteryBlock, vote, revocation): self.status = status self.purchaseBlock = purchaseBlock @@ -400,11 +400,11 @@ def __tojson__(self): class UTXO(object): """ - The UTXO is part of the wallet API. BlockChains create and parse UTXO + The UTXO is part of the wallet API. BlockChains create and parse UTXO objects and fill fields as required by the Wallet. """ - def __init__(self, address, txid, vout, ts=None, scriptPubKey=None, - height=-1, amount=0, satoshis=0, maturity=None, + def __init__(self, address, txid, vout, ts=None, scriptPubKey=None, + height=-1, amount=0, satoshis=0, maturity=None, scriptClass=None, tinfo=None): self.address = address self.txid = txid @@ -519,8 +519,8 @@ def checkOutput(output, fee): fee (float): The transaction fee rate (/kB). Returns: - There is not return value. If an output is deemed invalid, an exception - is raised. + There is not return value. If an output is deemed invalid, an exception + is raised. """ if output.value < 0: raise Exception("transaction output amount is negative") @@ -535,7 +535,7 @@ def checkOutput(output, fee): def hashFromHex(s): """ Parse a transaction hash or block hash from a hexadecimal string. - + Args: s (str): A byte-revesed, hexadecimal string encoded hash. @@ -567,7 +567,7 @@ def __init__(self, dbPath, params, datapath, skipConnect=False): """ self.db = KeyValueDatabase(dbPath) self.params = params - # The blockReceiver and addressReceiver will be set when the respective + # The blockReceiver and addressReceiver will be set when the respective # subscribe* method is called. self.blockReceiver = None self.addressReceiver = None @@ -587,7 +587,7 @@ def connect(self): Connect to dcrdata. """ self.dcrdata = DcrdataClient( - self.datapath, + self.datapath, emitter=self.pubsubSignal, ) self.updateTip() @@ -602,7 +602,7 @@ def subscribeBlocks(self, receiver): Subscribe to new block notifications. Args: - receiver (func(obj)): A function or method that accepts the block + receiver (func(obj)): A function or method that accepts the block notifications. """ self.blockReceiver = receiver @@ -613,7 +613,7 @@ def subscribeAddresses(self, addrs, receiver=None): Args: addrs (list(str)): List of base-58 encoded addresses. - receiver (func(obj)): A function or method that accepts the address + receiver (func(obj)): A function or method that accepts the address notifications. """ log.debug("subscribing to addresses %s" % repr(addrs)) @@ -624,11 +624,11 @@ def subscribeAddresses(self, addrs, receiver=None): self.dcrdata.subscribeAddresses(addrs) def processNewUTXO(self, utxo): """ - Processes an as-received blockchain utxo. + Processes an as-received blockchain utxo. Check for coinbase or stakebase, and assign a maturity as necessary. - + Args: - utxo UTXO: A new unspent transaction output from blockchain. + utxo UTXO: A new unspent transaction output from blockchain. Returns: bool: True if no errors are encountered. @@ -639,7 +639,7 @@ def processNewUTXO(self, utxo): # This is a coinbase or stakebase transaction. Set the maturity. utxo.maturity = utxo.height + self.params.CoinbaseMaturity if utxo.isTicket(): - # Mempool tickets will be returned from the utxo endpoint, but + # Mempool tickets will be returned from the utxo endpoint, but # the tinfo endpoint is an error until mined. try: rawTinfo = self.dcrdata.tx.tinfo(utxo.txid) @@ -649,7 +649,7 @@ def processNewUTXO(self, utxo): return utxo def UTXOs(self, addrs): """ - UTXOs will produce any known UTXOs for the list of addresses. + UTXOs will produce any known UTXOs for the list of addresses. Args: addrs (list(str)): List of base-58 encoded addresses. @@ -698,10 +698,10 @@ def txVout(self, txid, vout): def tx(self, txid): """ - Get the MsgTx. Retreive it from the blockchain if necessary. + Get the MsgTx. Retreive it from the blockchain if necessary. Args: - txid (str): A hex encoded transaction ID to fetch. + txid (str): A hex encoded transaction ID to fetch. Returns: MsgTx: The transaction. @@ -712,7 +712,7 @@ def tx(self, txid): encoded = ByteArray(txDB[hashKey]) return msgtx.MsgTx.deserialize(encoded) except database.NoValue: - try: + try: # Grab the hex encoded transaction txHex = self.dcrdata.tx.hex(txid) if not txHex: @@ -739,8 +739,8 @@ def blockForTx(self, txid): except database.NoValue: # If the blockhash is not in the database, get it from dcrdata decodedTx = self.dcrdata.tx(txid) - if ("block" not in decodedTx or - "blockhash" not in decodedTx["block"] or + if ("block" not in decodedTx or + "blockhash" not in decodedTx["block"] or decodedTx["block"]["blockhash"] == ""): return None hexHash = decodedTx["block"]["blockhash"] @@ -749,10 +749,10 @@ def blockForTx(self, txid): return header def decodedTx(self, txid): """ - decodedTx will produce a transaction as a Python dict. + decodedTx will produce a transaction as a Python dict. Args: - txid (str): Hex-encoded transaction ID. + txid (str): Hex-encoded transaction ID. Returns: dict: A Python dict with transaction information. @@ -765,7 +765,7 @@ def blockHeader(self, hexHash): Args: bHash (str): The block hash of the block header. - Returns: + Returns: BlockHeader: An object which implements the BlockHeader API. """ with self.headerDB as headers: @@ -784,7 +784,7 @@ def blockHeader(self, hexHash): def blockHeaderByHeight(self, height): """ Get the block header by height. The blcck header is retreived from the - blockchain if necessary, in which case it is stored. + blockchain if necessary, in which case it is stored. Args: height int: The block height @@ -815,7 +815,7 @@ def stakeDiff(self): return self.dcrdata.stake.diff()["next"] def updateTip(self): """ - Update the tip block. If the wallet is subscribed to block updates, + Update the tip block. If the wallet is subscribed to block updates, this can be used sparingly. """ try: @@ -826,7 +826,7 @@ def updateTip(self): raise Exception("no tip data retrieved") def relayFee(self): """ - Return the current transaction fee. + Return the current transaction fee. Returns: int: Atoms per kB of encoded transaction. @@ -850,12 +850,12 @@ def sendToAddress(self, value, address, keysource, utxosource, feeRate=None): Args: value int: The amount to send, in atoms. address str: The base-58 encoded address. - keysource func(str) -> PrivateKey: A function that returns the + keysource func(str) -> PrivateKey: A function that returns the private key for an address. - utxosource func(int, func(UTXO) -> bool) -> list(UTXO): A function - that takes an amount in atoms, and an optional filtering - function. utxosource returns a list of UTXOs that sum to >= the - amount. If the filtering function is provided, UTXOs for which + utxosource func(int, func(UTXO) -> bool) -> list(UTXO): A function + that takes an amount in atoms, and an optional filtering + function. utxosource returns a list of UTXOs that sum to >= the + amount. If the filtering function is provided, UTXOs for which the function return a falsey value will not be included in the returned UTXO list. MsgTx: The newly created transaction on success, `False` on failure. @@ -927,8 +927,8 @@ def confirmUTXO(self, utxo, block=None, tx=None): if not tx: # No tx found is an issue, so pass the exception. tx = self.tx(utxo.txid) - try: - # No block found is not an error. + try: + # No block found is not an error. if not block: block = self.blockForTx(utxo.txid) utxo.confirm(block, tx, self.params) @@ -938,7 +938,7 @@ def confirmUTXO(self, utxo, block=None, tx=None): return False def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf=1, randomizeChangeIdx=True): """ - Send the `TxOut`s to the address. + Send the `TxOut`s to the address. mostly based on: (dcrwallet/wallet/txauthor).NewUnsignedTransaction @@ -947,19 +947,19 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf Args: outputs (list(TxOut)): The transaction outputs to send. - keysource func(str) -> PrivateKey: A function that returns the + keysource func(str) -> PrivateKey: A function that returns the private key for an address. - utxosource func(int, func(UTXO) -> bool) -> list(UTXO): A function - that takes an amount in atoms, and an optional filtering - function. utxosource returns a list of UTXOs that sum to >= the - amount. If the filtering function is provided, UTXOs for which + utxosource func(int, func(UTXO) -> bool) -> list(UTXO): A function + that takes an amount in atoms, and an optional filtering + function. utxosource returns a list of UTXOs that sum to >= the + amount. If the filtering function is provided, UTXOs for which the function return a falsey value will not be included in the returned UTXO list. Returns: MsgTx: The sent transaction. list(UTXO): The spent UTXOs. - list(UTXO): Length 1 array containing the new change UTXO. + list(UTXO): Length 1 array containing the new change UTXO. """ total = 0 inputs = [] @@ -991,8 +991,8 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf opCodeClass = txscript.getP2PKHOpCode(txout.pkScript) tree = wire.TxTreeRegular if opCodeClass == txscript.opNonstake else wire.TxTreeStake op = msgtx.OutPoint( - txHash=tx.hash(), - idx=utxo.vout, + txHash=tx.hash(), + idx=utxo.vout, tree=tree ) txIn = msgtx.TxIn(previousOutPoint=op, valueIn=txout.value) @@ -1045,7 +1045,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf pkScript = scripts[i] sigScript = txin.signatureScript scriptClass, addrs, numAddrs = txscript.extractPkScriptAddrs(0, pkScript, self.params) - script = txscript.signTxOutput(self.params, newTx, i, pkScript, + script = txscript.signTxOutput(self.params, newTx, i, pkScript, txscript.SigHashAll, keysource, sigScript, crypto.STEcdsaSecp256k1) txin.signatureScript = script self.broadcast(newTx.txHex()) @@ -1068,12 +1068,12 @@ def purchaseTickets(self, keysource, utxosource, req): purchaseTickets indicates to the wallet that a ticket should be purchased using all currently available funds. The ticket address parameter in the request can be nil in which case the ticket address associated with the - wallet instance will be used. Also, when the spend limit in the request - is greater than or equal to 0, tickets that cost more than that limit + wallet instance will be used. Also, when the spend limit in the request + is greater than or equal to 0, tickets that cost more than that limit will return an error that not enough funds are available. """ self.updateTip() - # account minConf is zero for regular outputs for now. Need to make that + # account minConf is zero for regular outputs for now. Need to make that # adjustable. # if req.minConf < 0: # raise Exception("negative minconf") @@ -1107,7 +1107,7 @@ def purchaseTickets(self, keysource, utxosource, req): raise Exception("ticket price %f above spend limit %f" % (ticketPrice, req.spendLimit)) # Check that pool fees is zero, which will result in invalid zero-valued - # outputs. + # outputs. if req.poolFees == 0: raise Exception("no pool fee specified") @@ -1123,7 +1123,7 @@ def purchaseTickets(self, keysource, utxosource, req): if not req.votingAddress: raise Exception("voting address not set in purchaseTickets request") - # decode the string addresses. This is the P2SH multi-sig script + # decode the string addresses. This is the P2SH multi-sig script # address, not the wallets voting address, which is only one of the two # pubkeys included in the redeem P2SH script. votingAddress = txscript.decodeAddress(req.votingAddress, self.params) @@ -1172,8 +1172,6 @@ def purchaseTickets(self, keysource, utxosource, req): # immediately be consumed as tickets. splitTxAddr = keysource.internal() - print("splitTxAddr: %s" % splitTxAddr) - # TODO: Don't reuse addresses # TODO: Consider wrapping. see dcrwallet implementation. splitPkScript = txscript.makePayToAddrScript(splitTxAddr, self.params) @@ -1190,7 +1188,7 @@ def purchaseTickets(self, keysource, utxosource, req): userAmt = neededPerTicket - poolFeeAmt poolAmt = poolFeeAmt - # Pool amount. + # Pool amount. splitOuts.append(msgtx.TxOut( value = poolAmt, pkScript = splitPkScript, @@ -1272,12 +1270,12 @@ def purchaseTickets(self, keysource, utxosource, req): txscript.signP2PKHMsgTx(ticket, forSigning, keysource, self.params) - # dcrwallet actually runs the pk scripts through the script - # Engine as another validation step. Engine not implemented in + # dcrwallet actually runs the pk scripts through the script + # Engine as another validation step. Engine not implemented in # Python yet. # validateMsgTx(op, ticket, creditScripts(forSigning)) - # For now, don't allow any high fees (> 1000x default). Could later + # For now, don't allow any high fees (> 1000x default). Could later # be a property of the account. if txscript.paysHighFees(eop.amt, ticket): raise Exception("high fees detected") diff --git a/pydecred/stakepool.py b/pydecred/stakepool.py index 5468ca1f..ab88afb0 100644 --- a/pydecred/stakepool.py +++ b/pydecred/stakepool.py @@ -12,7 +12,7 @@ def resultIsSuccess(res): """ JSON-decoded stake pool responses have a common base structure that enables - a universal success check. + a universal success check. Args: res (object): The freshly-decoded-from-JSON response. @@ -121,15 +121,15 @@ def __fromjson__(obj): class StakePool(object): """ A StakePool is a voting service provider, uniquely defined by it's URL. The - StakePool class has methods for interacting with the VSP API. StakePool is - JSON-serializable if used with tinyjson, so can be stored as part of an + StakePool class has methods for interacting with the VSP API. StakePool is + JSON-serializable if used with tinyjson, so can be stored as part of an Account in the wallet. """ def __init__(self, url, apiKey): """ Args: url (string): The stake pool URL. - apiKey (string): The API key assigned to the VSP account during + apiKey (string): The API key assigned to the VSP account during registration. """ self.url = url @@ -137,7 +137,7 @@ def __init__(self, url, apiKey): # a call to StakePool.authorize before using the StakePool. self.net = None # The signingAddress (also called a votingAddress in other contexts) is - # the P2SH 1-of-2 multi-sig address that spends SSTX outputs. + # the P2SH 1-of-2 multi-sig address that spends SSTX outputs. self.signingAddress = None self.apiKey = apiKey self.lastConnection = 0 @@ -160,7 +160,7 @@ def __fromjson__(obj): @staticmethod def providers(net): """ - A static method to get the current Decred VSP list. + A static method to get the current Decred VSP list. Args: net (string): The network name. @@ -192,12 +192,12 @@ def headers(self): return {"Authorization": "Bearer %s" % self.apiKey} def validate(self, addr): """ - Validate performs some checks that the PurchaseInfo provided by the - stake pool API is valid for this given voting address. Exception is + Validate performs some checks that the PurchaseInfo provided by the + stake pool API is valid for this given voting address. Exception is raised on failure to validate. Args: - addr (string): The base58-encoded pubkey address that the wallet + addr (string): The base58-encoded pubkey address that the wallet uses to vote. """ pi = self.purchaseInfo @@ -219,11 +219,11 @@ def validate(self, addr): raise Exception("signing pubkey not found in redeem script") def authorize(self, address, net): """ - Authorize the stake pool for the provided address and network. Exception + Authorize the stake pool for the provided address and network. Exception is raised on failure to authorize. Args: - address (string): The base58-encoded pubkey address that the wallet + address (string): The base58-encoded pubkey address that the wallet uses to vote. net (object): The network parameters. """ diff --git a/ui/qutilities.py b/ui/qutilities.py index 6cb46288..cf8c85a3 100644 --- a/ui/qutilities.py +++ b/ui/qutilities.py @@ -42,11 +42,11 @@ def __init__(self): self.threads = [] def makeThread(self, func, callback=None, *args, **kwargs): """ - Create and start a `SmartThread`. + Create and start a `SmartThread`. A reference to the thread is stored in `self.threads` until it completes :param function func: The function to run in the thread - :param function callback: A function to call when the thread has completed. Any results returned by `func` will be passed as the first positional argument. + :param function callback: A function to call when the thread has completed. Any results returned by `func` will be passed as the first positional argument. :param list args: Positional arguments to pass to `func` :param dict kwargs: Keyword arguments to pass to `func` """ @@ -70,10 +70,10 @@ def __init__(self, func, callback, *args, qtConnectType=QtCore.Qt.AutoConnection """ Args: func (function): The function to run in a separate thread. - callback (function): A function to receive the return value from - `func`. + callback (function): A function to receive the return value from + `func`. *args: optional positional arguements to pass to `func`. - qtConnectType: Signal synchronisity. + qtConnectType: Signal synchronisity. **kwargs: optional keyword arguments to pass to `func`. """ super().__init__() @@ -95,7 +95,7 @@ def run(self): self.returns = False def callitback(self): """ - QThread Slot connected to the connect Signal. Send the value returned + QThread Slot connected to the connect Signal. Send the value returned from `func` to the callback function. """ self.callback(self.returns) @@ -188,7 +188,7 @@ def consoleScrollAction(self, action, mn=None, mx=None): class QToggle(QtWidgets.QAbstractButton): """ - Implementation of a clean looking toggle switch translated from + Implementation of a clean looking toggle switch translated from https://stackoverflow.com/a/38102598/1124661 QAbstractButton::setDisabled to disable """ @@ -296,14 +296,14 @@ def setToggle(self, toggle): def makeWidget(widgetClass, layoutDirection="vertical", parent=None): """ - The creates a tuple of (widget, layout), with layout of type specified with + The creates a tuple of (widget, layout), with layout of type specified with layout direction. - layout's parent will be widget. layout's alignment is set to top-left, and + layout's parent will be widget. layout's alignment is set to top-left, and margins are set to 0 on both layout and widget - + widgetClass (QtWidgets.QAbstractWidget:) The type of widget to make. - layoutDirection (str): optional. default "vertical". One of - ("vertical","horizontal","grid"). Determines the type of layout applied + layoutDirection (str): optional. default "vertical". One of + ("vertical","horizontal","grid"). Determines the type of layout applied to the widget. """ widget = widgetClass(parent) @@ -374,7 +374,7 @@ def makeLabel(s, fontSize, a=ALIGN_CENTER, **k): Args: s (str): The label text. fontSize (int): Pixel size of the label font. - a (Qt.QAlignment): The text alignment in the label. + a (Qt.QAlignment): The text alignment in the label. default QtCore.Qt.AlignCenter **k: Additional keyword arguments to pass to setProperties. """ @@ -383,9 +383,9 @@ def makeLabel(s, fontSize, a=ALIGN_CENTER, **k): lbl.setAlignment(a) return lbl -def setProperties(lbl, color=None, fontSize=None, fontFamily=None): +def setProperties(lbl, color=None, fontSize=None, fontFamily=None, underline=False): """ - A few common properties of QLabels. + A few common properties of QLabels. """ if color: palette = lbl.palette() @@ -399,12 +399,14 @@ def setProperties(lbl, color=None, fontSize=None, fontFamily=None): lbl.setFont(font) if fontFamily: font.setFamily(fontFamily) + if underline: + font.setUnderline(True) lbl.setFont(font) return lbl def pad(wgt, t, r, b, l): """ - Add padding around the widget by wrapping it in another widget. + Add padding around the widget by wrapping it in another widget. """ w, lyt = makeWidget(QtWidgets.QWidget, HORIZONTAL) lyt.addWidget(wgt) @@ -419,7 +421,7 @@ def clearLayout(layout, delete=False): layout (QAbstractLayout): Layout to clear delete (bool): Default False. Whether or not to delete the widget as well """ - for i in reversed(range(layout.count())): + for i in reversed(range(layout.count())): widget = layout.itemAt(i).widget() widget.setParent(None) if delete: @@ -429,14 +431,44 @@ def clearLayout(layout, delete=False): def layoutWidgets(layout): """ generator to iterate the widgets in a layout - + Args: layout (QAbstractLayout): Layout to clear delete (bool): Default False. Whether or not to delete the widget as well. """ - for i in range(layout.count()): + for i in range(layout.count()): yield layout.itemAt(i).widget() +def _setMouseDown(wgt, e): + if e.button() == QtCore.Qt.LeftButton: + wgt._mousedown = True + +def _releaseMouse(wgt, e): + if e.button() == QtCore.Qt.LeftButton and wgt._mousedown: + wgt._clickcb() + wgt._mousedown = False + +def _mouseMoved(wgt, e): + """ + When the mouse is moved, check whether the mouse is within the bounds of + the widget. If not, set _mousedown to False. The user must click and + release without the mouse leaving the label to trigger the callback. + """ + if wgt._mousedown == False: + return + qSize = wgt.size() + ePos = e.pos() + x, y = ePos.x(), ePos.y() + if x < 0 or y < 0 or x > qSize.width() or y > qSize.height(): + wgt._mousedown = False + +def addClickHandler(wgt, cb): + wgt._mousedown = False + wgt._clickcb = cb + wgt.mousePressEvent = lambda e, w=wgt: _setMouseDown(wgt, e) + wgt.mouseReleaseEvent = lambda e, w=wgt: _releaseMouse(wgt, e) + wgt.mouseMoveEvent = lambda e, w=wgt: _mouseMoved(wgt, e) + lightThemePalette = QtGui.QPalette() lightThemePalette.setColor(QtGui.QPalette.Window, QtGui.QColor("#ffffff")) lightThemePalette.setColor(QtGui.QPalette.WindowText, QtGui.QColor("#333333")) diff --git a/ui/screens.py b/ui/screens.py index 0caa6ff2..57a09606 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -45,12 +45,13 @@ def pixmapFromSvg(filename, w, h): class TinyDialog(QtWidgets.QFrame): """ TinyDialog is a widget for handling Screen instances. This si the primary - window of the TinyDecred application. It has a fixed (tiny!) size. + window of the TinyDecred application. It has a fixed (tiny!) size. """ maxWidth = 525 maxHeight = 375 targetPadding = 15 popSig = QtCore.pyqtSignal(Q.PyObj) + stackSig = QtCore.pyqtSignal(Q.PyObj) topMenuHeight = 26 successSig = QtCore.pyqtSignal(str) errorSig = QtCore.pyqtSignal(str) @@ -64,17 +65,19 @@ def __init__(self, app): self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.popSig.connect(self.pop_) self.pop = lambda w=None: self.popSig.emit(w) + self.stackSig.connect(self.stack_) + self.stack = lambda p: self.stackSig.emit(p) # Set the width and height explicitly. Keep it tiny. - screenGeo = app.qApp.primaryScreen().availableGeometry() + screenGeo = app.qApp.primaryScreen().availableGeometry() self.w = self.maxWidth if screenGeo.width() >= self.maxWidth else screenGeo.width() self.h = self.maxHeight if screenGeo.height() >= self.maxHeight else screenGeo.height() availPadX = (screenGeo.width() - self.w) / 2 self.padX = self.targetPadding if availPadX >= self.targetPadding else availPadX self.setGeometry( - screenGeo.x() + screenGeo.width() - self.w - self.padX, - screenGeo.y() + screenGeo.height() - self.h, - self.w, + screenGeo.x() + screenGeo.width() - self.w - self.padX, + screenGeo.y() + screenGeo.height() - self.h, + self.w, self.h ) @@ -82,7 +85,7 @@ def __init__(self, app): self.showSuccess = lambda s: self.successSig.emit(s) self.errorSig.connect(self.showError_) self.showError = lambda s: self.errorSig.emit(s) - + self.mainLayout = QtWidgets.QVBoxLayout(self) self.setFrameShape(QtWidgets.QFrame.Box) self.setLineWidth(1) @@ -100,7 +103,7 @@ def __init__(self, app): app.registerSignal(ui.WORKING_SIGNAL, lambda: self.working.setVisible(True)) app.registerSignal(ui.DONE_SIGNAL, lambda: self.working.setVisible(False)) - # If enabled by a Screen instance, the user can navigate back to the + # If enabled by a Screen instance, the user can navigate back to the # previous screen. self.backIcon = ClickyLabel(self.backClicked) self.backIcon.setPixmap(pixmapFromSvg("back.svg", 20, 20)) @@ -142,7 +145,7 @@ def showEvent(self, e): def closeEvent(self, e): self.hide() e.ignore() - def stack(self, w): + def stack_(self, w): """ Add the Screen instance to the stack, making it the displayed screen. """ @@ -158,7 +161,7 @@ def stack(self, w): w.stacked() def pop_(self, screen=None): """ - Pop the top screen from the stack. If a Screen instance is provided, + Pop the top screen from the stack. If a Screen instance is provided, only pop if that is the top screen. Args: @@ -200,7 +203,7 @@ def setIcons(self, top): """ Set the icons according to the Screen's settings. - Args: + Args: top (Screen): The top screen. """ self.backIcon.setVisible(top.isPoppable) @@ -214,14 +217,14 @@ def homeClicked(self): self.pop() def closeClicked(self): """ - User has clicked close. Since TinyDecred is a system tray application, - the window and it's application panel icon are hidden, but the - application does not close. + User has clicked close. Since TinyDecred is a system tray application, + the window and it's application panel icon are hidden, but the + application does not close. """ self.hide() def backClicked(self): """ - The clicked slot for the back icon. Pops the top screen. + The clicked slot for the back icon. Pops the top screen. """ self.pop() def showError_(self, msg): @@ -268,14 +271,14 @@ def paintEvent(self, e): painter.setPen(self.borderPen) painter.setFont(self.mainFont) # painter.setBrush(self.bgBrush) - + pad = 15 fullWidth = self.geometry().width() column = QtCore.QRect(0, 0, fullWidth - 4*pad, 10000) textBox = painter.boundingRect( - column, + column, self.textFlags, self.msg ) @@ -287,11 +290,11 @@ def paintEvent(self, e): w = textBox.width() + 2*pad pTop = TinyDialog.topMenuHeight + pad - + painter.fillRect(lrPad, pTop, outerWidth, outerHeight, self.bgBrush) painter.drawRect(lrPad, pTop, outerWidth, outerHeight) painter.drawText( - QtCore.QRect(lrPad + pad, pTop + pad, w, 10000), + QtCore.QRect(lrPad + pad, pTop + pad, w, 10000), self.textFlags, self.msg ) @@ -299,7 +302,7 @@ def paintEvent(self, e): class Screen(QtWidgets.QWidget): """ Screen is all the user sees in the main application window. All UI widgets - should inherit Screen. + should inherit Screen. """ def __init__(self, app): """ @@ -308,16 +311,16 @@ def __init__(self, app): """ super().__init__() self.app = app - # isPoppable indicates whether this screen can be popped by the user + # isPoppable indicates whether this screen can be popped by the user # when this screen is displayed. self.isPoppable = False - # canGoHome indicates whether the user can navigate directly to the - # home page when this screen is displayed. + # canGoHome indicates whether the user can navigate directly to the + # home page when this screen is displayed. self.canGoHome = True self.animations = {} # The layout the that child will use is actually a 2nd descendent of the - # primary Screen layout. Stretches are used to center a widget + # primary Screen layout. Stretches are used to center a widget # regardless of size. vLayout = QtWidgets.QVBoxLayout(self) vLayout.addStretch(1) @@ -333,17 +336,17 @@ def runAnimation(self, ani): Run an animation if its trigger is registered. By default, no animations are attached to the screen. - Args: + Args: ani (str): The animation trigger. """ if ani in self.animations: return self.animations[ani].start() def setFadeIn(self, v): """ - Set the screen to use a fade-in animation. + Set the screen to use a fade-in animation. Args: - v (bool): If True, run the fade-in animation when its trigger is + v (bool): If True, run the fade-in animation when its trigger is received. False will disable the animation. """ if v: @@ -359,7 +362,7 @@ def setFadeIn(self, v): class HomeScreen(Screen): """ - The standard home screen for a TinyDecred account. + The standard home screen for a TinyDecred account. """ def __init__(self, app): """ @@ -376,7 +379,7 @@ def __init__(self, app): self.balance = None self.stakeScreen = StakingScreen(app) - # Update the home screen when the balance signal is received. + # Update the home screen when the balance signal is received. app.registerSignal(ui.BALANCE_SIGNAL, self.balanceUpdated) app.registerSignal(ui.SYNC_SIGNAL, self.setTicketStats) @@ -387,7 +390,7 @@ def __init__(self, app): # Display the current account balance. logo = QtWidgets.QLabel() logo.setPixmap(pixmapFromSvg(DCR.LOGO, 40, 40)) - + self.totalBalance = b = ClickyLabel(self.balanceClicked, "0.00") Q.setProperties(b, fontFamily="Roboto-Bold", fontSize=36) self.totalUnit = Q.makeLabel("DCR", 18, color="#777777") @@ -398,12 +401,12 @@ def __init__(self, app): self.statsLbl = Q.makeLabel("", 15) - tot, totLyt = Q.makeSeries(Q.HORIZONTAL, + tot, totLyt = Q.makeSeries(Q.HORIZONTAL, self.totalBalance, self.totalUnit, ) - bals, balsLyt = Q.makeSeries(Q.VERTICAL, + bals, balsLyt = Q.makeSeries(Q.VERTICAL, tot, self.availBalance, self.statsLbl, @@ -416,8 +419,8 @@ def __init__(self, app): align=Q.ALIGN_LEFT, ) - row, rowLyt = Q.makeSeries(Q.HORIZONTAL, - logoCol, + row, rowLyt = Q.makeSeries(Q.HORIZONTAL, + logoCol, Q.STRETCH, bals, ) @@ -437,7 +440,7 @@ def __init__(self, app): new, ) - col, colLyt = Q.makeSeries(Q.VERTICAL, + col, colLyt = Q.makeSeries(Q.VERTICAL, header, self.address, ) @@ -511,7 +514,7 @@ def balanceUpdated(self, bal): self.setTicketStats() def setTicketStats(self): """ - Set the staking statistics. + Set the staking statistics. """ acct = self.app.wallet.selectedAccount balance = self.balance @@ -522,7 +525,7 @@ def setTicketStats(self): self.ticketStats = stats def spendClicked(self, e=None): """ - Display a form to send funds to an address. A Qt Slot, but any event + Display a form to send funds to an address. A Qt Slot, but any event parameter is ignored. """ self.app.appWindow.stack(self.app.sendScreen) @@ -579,10 +582,10 @@ def showPwToggled(self, state, switch): QToggle callback. Set plain text password field display. Args: - state (bool): The toggle switch state. + state (bool): The toggle switch state. switch (QToggle): The toggle switch instance. """ - if state: + if state: self.pwInput.setEchoMode(QtWidgets.QLineEdit.Normal) else: self.pwInput.setEchoMode(QtWidgets.QLineEdit.Password) @@ -593,19 +596,19 @@ def pwSubmit(self): self.callback(self.pwInput.text()) def withCallback(self, callback, *args, **kwargs): """ - Sets the screens callback function, which will be called when the user - presses the return key while the password field has focus. + Sets the screens callback function, which will be called when the user + presses the return key while the password field has focus. Args: callback (func(str, ...)): A function to receive the users password. - *args: optional. Positional arguments to pass to the callback. The + *args: optional. Positional arguments to pass to the callback. The arguments are shifted and the password will be the zeroth argument. - **kwargs: optional. Keyword arguments to pass through to the + **kwargs: optional. Keyword arguments to pass through to the callback. Returns: - The screen itself is returned as a convenience. + The screen itself is returned as a convenience. """ self.callback = lambda p, a=args, k=kwargs: callback(p, *a, **k) return self @@ -619,7 +622,7 @@ def __init__(self, callback, *a): Args: callback (func): A callback function to be called when the label is clicked. - *a: Any additional arguments are passed directly to the parent + *a: Any additional arguments are passed directly to the parent QLabel constructor. """ super().__init__(*a) @@ -635,7 +638,7 @@ def mouseReleaseEvent(self, e): def mouseMoveEvent(self, e): """ When the mouse is moved, check whether the mouse is within the bounds of - the label. If not, set mouseDown to False. The user must click and + the label. If not, set mouseDown to False. The user must click and release without the mouse leaving the label to trigger the callback. """ if self.mouseDown == False: @@ -649,7 +652,7 @@ def mouseMoveEvent(self, e): class InitializationScreen(Screen): """ A screen shown when no wallet file is detected. This screen offers options - for creating a new wallet or loading an existing wallet. + for creating a new wallet or loading an existing wallet. """ def __init__(self, app): """ @@ -679,7 +682,7 @@ def __init__(self, app): self.restoreBttn.clicked.connect(self.restoreClicked) def initClicked(self): """ - Qt Slot for the new wallet button. Initializes the creation of a new + Qt Slot for the new wallet button. Initializes the creation of a new wallet. """ self.app.getPassword(self.initPasswordCallback) @@ -719,7 +722,7 @@ def finishInit(self, ret): app.appWindow.showError("failed to create wallet") def loadClicked(self): """ - The user has selected the "load from from" option. Prompt for a file + The user has selected the "load from from" option. Prompt for a file location and load the wallet. """ app = self.app @@ -760,7 +763,7 @@ def load(pw, userPath): app.getPassword(load, walletPath) def restoreClicked(self): """ - User has selected to generate a wallet from a mnemonic seed. + User has selected to generate a wallet from a mnemonic seed. """ restoreScreen = MnemonicRestorer(self.app) self.app.appWindow.stack(restoreScreen) @@ -777,7 +780,7 @@ def sendToAddress(wallet, val, addr): class SendScreen(Screen): """ - A screen that displays a form to send funds to an address. + A screen that displays a form to send funds to an address. """ def __init__(self, app): """ @@ -817,7 +820,7 @@ def __init__(self, app): def sendClicked(self, e): """ Qt slot for clicked signal from submit button. Send the amount specified - to the address specified. + to the address specified. """ val = float(self.valField.text()) address = self.addressField.text() @@ -841,7 +844,7 @@ def sent(self, res): class WaitingScreen(Screen): """ - Waiting screen displays a Spinner. + Waiting screen displays a Spinner. """ def __init__(self, app): """ @@ -856,7 +859,7 @@ def __init__(self, app): class MnemonicScreen(Screen): """ - Display the mnemonic seed from wallet creation. + Display the mnemonic seed from wallet creation. """ def __init__(self, app, words): """ @@ -870,10 +873,10 @@ def __init__(self, app, words): self.layout.setSpacing(10) # Some instructions for the user. It is critical that they copy the seed - # now, as it can't be regenerated. + # now, as it can't be regenerated. self.lbl = Q.makeLabel( "Copy these words carefully and keep them somewhere secure. " - "You will not have this chance again.", + "You will not have this chance again.", 16) self.lbl.setWordWrap(True) self.layout.addWidget(self.lbl) @@ -904,7 +907,7 @@ def clearAndClose(self, e): class MnemonicRestorer(Screen): """ - A screen with a simple form for entering a mnemnic seed from which to + A screen with a simple form for entering a mnemnic seed from which to generate a wallet. """ def __init__(self, app): @@ -925,6 +928,7 @@ def __init__(self, app): # A field to enter the seed words. self.edit = edit = QtWidgets.QTextEdit() + edit.setAcceptRichText(False) edit.setMaximumWidth(300) edit.setFixedHeight(225) edit.setStyleSheet("QLabel{border: 1px solid #777777; padding: 10px;}") @@ -941,13 +945,13 @@ def __init__(self, app): button.clicked.connect(self.tryWords) def showEvent(self, e): """ - QWidget method. Sets the focus in the QTextEdit. + QWidget method. Sets the focus in the QTextEdit. """ self.edit.setFocus() def tryWords(self, e): """ - Qt Slot for the submit button clicked signal. Attempt to create a - wallet with the provided words. + Qt Slot for the submit button clicked signal. Attempt to create a + wallet with the provided words. """ app = self.app words = self.edit.toPlainText().strip().split() @@ -984,8 +988,8 @@ def walletCreationComplete(self, wallet): Receives the result from wallet creation. Args: - ret (None or Wallet): The wallet if successfully created. None if - failed. + ret (None or Wallet): The wallet if successfully created. None if + failed. """ app = self.app if wallet: @@ -997,8 +1001,7 @@ def walletCreationComplete(self, wallet): class StakingScreen(Screen): """ - A screen with a simple form for entering a mnemnic seed from which to - generate a wallet. + A screen with a form to purchase tickets. """ def __init__(self, app): """ @@ -1010,6 +1013,7 @@ def __init__(self, app): self.canGoHome = True self.layout.setSpacing(20) self.poolScreen = PoolScreen(app, self.poolAuthed) + self.accountScreen = PoolAccountScreen(app, self.poolScreen) self.balance = None self.wgt.setContentsMargins(5, 5, 5, 5) self.wgt.setMinimumWidth(400) @@ -1020,7 +1024,7 @@ def __init__(self, app): self.app.registerSignal(ui.BALANCE_SIGNAL, self.balanceSet) self.app.registerSignal(ui.SYNC_SIGNAL, self.setStats) - # ticket price is a single row reading `Ticket Price: XX.YY DCR`. + # ticket price is a single row reading `Ticket Price: XX.YY DCR`. lbl = Q.makeLabel("Ticket Price: ", 16) self.lastPrice = None self.lastPriceStamp = 0 @@ -1029,7 +1033,7 @@ def __init__(self, app): priceWgt, _ = Q.makeSeries(Q.HORIZONTAL, lbl, self.ticketPrice, unit) self.layout.addWidget(priceWgt) - # Current holdings is a single row that reads `Currently staking X + # Current holdings is a single row that reads `Currently staking X # tickets worth YY.ZZ DCR` lbl = Q.makeLabel("Currently staking", 14) self.ticketCount = Q.makeLabel("", 18, fontFamily="Roboto-Bold") @@ -1047,7 +1051,7 @@ def __init__(self, app): affordWgt.setContentsMargins(0, 0, 0, 30) self.layout.addWidget(affordWgt) - # The actual purchase form. A box with a drop shadow that contains a + # The actual purchase form. A box with a drop shadow that contains a # single row reading `Purchase [ ] tickets [Buy Now]`. lbl = Q.makeLabel("Purchase", 16) lbl2 = Q.makeLabel("tickets", 16) @@ -1066,6 +1070,15 @@ def __init__(self, app): Q.addDropShadow(purchaseWgt) self.layout.addWidget(purchaseWgt) + # Navigate to account screen, to choose or add a different VSP account. + self.currentPool = Q.makeLabel("", 15) + lbl2 = ClickyLabel(self.stackAccounts, "change") + Q.setProperties(lbl2, underline=True, fontSize=15) + Q.addHoverColor(lbl2, "#f5ffff") + wgt, lyt = Q.makeSeries(Q.HORIZONTAL, self.currentPool, Q.STRETCH, lbl2) + lyt.setContentsMargins(0, 10, 0, 0) + self.layout.addWidget(wgt) + def stacked(self): """ stacked is called on screens when stacked by the TinyDialog. @@ -1075,6 +1088,8 @@ def stacked(self): self.app.appWindow.pop(self) self.app.appWindow.stack(self.poolScreen) + def stackAccounts(self): + self.app.appWindow.stack(self.accountScreen) def setStats(self): """ Get the current ticket stats and update the display. @@ -1083,6 +1098,9 @@ def setStats(self): stats = acct.ticketStats() self.ticketCount.setText(str(stats.count)) self.ticketValue.setText("%.2f" % (stats.value/1e8)) + stakePool = acct.stakePool() + if stakePool: + self.currentPool.setText(stakePool.url) def blockchainConnected(self): """ @@ -1108,8 +1126,8 @@ def ticketPriceCB(self, ticketPrice): def balanceSet(self, balance): """ - Connected to the BALANCE_SIGNAL signal. Sets the balance and updates - the display. + Connected to the BALANCE_SIGNAL signal. Sets the balance and updates + the display. Args: balance (account.Balance): The current account balance. @@ -1126,7 +1144,7 @@ def setBuyStats(self): def buyClicked(self, e=None): """ - Connected to the "Buy Now" button clicked signal. Initializes the ticket + Connected to the "Buy Now" button clicked signal. Initializes the ticket purchase routine. """ qtyStr = self.ticketQty.text() @@ -1140,7 +1158,7 @@ def step(): self.app.withUnlockedWallet(self.buyTickets, self.ticketsBought, qty) self.app.confirm("Are you sure you want to purchase %d ticket(s) for %.2f DCR? " "Once purchased, these funds will be locked until your tickets vote or expire." - % (qty, qty*self.lastPrice), + % (qty, qty*self.lastPrice), step) def buyTickets(self, wallet, qty): """ @@ -1165,7 +1183,7 @@ def buyTickets(self, wallet, qty): return txs def ticketsBought(self, res): """ - The final callback from a ticket purchase. If res evaluates True, it + The final callback from a ticket purchase. If res evaluates True, it should be a list of purchased tickets. """ if not res: @@ -1202,7 +1220,7 @@ def __init__(self, app, callback): self.wgt.setContentsMargins(15, 0, 15, 0) # After the header, there are two rows that make up the form. The first - # row is a QLineEdit and a button that takes the pool URL. The second + # row is a QLineEdit and a button that takes the pool URL. The second # row is a slightly larger QLineEdit for the API key. lbl = Q.makeLabel("Add a voting service provider", 16) wgt, _ = Q.makeSeries(Q.HORIZONTAL, lbl, Q.STRETCH) @@ -1226,7 +1244,7 @@ def __init__(self, app, callback): lbl = Q.makeLabel("Don't have a VSP yet? Heres one.", 15, a=l) self.layout.addWidget(lbl) - # Display info for a randomly chosen pool (with some filtering), and a + # Display info for a randomly chosen pool (with some filtering), and a # couple of links to aid in selecting a VSP.. self.poolUrl = Q.makeLabel("", 16, a=l, fontFamily="Roboto-Medium") self.poolUrl.setOpenExternalLinks(True) @@ -1251,7 +1269,7 @@ def __init__(self, app, callback): poolWgt.mouseReleaseEvent = self.poolClicked self.layout.addWidget(poolWgt) - # A button to select a different pool and a link to the master list on + # A button to select a different pool and a link to the master list on # decred.org. btn1 = app.getButton(TINY, "show another") btn1.clicked.connect(self.randomizePool) @@ -1265,13 +1283,13 @@ def getPools(self): Get the current master list of VSPs from decred.org. """ net = self.app.dcrdata.params - def getPools(): + def get(): try: return StakePool.providers(net) except Exception as e: log.error("error retrieving stake pools: %s" % e) return False - self.app.makeThread(getPools, self.setPools) + self.app.makeThread(get, self.setPools) def setPools(self, pools): """ @@ -1349,7 +1367,7 @@ def votingAddr(wallet): app.withUnlockedWallet(votingAddr, self.continueAuth) def continueAuth(self, res): """ - Follows authPool in the pool authorization process. Send the wallet + Follows authPool in the pool authorization process. Send the wallet voting address to the pool and authorize the response. """ if not res: @@ -1365,7 +1383,7 @@ def setAddr(): except Exception as e: # log.error("failed to set pool address: %s" % e) # self.app.appWindow.showError("failed to set address") - # might be okay. + # might be okay. log.error("failed to authorize stake pool: %s" % e) app.appWindow.showError("pool authorization failed") self.callback(False) @@ -1386,12 +1404,145 @@ def poolClicked(self, e=None): self.poolIp.setText(url) QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) +class PoolAccountScreen(Screen): + """ + A screen that lists currently known VSP accounts, and allows adding new + accounts or changing the selected account. + """ + def __init__(self, app, poolScreen): + """ + Args: + app (TinyDecred): The TinyDecred application instance. + """ + super().__init__(app) + self.isPoppable = True + self.canGoHome = True + + self.pages = [] + self.page = 0 + + self.poolScreen = poolScreen + self.app.registerSignal(ui.SYNC_SIGNAL, self.setPools) + self.wgt.setMinimumWidth(400) + self.wgt.setMinimumHeight(225) + + + lbl = Q.makeLabel("Accounts", 18) + self.layout.addWidget(lbl, 0, Q.ALIGN_LEFT) + + wgt, self.poolsLyt = Q.makeWidget(QtWidgets.QWidget, Q.VERTICAL) + self.poolsLyt.setSpacing(10) + self.poolsLyt.setContentsMargins(5, 5, 5, 5) + self.layout.addWidget(wgt) + + self.prevPg = app.getButton(TINY, "back") + self.prevPg.clicked.connect(self.pageBack) + self.nextPg = app.getButton(TINY, "next") + self.nextPg.clicked.connect(self.pageFwd) + self.pgNum = Q.makeLabel("", 15) + + self.layout.addStretch(1) + + self.pagination, _ = Q.makeSeries(Q.HORIZONTAL, + self.prevPg, + Q.STRETCH, + self.pgNum, + Q.STRETCH, + self.nextPg) + self.layout.addWidget(self.pagination) + + btn = app.getButton(SMALL, "add new acccount") + btn.clicked.connect(self.addClicked) + self.layout.addWidget(btn) + def stacked(self): + """ + stacked is called on screens when stacked by the TinyDialog. + """ + self.setPools() + def pageBack(self): + """ + Go back one page. + """ + newPg = self.page + 1 + if newPg > len(self.pages) - 1: + newPg = 0 + self.page = newPg + self.setWidgets(self.pages[newPg]) + self.setPgNum() + def pageFwd(self): + """ + Go the the next displayed page. + """ + newPg = self.page - 1 + if newPg < 0: + newPg = len(self.pages) - 1 + self.page = newPg + self.setWidgets(self.pages[newPg]) + self.setPgNum() + def setPgNum(self): + """ + Set the displayed page number. + """ + self.pgNum.setText("%d/%d" % (self.page+1, len(self.pages))) + def setPools(self): + """ + Reset the stake pools list from that active account and set the first + page. + """ + acct = self.app.wallet.selectedAccount + if not acct: + log.error("no account selected") + pools = acct.stakePools + if len(pools) == 0: + return + self.pages = [pools[i*2:i*2+2] for i in range((len(pools)+1)//2)] + self.page = 0 + self.setWidgets(self.pages[0]) + self.pagination.setVisible(len(self.pages) > 1) + self.setPgNum() + def setWidgets(self, pools): + """ + Set the displayed pool widgets. + + Args: + pools list(StakePool): pools to display + """ + Q.clearLayout(self.poolsLyt, delete=True) + for pool in pools: + ticketAddr = pool.purchaseInfo.ticketAddress + urlLbl = Q.makeLabel(pool.url, 16) + addrLbl = Q.makeLabel(ticketAddr, 14) + wgt, lyt = Q.makeSeries(Q.VERTICAL, + urlLbl, addrLbl, align=Q.ALIGN_LEFT) + wgt.setMinimumWidth(360) + lyt.setContentsMargins(5, 5, 5, 5) + Q.addDropShadow(wgt) + Q.addClickHandler(wgt, lambda p=pool: self.selectActivePool(p)) + self.poolsLyt.addWidget(wgt, 1) + + def selectActivePool(self, pool): + """ + Set the current active pool. + + Args: + pool (StakePool): The new active pool. + """ + self.app.appWindow.showSuccess("new pool selected") + self.app.wallet.selectedAccount.setPool(pool) + self.setPools() + + def addClicked(self, e=None): + """ + The clicked slot for the add pool button. Stacks the pool screen. + """ + self.app.appWindow.pop(self) + self.app.appWindow.stack(self.poolScreen) class ConfirmScreen(Screen): """ - A screen that displays a custom prompt and calls a callback function - conditionally on user affirmation. The two available buttons say "ok" and - "no". Clicking "ok" triggers the callback. Clicking "no" simply pops this + A screen that displays a custom prompt and calls a callback function + conditionally on user affirmation. The two available buttons say "ok" and + "no". Clicking "ok" triggers the callback. Clicking "no" simply pops this Screen. """ def __init__(self, app): @@ -1402,8 +1553,8 @@ def __init__(self, app): super().__init__(app) self.isPoppable = True self.canGoHome = False - self.callback = None + self.callback = None self.prompt = Q.makeLabel("", 16) self.prompt.setWordWrap(True) self.layout.addWidget(self.prompt) @@ -1419,7 +1570,7 @@ def withPurpose(self, prompt, callback): Set the prompts and callback and return self. Args: - prompt (string): The prompt for the users. + prompt (string): The prompt for the users. callback (function): The function to call when the user clicks "ok". Returns: From 426d4cda7cd2ce68ba7bf6617bafbf0b2a24fd5b Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 13 Nov 2019 17:32:02 -0600 Subject: [PATCH 06/12] add missing docs. remove unused code --- crypto/crypto.py | 70 ++++++++++++++++-------------------- pydecred/dcrdata.py | 88 +++++++++++++++++++++++++++++++++------------ 2 files changed, 96 insertions(+), 62 deletions(-) diff --git a/crypto/crypto.py b/crypto/crypto.py index 6cb7bfad..90e664dd 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -99,8 +99,8 @@ def hash160(self): class AddressSecpPubKey: """ - AddressSecpPubKey represents and address, which is a pubkey hash and it's - base-58 encoding. Argument pubkey should be a ByteArray corresponding the + AddressSecpPubKey represents and address, which is a pubkey hash and it's + base-58 encoding. Argument pubkey should be a ByteArray corresponding the the serializedCompressed public key (33 bytes). """ def __init__(self, serializedPubkey, net): @@ -135,7 +135,7 @@ def string(self): """ A base-58 encoding of the pubkey. - Returns: + Returns: str: The encoded address. """ encoded = ByteArray(self.pubkeyID) @@ -177,7 +177,7 @@ def string(self): """ A base-58 encoding of the pubkey hash. - Returns: + Returns: str: The encoded address. """ return encodeAddress(self.netID, self.scriptHash) @@ -346,10 +346,10 @@ def b58CheckDecode(s): def newAddressPubKey(decoded, net): """ - NewAddressPubKey returns a new Address. decoded must be 33 bytes. This + NewAddressPubKey returns a new Address. decoded must be 33 bytes. This constructor takes the decoded pubkey such as would be decoded from a base58 - string. The first byte indicates the signature suite. For compressed - secp256k1 pubkeys, use AddressSecpPubKey directly. + string. The first byte indicates the signature suite. For compressed + secp256k1 pubkeys, use AddressSecpPubKey directly. """ if len(decoded) == 33: # First byte is the signature suite and ybit. @@ -400,13 +400,13 @@ def newAddressPubKeyHash(pkHash, net, algo): def newAddressScriptHash(script, net): """ newAddressScriptHash returns a new AddressScriptHash from a redeem script. - + Args: script (ByteArray): the redeem script net (obj): the network parameters Returns: - AddressScriptHash: An address object. + AddressScriptHash: An address object. """ return newAddressScriptHashFromHash(hash160(script.b), net) @@ -420,7 +420,7 @@ def newAddressScriptHashFromHash(scriptHash, net): net (obj): The network parameters. Returns: - AddressScriptHash: An address object. + AddressScriptHash: An address object. """ if len(scriptHash) != RIPEMD160_SIZE: raise Exception("incorrect script hash length") @@ -743,8 +743,6 @@ def publicKey(self): """ return Curve.parsePubKey(self.pubKey) -# tinyjson.register(ExtendedKey) - def decodeExtendedKey(net, pw, key): """ Decode an base58 ExtendedKey using the passphrase and network parameters. @@ -969,16 +967,6 @@ def rekey(password, kp): raise PasswordError("rekey digest check failed") return sk - -testAddrMagics = { - "pubKeyID": (0x1386).to_bytes(2, byteorder="big"), # starts with Dk - "pkhEcdsaID": (0x073f).to_bytes(2, byteorder="big"), # starts with Ds - "pkhEd25519ID": (0x071f).to_bytes(2, byteorder="big"), # starts with De - "pkhSchnorrID": (0x0701).to_bytes(2, byteorder="big"), # starts with DS - "scriptHashID": (0x071a).to_bytes(2, byteorder="big"), # starts with Dc - "privKeyID": (0x22de).to_bytes(2, byteorder="big"), # starts with Pm -} - class TestCrypto(unittest.TestCase): def test_encryption(self): ''' @@ -991,24 +979,16 @@ def test_encryption(self): self.assertTrue(a, aUnenc) def test_addr_pubkey(self): from tinydecred.pydecred import mainnet - hexKeys = [ - "033b26959b2e1b0d88a050b111eeebcf776a38447f7ae5806b53c9b46e07c267ad", - "0389ced3eaee84d5f0d0e166f6cd15f1bf6f429d1d13709393b418a6fb22d8be53", - "02a14a0023d7d8cbc5d39fa60f7e4dc4d5bf18a7031f52875fbca6bf837f68713f", - "03c3e3d7cde1c453a6283f5802a73d1cb3827cb4b007f58e3a52a36ce189934b6a", - "0254e17b230e782e591a9910794fdbf9943d500a47f2bf8446e1238f84e809bffc", - ] - b58Keys = [ - "DkRKjw7LmGCSzBwaUtjQLfb75Zcx9hH8yGNs3qPSwVzZuUKs7iu2e", - "DkRLLaJWkmH75iZGtQYE6FEf16zxeHr6TCAF59tGxhds4MFc2HqUS", - "DkM3hdWuKSSTm7Vq8WZx5f294vcZbPkAQYBDswkjmF1CFuWCRYxTr", - "DkRLn9vzsjK4ZYgDKy7JVYHKGvpZU5CYGK9H8zF2VCWbpTyVsEf4P", - "DkM37ymaat9j6oTFii1MZVpXrc4aRLEMHhTZrvrz8QY6BZ2HX843L", + pairs = [ + ("033b26959b2e1b0d88a050b111eeebcf776a38447f7ae5806b53c9b46e07c267ad", "DkRKjw7LmGCSzBwaUtjQLfb75Zcx9hH8yGNs3qPSwVzZuUKs7iu2e"), + ("0389ced3eaee84d5f0d0e166f6cd15f1bf6f429d1d13709393b418a6fb22d8be53", "DkRLLaJWkmH75iZGtQYE6FEf16zxeHr6TCAF59tGxhds4MFc2HqUS"), + ("02a14a0023d7d8cbc5d39fa60f7e4dc4d5bf18a7031f52875fbca6bf837f68713f", "DkM3hdWuKSSTm7Vq8WZx5f294vcZbPkAQYBDswkjmF1CFuWCRYxTr"), + ("03c3e3d7cde1c453a6283f5802a73d1cb3827cb4b007f58e3a52a36ce189934b6a", "DkRLn9vzsjK4ZYgDKy7JVYHKGvpZU5CYGK9H8zF2VCWbpTyVsEf4P"), + ("0254e17b230e782e591a9910794fdbf9943d500a47f2bf8446e1238f84e809bffc", "DkM37ymaat9j6oTFii1MZVpXrc4aRLEMHhTZrvrz8QY6BZ2HX843L"), ] - for hexKey, b58Key in zip(hexKeys, b58Keys): - pubkey = ByteArray(hexKey) - addr = AddressSecpPubKey(pubkey, mainnet) - self.assertEqual(addr.string(), b58Key) + for hexKey, addrStr in pairs: + addr = AddressSecpPubKey(ByteArray(hexKey), mainnet) + self.assertEqual(addr.string(), addrStr) def test_addr_pubkey_hash(self): from tinydecred.pydecred import mainnet pairs = [ @@ -1021,3 +1001,15 @@ def test_addr_pubkey_hash(self): for pubkeyHash, addrStr in pairs: addr = AddressPubKeyHash(mainnet.PubKeyHashAddrID, ByteArray(pubkeyHash)) self.assertEqual(addr.string(), addrStr) + def test_addr_script_hash(self): + from tinydecred.pydecred import mainnet + pairs = [ + ("52fdfc072182654f163f5f0f9a621d729566c74d", "Dcf2QjJ1pSnLwthhw1cwE55MVZNQVXDZWQT"), + ("10037c4d7bbb0407d1e2c64981855ad8681d0d86", "DcYvG3fPxHDZ5pzW8nj4rcYq5kM9XFxXpUy"), + ("d1e91e00167939cb6694d2c422acd208a0072939", "DcrbVYmhm5yX9mw9qdwUVWw6psUhPGrQJsT"), + ("487f6999eb9d18a44784045d87f3c67cf22746e9", "Dce4vLzzENaZT7D2Wq5crRZ4VwfYMDMWkD9"), + ("95af5a25367951baa2ff6cd471c483f15fb90bad", "Dcm73og7Hn9PigaNu59dHgKnNSP1myCQ39t"), + ] + for scriptHash, addrStr in pairs: + addr = newAddressScriptHashFromHash(ByteArray(scriptHash), mainnet) + self.assertEqual(addr.string(), addrStr) diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 59c40360..8107ba54 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -346,22 +346,10 @@ def __init__(self, name, message): self.name = name self.message = message -class APIBlock: - def __init__(self, blockHash, height): - self.hash = blockHash - self.height = height - @staticmethod - def __fromjson__(obj): - return APIBlock(obj["hash"], obj["height"]) - def __tojson__(self): - return { - "hash": self.blockHash, - "height": self.blockHeight, - } - -tinyjson.register(APIBlock, tag="dcrdata.APIBlock") - class TicketInfo: + """ + Ticket-related transaction information. + """ def __init__(self, status, purchaseBlock, maturityHeight, expirationHeight, lotteryBlock, vote, revocation): self.status = status @@ -439,6 +427,12 @@ def __fromjson__(obj): return UTXO.parse(obj) @staticmethod def parse(obj): + """ + Parse the decoded JSON from dcrdata into a UTXO. + + Args: + obj (dict): The dcrdata /api/tx response, decoded. + """ utxo = UTXO( address = obj["address"], txid = obj["txid"], @@ -454,29 +448,78 @@ def parse(obj): ) return utxo def parseScriptClass(self): + """ + Set the script class. + """ if self.scriptPubKey: self.scriptClass = txscript.getScriptClass(0, self.scriptPubKey) def confirm(self, block, tx, params): + """ + This output has been mined. Set the block details. + + Args: + block (msgblock.BlockHeader): The block header. + tx (dict): The dcrdata transaction. + params (obj): The network parameters. + """ self.height = block.height self.maturity = block.height + params.CoinbaseMaturity if tx.looksLikeCoinbase() else None self.ts = block.timestamp def isSpendable(self, tipHeight): + """ + isSpendable will be True if the UTXO is considered mature at the + specified height. + + Args: + tipHeight (int): The current blockchain tip height. + + Returns: + bool: True if mature. + """ if self.isTicket(): return False if self.maturity: return self.maturity <= tipHeight return True def key(self): + """ + A unique ID for this UTXO. + """ return UTXO.makeKey(self.txid, self.vout) @staticmethod def makeKey(txid, vout): + """ + A unique ID for a UTXO. + + Args: + txid (str): UTXO's transaction ID. + vout (int): UTXO's transaction output index. + """ return txid + "#" + str(vout) def setTicketInfo(self, apiTinfo): + """ + Set the ticket info. Only useful for tickets. + + Args: + apiTinfo (dict): dcrdata /api/tinfo response. + """ self.tinfo = TicketInfo.parse(apiTinfo) self.maturity = self.tinfo.maturityHeight def isTicket(self): + """ + isTicket will be True if this is SSTX output. + + Returns: + bool: True if this is an SSTX output. + """ return self.scriptClass == txscript.StakeSubmissionTy def isLiveTicket(self): + """ + isLiveTicket will return True if this is a live ticket. + + Returns: + bool. True if this is a live ticket. + """ return self.tinfo and self.tinfo.status in ("immature", "live") tinyjson.register(UTXO, tag="dcr.UTXO") @@ -1065,12 +1108,11 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): # , minconf def purchaseTickets(self, keysource, utxosource, req): """ Based on dcrwallet (*Wallet).purchaseTickets. - purchaseTickets indicates to the wallet that a ticket should be purchased - using all currently available funds. The ticket address parameter in the - request can be nil in which case the ticket address associated with the - wallet instance will be used. Also, when the spend limit in the request - is greater than or equal to 0, tickets that cost more than that limit - will return an error that not enough funds are available. + purchaseTickets indicates to the wallet that a ticket should be + purchased using any currently available funds. Also, when the spend + limit in the request is greater than or equal to 0, tickets that cost + more than that limit will return an error that not enough funds are + available. """ self.updateTip() # account minConf is zero for regular outputs for now. Need to make that @@ -1094,8 +1136,8 @@ def purchaseTickets(self, keysource, utxosource, req): # generating a ticket. The account balance is checked first # in case there is not enough money to generate the split # even without fees. - # TODO This can still sometimes fail if the split amount - # required plus fees for the split is larger than the + # TODO (copied from dcrwallet) This can still sometimes fail if the + # split amount required plus fees for the split is larger than the # balance we have, wasting an address. In the future, # address this better and prevent address burning. From 040fbbbd81f23edee2dde9fdc8a983563459a373 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 14 Nov 2019 04:27:23 -0600 Subject: [PATCH 07/12] wallets directory for wallet-related modules --- app.py | 6 +- crypto/crypto.py | 31 ++--- pydecred/account.py | 64 +++++----- pydecred/dcrdata.py | 2 +- pydecred/txscript.py | 114 +++++++++--------- ui/screens.py | 2 +- accounts.py => wallet/accounts.py | 14 ++- wallet/accounts_test.py | 188 ++++++++++++++++++++++++++++++ api.py => wallet/api.py | 0 wallet.py => wallet/wallet.py | 2 +- 10 files changed, 311 insertions(+), 112 deletions(-) rename accounts.py => wallet/accounts.py (99%) create mode 100644 wallet/accounts_test.py rename api.py => wallet/api.py (100%) rename wallet.py => wallet/wallet.py (99%) diff --git a/app.py b/app.py index 6ab8a33c..d53e548b 100644 --- a/app.py +++ b/app.py @@ -9,10 +9,10 @@ import sys from PyQt5 import QtGui, QtCore, QtWidgets from tinydecred import config +from tinydecred.wallet.wallet import Wallet from tinydecred.util import helpers from tinydecred.pydecred import constants as DCR from tinydecred.pydecred.dcrdata import DcrdataBlockchain -from tinydecred.wallet import Wallet from tinydecred.ui import screens, ui, qutilities as Q # the directory of the tinydecred package @@ -211,7 +211,7 @@ def walletFilename(self): return self.getNetSetting(currentWallet) def sysTrayActivated(self, trigger): """ - Qt Slot called when the user interacts with the system tray icon. Shows + Qt Slot called when the user interacts with the system tray icon. Shows the window, creating an icon in the user's application panel that persists until the appWindow is minimized. """ @@ -331,7 +331,7 @@ def signal_(self, s): A Qt Slot used for routing signalRegistry signals. Args: - s (tuple): A tuple of (func, signal args, user args, signal kwargs, + s (tuple): A tuple of (func, signal args, user args, signal kwargs, user kwargs). """ cb, sigA, a, sigK, k = s diff --git a/crypto/crypto.py b/crypto/crypto.py index 90e664dd..136d722f 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -56,11 +56,21 @@ # compressed public key. PKFCompressed = 1 -class ParameterRangeError(Exception): - pass -class ZeroBytesError(Exception): +class CrazyKeyError(Exception): + """ + Both derived public or private keys rely on treating the left 32-byte + sequence calculated above (Il) as a 256-bit integer that must be within the + valid range for a secp256k1 private key. There is a small chance + (< 1 in 2^127) this condition will not hold, and in that case, a child + extended key can't be created for this index and the caller should simply + increment to the next index. + """ pass -class PasswordError(Exception): + +class ParameterRangeError(Exception): + """ + An input parameter is out of the acceptable range. + """ pass def encodeAddress(netID, k): @@ -561,14 +571,9 @@ def child(self, i): il = ilr[:len(ilr)//2] childChainCode = ilr[len(ilr)//2:] - # Both derived public or private keys rely on treating the left 32-byte - # sequence calculated above (Il) as a 256-bit integer that must be - # within the valid range for a secp256k1 private key. There is a small - # chance (< 1 in 2^127) this condition will not hold, and in that case, - # a child extended key can't be created for this index and the caller - # should simply increment to the next index. + # See CrazyKeyError docs for an explanation of this condition. if il.int() >= Curve.N or il.iszero(): - raise ParameterRangeError("ExtendedKey.child: generated Il outside valid range") + raise CrazyKeyError("ExtendedKey.child: generated Il outside valid range") # The algorithm used to derive the child key depends on whether or not # a private or public child is being derived. @@ -686,7 +691,7 @@ def string(self): str: The encoded extended key. """ if self.key.iszero(): - raise ZeroBytesError("unexpected zero key") + raise Exception("unexpected zero key") childNumBytes = ByteArray(self.childNum, length=4) depthByte = ByteArray(self.depth % 256, length=1) @@ -964,7 +969,7 @@ def rekey(password, kp): raise Exception("unkown key derivation function") checkDigest = ByteArray(hashlib.sha256(sk.key.b).digest()) if checkDigest != kp.digest: - raise PasswordError("rekey digest check failed") + raise Exception("rekey digest check failed") return sk class TestCrypto(unittest.TestCase): diff --git a/pydecred/account.py b/pydecred/account.py index d6692e56..83b5905c 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -2,11 +2,11 @@ Copyright (c) 2019, Brian Stafford See LICENSE for details -The DecredAccount inherits from the tinydecred base Account and adds staking +The DecredAccount inherits from the tinydecred base Account and adds staking support. """ -from tinydecred.accounts import Account +from tinydecred.wallet.accounts import Account from tinydecred.util import tinyjson, helpers from tinydecred.crypto.crypto import AddressSecpPubKey from tinydecred.pydecred import txscript @@ -21,18 +21,18 @@ class KeySource(object): """ - Implements the KeySource API from tinydecred.api. Must provide access to + Implements the KeySource API from tinydecred.api. Must provide access to internal addresses via the KeySource.internal method, and PrivateKeys for a specified address via the KeySource.priv method. This implementation just sets the passed functions to class properties with the required method - names. + names. """ def __init__(self, priv, internal): """ Args: priv (func): func(address : string) -> PrivateKey. Retrieves the associated with the specified address. - internal (func): func() -> address : string. Get a new internal + internal (func): func() -> address : string. Get a new internal address. """ self.priv = priv @@ -47,9 +47,9 @@ def __init__(self, minConf, expiry, spendLimit, poolAddress, votingAddress, tick # I add the ability to change it. self.minConf = minConf # expiry can be set to some reasonable block height. This may be - # important when approaching the end of a ticket window. + # important when approaching the end of a ticket window. self.expiry = expiry - # Price is calculated purely from the ticket count, price, and fees, but + # Price is calculated purely from the ticket count, price, and fees, but # cannot go over spendLimit. self.spendLimit = spendLimit # The VSP fee payment address. @@ -60,12 +60,12 @@ def __init__(self, minConf, expiry, spendLimit, poolAddress, votingAddress, tick # ticketFee is the transaction fee rate to pay the miner for the ticket. # Set to zero to use wallet's network default fee rate. self.ticketFee = ticketFee - # poolFees are set by the VSP. If you don't set these correctly, the + # poolFees are set by the VSP. If you don't set these correctly, the # VSP may not vote for you. self.poolFees = poolFees - # How many tickets to buy. + # How many tickets to buy. self.count = count - # txFee is the transaction fee rate to pay the miner for the split + # txFee is the transaction fee rate to pay the miner for the split # transaction required to fund the ticket. # Set to zero to use wallet's network default fee rate. self.txFee = txFee @@ -80,7 +80,7 @@ def __init__(self, count=0, value=0): Args: count (int): How many tickets the account owns. No differentiation is made between immature, live, missed, or expired tickets. - value (int): How much value is locked in the tickets counted in + value (int): How much value is locked in the tickets counted in count. """ self.count = count @@ -89,7 +89,7 @@ def __init__(self, count=0, value=0): class DecredAccount(Account): """ DecredAccount is the Decred version of the base tinydecred Account. - Decred Account inherits Account, and adds the necessary functionality to + Decred Account inherits Account, and adds the necessary functionality to handle staking. """ def __init__(self, *a, **k): @@ -134,8 +134,8 @@ def resolveUTXOs(self, blockchainUTXOs): to hook into the sync to authorize the stake pool. Args: - blockchainUTXOs (list(obj)): A list of Python objects decoded from - dcrdata's JSON response from ...addr/utxo endpoint. + blockchainUTXOs (list(obj)): A list of Python objects decoded from + dcrdata's JSON response from ...addr/utxo endpoint. """ super().resolveUTXOs(blockchainUTXOs) self.updateStakeStats() @@ -151,7 +151,7 @@ def addUTXO(self, utxo): self.updateStakeStats() def addTicketAddresses(self, a): """ - Add the ticket voting addresses from each known stake pool. + Add the ticket voting addresses from each known stake pool. Args: a (list(string)): The ticket addresses will be appended to this @@ -173,7 +173,7 @@ def watchAddrs(self): return self.addTicketAddresses(super().watchAddrs()) def votingKey(self): """ - For now, the voting key is the zeroth child + For now, the voting key is the zeroth child """ return self.privKey.child(STAKE_BRANCH).child(0).privateKey() def votingAddress(self): @@ -190,7 +190,7 @@ def setPool(self, pool): """ Set the specified pool as the default. - Args: + Args: pool (stakepool.StakePool): The stake pool object. """ assert isinstance(pool, StakePool) @@ -202,7 +202,7 @@ def hasPool(self): return self.stakePool() != None def stakePool(self): """ - stakePool is the default stakepool.StakePool for the account. + stakePool is the default stakepool.StakePool for the account. Returns: staekpool.StakePool: The default stake pool object. @@ -230,7 +230,7 @@ def blockSignal(self, sig): if self.caresAboutTxid(txid): tx = self.blockchain.tx(txid) self.confirmTx(tx, self.blockchain.tipHeight) - # "Spendable" balance can change as utxo's mature, so update the + # "Spendable" balance can change as utxo's mature, so update the # balance at every block. self.signals.balance(self.calcBalance(self.blockchain.tipHeight)) def addressSignal(self, addr, txid): @@ -278,7 +278,7 @@ def sendToAddress(self, value, address, feeRate): value int: The amount to send, in atoms. address str: The base-58 encoded pubkey hash. - Returns: + Returns: MsgTx: The newly created transaction on success, `False` on failure. """ keysource = KeySource( @@ -293,8 +293,8 @@ def sendToAddress(self, value, address, feeRate): return tx def purchaseTickets(self, qty, price): """ - purchaseTickets completes the purchase of the specified tickets. The - DecredAccount uses the blockchain to do the heavy lifting, but must + purchaseTickets completes the purchase of the specified tickets. The + DecredAccount uses the blockchain to do the heavy lifting, but must prepare the TicketRequest and KeySource and gather some other account- related information. """ @@ -305,14 +305,14 @@ def purchaseTickets(self, qty, price): pool = self.stakePool() pi = pool.purchaseInfo req = TicketRequest( - minConf = 0, - expiry = 0, + minConf = 0, + expiry = 0, spendLimit = int(price*qty*1.1*1e8), # convert to atoms here - poolAddress = pi.poolAddress, - votingAddress = pi.ticketAddress, + poolAddress = pi.poolAddress, + votingAddress = pi.ticketAddress, ticketFee = 0, # use network default - poolFees = pi.poolFees, - count = qty, + poolFees = pi.poolFees, + count = qty, txFee = 0, # use network default ) txs, spentUTXOs, newUTXOs = self.blockchain.purchaseTickets(keysource, self.getUTXOs, req) @@ -326,7 +326,7 @@ def purchaseTickets(self, qty, price): self.tickets.extend([tx.txid() for tx in txs[1]]) # Remove spent utxos from cache. self.spendUTXOs(spentUTXOs) - # Add new UTXOs to set. These may be replaced with network-sourced + # Add new UTXOs to set. These may be replaced with network-sourced # UTXOs once the wallet receives an update from the BlockChain. for utxo in newUTXOs: self.addUTXO(utxo) @@ -341,7 +341,7 @@ def sync(self, blockchain, signals): signals.balance(self.balance) self.generateGapAddresses() - # First, look at addresses that have been generated but not seen. Run in + # First, look at addresses that have been generated but not seen. Run in # loop until the gap limit is reached. requestedTxs = 0 addrs = self.unseenAddrs() @@ -355,7 +355,7 @@ def sync(self, blockchain, signals): # start with a search for all known addresses addresses = self.allAddresses() - + # Until the server stops returning UTXOs, keep requesting more addresses # to check. while True: @@ -377,5 +377,5 @@ def sync(self, blockchain, signals): # Signal the new balance. signals.balance(self.calcBalance(self.blockchain.tip["height"])) return True - + tinyjson.register(DecredAccount) \ No newline at end of file diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 8107ba54..0a17793a 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -17,7 +17,7 @@ from tinydecred.util import tinyjson, helpers, database, http from tinydecred.crypto import crypto from tinydecred.crypto.bytearray import ByteArray -from tinydecred.api import InsufficientFundsError +from tinydecred.wallet.api import InsufficientFundsError from tinydecred.pydecred import txscript, calc from tinydecred.pydecred.wire import msgtx, wire, msgblock from tinydecred.util.database import KeyValueDatabase diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 888b3ebb..882a34b2 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -90,7 +90,7 @@ P2SHPkScriptSize = 1 + 1 + 20 + 1 # Many of these constants were pulled from the dcrd, and are left as mixed case -# to maintain reference. +# to maintain reference. # DefaultRelayFeePerKb is the default minimum relay fee policy for a mempool. DefaultRelayFeePerKb = 1e4 @@ -183,7 +183,7 @@ class Signature: """ - The Signature class represents an ECDSA-algorithm signature. + The Signature class represents an ECDSA-algorithm signature. """ def __init__(self, r, s): self.r = r @@ -193,7 +193,7 @@ def serialize(self): serialize returns the ECDSA signature in the more strict DER format. Note that the serialized bytes returned do not include the appended hash type used in Decred signature scripts. - + encoding/asn1 is broken so we hand roll this output: 0x30 0x02 r 0x02 s """ @@ -202,7 +202,7 @@ def serialize(self): halforder = order>>1 # low 'S' malleability breaker sigS = self.s - if sigS > halforder: + if sigS > halforder: sigS = order - sigS # Ensure the encoded bytes for the r and s values are canonical and # thus suitable for DER encoding. @@ -236,7 +236,7 @@ class ScriptTokenizer: complete, either due to successfully tokenizing the entire script or encountering a parse error. In the case of failure, the Err function may be used to obtain the specific parse error. - + Upon successfully parsing an opcode, the opcode and data associated with it may be obtained via the Opcode and Data functions, respectively. """ @@ -253,16 +253,16 @@ def next(self): successful. It will not be successful if invoked when already at the end of the script, a parse failure is encountered, or an associated error already exists due to a previous parse failure. - + In the case of a true return, the parsed opcode and data can be obtained with the associated functions and the offset into the script will either point to the next opcode or the end of the script if the final opcode was parsed. - + In the case of a false return, the parsed opcode and data will be the last successfully parsed values (if any) and the offset into the script will either point to the failing opcode or the end of the script if the function was invoked when already at the end of the script. - + Invoking this function when already at the end of the script is not considered an error and will simply return false. """ @@ -326,10 +326,10 @@ def next(self): # impossible. raise Exception("unreachable") def done(self): - """ - Script parsing has completed - - Returns: + """ + Script parsing has completed + + Returns: bool: True if script parsing complete. """ return self.err != None or self.offset >= len(self.script) @@ -354,8 +354,8 @@ def data(self): return self.d def byteIndex(self): """ - ByteIndex returns the current offset into the full script that will be - parsed next and therefore also implies everything before it has already + ByteIndex returns the current offset into the full script that will be + parsed next and therefore also implies everything before it has already been parsed. Returns: @@ -385,15 +385,15 @@ def __init__(self, op, amt, pkScript): self.pkScript = pkScript def checkScriptParses(scriptVersion, script): - """ - checkScriptParses returns None when the script parses without error. - + """ + checkScriptParses returns None when the script parses without error. + Args: scriptVersion (int): The script version. script (ByteArray): The script. Returns: - None or Exception: None on success. Exception is returned, not raised. + None or Exception: None on success. Exception is returned, not raised. """ tokenizer = ScriptTokenizer(scriptVersion, script) while tokenizer.next(): @@ -401,7 +401,7 @@ def checkScriptParses(scriptVersion, script): return tokenizer.err def finalOpcodeData(scriptVersion, script): - """ + """ finalOpcodeData returns the data associated with the final opcode in the script. It will return nil if the script fails to parse. @@ -446,7 +446,7 @@ def canonicalizeInt(val): b = ByteArray(0, length=len(b)+1) | b return b -def hashToInt(h): +def hashToInt(h): """ hashToInt converts a hash value to an integer. There is some disagreement about how this is done. [NSA] suggests that this is done in the obvious @@ -482,7 +482,7 @@ def getScriptClass(version, script): version (int): The script version. script (ByteArray): The script. - Returns: + Returns: int: The script class. """ if version != DefaultScriptVersion: @@ -494,7 +494,7 @@ def typeOfScript(scriptVersion, script): """ scriptType returns the type of the script being inspected from the known standard types. - + NOTE: All scripts that are not version 0 are currently considered non standard. """ @@ -551,7 +551,7 @@ def extractScriptHash(pkScript): """ extractScriptHash extracts the script hash from the passed script if it is a standard pay-to-script-hash script. It will return nil otherwise. - + NOTE: This function is only valid for version 0 opcodes. Since the function does not accept a script version, the results are undefined for other script versions. @@ -670,7 +670,7 @@ def isStakeSubmissionScript(scriptVersion, script): """ isStakeSubmissionScript returns whether or not the passed script is a supported stake submission script. - + NOTE: This function is only valid for version 0 scripts. It will always return false for other script versions. """ @@ -688,7 +688,7 @@ def isStakeGenScript(scriptVersion, script): """ isStakeGenScript returns whether or not the passed script is a supported stake generation script. - + NOTE: This function is only valid for version 0 scripts. It will always return false for other script versions. """ @@ -706,7 +706,7 @@ def isStakeRevocationScript(scriptVersion, script): """ isStakeRevocationScript returns whether or not the passed script is a supported stake revocation script. - + NOTE: This function is only valid for version 0 scripts. It will always return false for other script versions. """ @@ -724,7 +724,7 @@ def isStakeChangeScript(scriptVersion, script): """ isStakeChangeScript returns whether or not the passed script is a supported stake change script. - + NOTE: This function is only valid for version 0 scripts. It will always return false for other script versions. """ @@ -740,9 +740,9 @@ def isStakeChangeScript(scriptVersion, script): def getStakeOutSubclass(pkScript): """ - getStakeOutSubclass extracts the subclass (P2PKH or P2SH) from a stake + getStakeOutSubclass extracts the subclass (P2PKH or P2SH) from a stake output. - + NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. @@ -781,12 +781,12 @@ def extractMultisigScriptDetails(scriptVersion, script, extractPubKeys): extractMultisigScriptDetails attempts to extract details from the passed script if it is a standard multisig script. The returned details struct will have the valid flag set to false otherwise. - + The extract pubkeys flag indicates whether or not the pubkeys themselves should also be extracted and is provided because extracting them results in an allocation that the caller might wish to avoid. The pubKeys member of the returned details struct will be nil when the flag is false. - + NOTE: This function is only valid for version 0 scripts. The returned details struct will always be empty and have the valid flag set to false for other script versions. @@ -838,7 +838,7 @@ def isMultisigScript(scriptVersion, script): """ isMultisigScript returns whether or not the passed script is a standard multisig script. - + NOTE: This function is only valid for version 0 scripts. It will always return false for other script versions. """ @@ -851,7 +851,7 @@ def isNullDataScript(scriptVersion, script): """ isNullDataScript returns whether or not the passed script is a standard null data script. - + NOTE: This function is only valid for version 0 scripts. It will always return false for other script versions. """ @@ -886,14 +886,14 @@ def checkSStx(tx): checkSStx returns an error if a transaction is not a stake submission transaction. It does some simple validation steps to make sure the number of inputs, number of outputs, and the input/output scripts are valid. - + SStx transactions are specified as below. Inputs: untagged output 1 [index 0] untagged output 2 [index 1] ... untagged output MaxInputsPerSStx [index MaxInputsPerSStx-1] - + Outputs: OP_SSTX tagged output [index 0] OP_RETURN push of input 1's address for reward receiving [index 1] @@ -904,7 +904,7 @@ def checkSStx(tx): OP_RETURN push of input MaxInputsPerSStx's address for reward receiving [index (MaxInputsPerSStx*2)-2] OP_SSTXCHANGE tagged output [index (MaxInputsPerSStx*2)-1] - + The output OP_RETURN pushes should be of size 20 bytes (standard address). """ # Check to make sure there aren't too many inputs. @@ -1231,7 +1231,7 @@ def nonceRFC6979(privKey, inHash, extra, version): bx += extra if len(version) == 16 and len(extra) != 32: bx += ByteArray(0, length=32) - bx += version + bx += version # Step B v = ByteArray(bytearray([1]*holen)) @@ -1274,14 +1274,14 @@ def verifySig(pub, inHash, r, s): """ verifySig verifies the signature in r, s of inHash using the public key, pub. - Args: + Args: pub (PublicKey): The public key. inHash (byte-like): The thing being signed. r (int): The R-parameter of the ECDSA signature. s (int): The S-parameter of the ECDSA signature. Returns: - bool: True if the signature verifies the key. + bool: True if the signature verifies the key. """ # See [NSA] 3.4.2 N = Curve.N @@ -1421,7 +1421,7 @@ def rawTxInSignature(tx, idx, subScript, hashType, key): """ rawTxInSignature returns the serialized ECDSA signature for the input idx of the given transaction, with hashType appended to it. - + NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. @@ -1437,7 +1437,7 @@ def calcSignatureHash(script, hashType, tx, idx, cachedPrefix): cached prefix parameter allows the caller to optimize the calculation by providing the prefix hash to be reused in the case of SigHashAll without the SigHashAnyOneCanPay flag set. - + NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. @@ -1681,7 +1681,7 @@ def paysHighFees(totalInput, tx): maxFee = calcMinRequiredTxRelayFee(1000*DefaultRelayFeePerKb, tx.serializeSize()) return fee > maxFee -def sigHashPrefixSerializeSize(hashType, txIns, txOuts, signIdx): +def sigHashPrefixSerializeSize(hashType, txIns, txOuts, signIdx): """ sigHashPrefixSerializeSize returns the number of bytes the passed parameters would take when encoded with the format used by the prefix hash portion of @@ -1758,7 +1758,7 @@ def extractPkScriptAddrs(version, pkScript, chainParams): signatures associated with the passed PkScript. Note that it only works for 'standard' transaction script types. Any data such as public keys which are invalid are omitted from the results. - + NOTE: This function only attempts to identify version 0 scripts. The return value will indicate a nonstandard script type for other script versions along with an invalid script version error. @@ -1919,7 +1919,7 @@ def mergeScripts(chainParams, tx, idx, pkScript, scriptClass, addresses, nRequir The return value is the best effort merging of the two scripts. Calling this function with addresses, class and nrequired that do not match pkScript is an error and results in undefined behaviour. - + NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. @@ -1981,7 +1981,7 @@ def signTxOutput(chainParams, tx, idx, pkScript, hashType, keysource, previousSc getScript. If previousScript is provided then the results in previousScript will be merged in a type-dependent manner with the newly generated. signature script. - + NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. @@ -2068,11 +2068,11 @@ def estimateInputSize(scriptSize): - the supplied script size - 4 bytes sequence - Args: + Args: scriptSize int: Byte-length of the script. Returns: - int: Estimated size of the byte-encoded transaction input. + int: Estimated size of the byte-encoded transaction input. """ return 32 + 4 + 1 + 8 + 4 + 4 + wire.varIntSerializeSize(scriptSize) + scriptSize + 4 @@ -2084,11 +2084,11 @@ def estimateOutputSize(scriptSize): - the compact int representation of the script size - the supplied script size - Args: + Args: scriptSize int: Byte-length of the script. Returns: - int: Estimated size of the byte-encoded transaction output. + int: Estimated size of the byte-encoded transaction output. """ return 8 + 2 + wire.varIntSerializeSize(scriptSize) + scriptSize @@ -2096,11 +2096,11 @@ def sumOutputSerializeSizes(outputs): # outputs []*wire.TxOut) (serializeSize in """ sumOutputSerializeSizes sums up the serialized size of the supplied outputs. - Args: + Args: outputs list(TxOut): Transaction outputs. - Returns: - int: Estimated size of the byte-encoded transaction outputs. + Returns: + int: Estimated size of the byte-encoded transaction outputs. """ serializeSize = 0 for txOut in outputs: @@ -2115,13 +2115,13 @@ def estimateSerializeSize(scriptSizes, txOuts, changeScriptSize): additional change output if changeScriptSize is greater than 0. Passing 0 does not add a change output. - Args: + Args: scriptSizes list(int): Pubkey script sizes txOuts list(TxOut): Transaction outputs. changeScriptSize int: Size of the change script. - Returns: - int: Estimated size of the byte-encoded transaction outputs. + Returns: + int: Estimated size of the byte-encoded transaction outputs. """ # Generate and sum up the estimated sizes of the inputs. txInsSize = 0 @@ -2200,7 +2200,7 @@ def isUnspendable(amount, pkScript): isUnspendable returns whether the passed public key script is unspendable, or guaranteed to fail at execution. This allows inputs to be pruned instantly when entering the UTXO set. In Decred, all zero value outputs are unspendable. - + NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. @@ -2269,7 +2269,7 @@ def stakePoolTicketFee(stakeDiff, relayFee, height, poolFee, subsidyCache, param to 100.00%. This all must be done with integers. See the included doc.go of this package for more information about the calculation of this fee. - """ + """ # Shift the decimal two places, e.g. 1.00% # to 100. This assumes that the proportion # is already multiplied by 100 to give a diff --git a/ui/screens.py b/ui/screens.py index 57a09606..77f9da90 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -8,7 +8,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets from tinydecred import config from tinydecred.ui import qutilities as Q, ui -from tinydecred.wallet import Wallet +from tinydecred.wallet.wallet import Wallet from tinydecred.util import helpers from tinydecred.pydecred import constants as DCR from tinydecred.pydecred.stakepool import StakePool diff --git a/accounts.py b/wallet/accounts.py similarity index 99% rename from accounts.py rename to wallet/accounts.py index 6b4b4ac5..3db9d38d 100644 --- a/accounts.py +++ b/wallet/accounts.py @@ -10,7 +10,7 @@ """ import unittest from tinydecred.util import tinyjson, helpers -from tinydecred import api +from tinydecred.wallet import api from tinydecred.pydecred import nets, constants as DCR from tinydecred.crypto import crypto from tinydecred.crypto.rando import generateSeed @@ -24,9 +24,15 @@ SALT_SIZE = 32 DEFAULT_ACCOUNT_NAME = "default" +# See CrazyKeyError docs. When an out-of-range key is created, a placeholder +# is set for that child's position internally in Account. CrazyAddress = "CRAZYADDRESS" def filterCrazyAddress(addrs): + """ + When addresses are read out in bulk, they should be filtered for the + CrazyAddress. + """ return [a for a in addrs if a != CrazyAddress] # DefaultGapLimit is the default unused address gap limit defined by BIP0044. @@ -210,7 +216,7 @@ def __init__(self, pubKeyEncrypted, privKeyEncrypted, name, coinID, netID): # cursor position, rather than the next. self.lastSeenExt = 0 # For internal addresses, the cursor can sit below zero, since the - # addresses are always retrieved with with nextInternalAddress. + # addresses are always retrieved with nextInternalAddress. self.lastSeenInt = -1 self.externalAddresses = [] self.internalAddresses = [] @@ -397,7 +403,7 @@ def getUTXOs(self, requested, approve=None): Args: requested (int): Required amount in atoms. - filter (func(UTXO) -> bool): Optional UTXO filtering function. + approve (func(UTXO) -> bool): Optional UTXO filtering function. Returns: list(UTXO): A list of UTXOs. @@ -518,7 +524,7 @@ def nextBranchAddress(self, branchKey, branchAddrs): addr = branchKey.deriveChildAddress(len(branchAddrs), self.net) branchAddrs.append(addr) return addr - except crypto.ParameterRangeError: + except crypto.CrazyKeyError: log.warning("crazy address generated") addr = CrazyAddress branchAddrs.append(addr) diff --git a/wallet/accounts_test.py b/wallet/accounts_test.py new file mode 100644 index 00000000..249fe896 --- /dev/null +++ b/wallet/accounts_test.py @@ -0,0 +1,188 @@ +import unittest + +from tinydecred.util import helpers +from tinydecred.crypto.bytearray import ByteArray +from tinydecred.crypto import crypto +from tinydecred.pydecred import nets +from tinydecred.wallet import accounts + +testSeed = ByteArray("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").b + +def addressForPubkeyBytes(b, net): + ''' + Helper function to convert ECDSA public key bytes to a human readable + ripemind160 hash for use on the specified network. + + Args: + b (bytes): Public key bytes. + net (obj): Network the address will be used on. + + Returns: + crypto.Address: A pubkey-hash address. + ''' + return crypto.newAddressPubKeyHash(crypto.hash160(b), net, crypto.STEcdsaSecp256k1).string() + +class TestAccounts(unittest.TestCase): + @classmethod + def setUpClass(cls): + ''' + Set up for tests. Arguments are ignored. + ''' + helpers.prepareLogger("TestTinyCrypto") + # log.setLevel(0) + def test_child_neuter(self): + ''' + Test the ExtendedKey.neuter method. + ''' + extKey = accounts.newMaster(testSeed, nets.mainnet) + extKey.child(0) + pub = extKey.neuter() + self.assertEqual(pub.string(), "dpubZ9169KDAEUnyo8vdTJcpFWeaUEKH3G6detaXv46HxtQcENwxGBbRqbfTCJ9BUnWPCkE8WApKPJ4h7EAapnXCZq1a9AqWWzs1n31VdfwbrQk") + def test_accounts(self): + ''' + Test account functionality. + ''' + pw = "abc".encode() + am = accounts.createNewAccountManager(testSeed, bytearray(0), pw, nets.mainnet) + rekey = am.acctPrivateKey(0, nets.mainnet, pw) + pubFromPriv = rekey.neuter() + addr1 = pubFromPriv.deriveChildAddress(5, nets.mainnet) + pubKey = am.acctPublicKey(0, nets.mainnet, "") + addr2 = pubKey.deriveChildAddress(5, nets.mainnet) + self.assertEqual(addr1, addr2) + acct = am.openAccount(0, pw) + for n in range(20): + acct.nextExternalAddress() + v = 5 + satoshis = v*1e8 + txid = "abcdefghijkl" + vout = 2 + from tinydecred.pydecred import dcrdata + utxo = dcrdata.UTXO( + address = None, + txid = txid, + vout = vout, + scriptPubKey = ByteArray(0), + amount = v, + satoshis = satoshis, + maturity = 1, + ) + utxocount = lambda: len(list(acct.utxoscan())) + acct.addUTXO(utxo) + self.assertEqual(utxocount(), 1) + self.assertEqual(acct.calcBalance(1).total, satoshis) + self.assertEqual(acct.calcBalance(1).available, satoshis) + self.assertEqual(acct.calcBalance(0).available, 0) + self.assertIsNot(acct.getUTXO(txid, vout), None) + self.assertIs(acct.getUTXO("", -1), None) + self.assertTrue(acct.caresAboutTxid(txid)) + utxos = acct.UTXOsForTXID(txid) + self.assertEqual(len(utxos), 1) + acct.spendUTXOs(utxos) + self.assertEqual(utxocount(), 0) + acct.addUTXO(utxo) + self.assertEqual(utxocount(), 1) + acct.spendUTXO(utxo) + self.assertEqual(utxocount(), 0) + def test_newmaster(self): + ''' + Test extended key derivation. + ''' + kpriv = accounts.newMaster(testSeed, nets.mainnet) + self.assertEqual(kpriv.key.hex(), "f2418d00085be520c6449ddb94b25fe28a1944b5604193bd65f299168796f862") + kpub = kpriv.neuter() + self.assertEqual(kpub.key.hex(), "0317a47499fb2ef0ff8dc6133f577cd44a5f3e53d2835ae15359dbe80c41f70c9b") + kpub_branch0 = kpub.child(0) + self.assertEqual(kpub_branch0.key.hex(), "02dfed559fddafdb8f0041cdd25c4f9576f71b0e504ce61837421c8713f74fb33c") + kpub_branch0_child1 = kpub_branch0.child(1) + self.assertEqual(kpub_branch0_child1.key.hex(), "03745417792d529c66980afe36f364bee6f85a967bae117bc4d316b77e7325f50c") + kpriv_branch0 = kpriv.child(0) + self.assertEqual(kpriv_branch0.key.hex(), "6469a8eb3ed6611cc9ee4019d44ec545f3174f756cc41f9867500efdda742dd9") + kpriv_branch0_child1 = kpriv_branch0.child(1) + self.assertEqual(kpriv_branch0_child1.key.hex(), "fb8efe52b3e4f31bc12916cbcbfc0e84ef5ebfbceb7197b8103e8009c3a74328") + kpriv01_neutered = kpriv_branch0_child1.neuter() + self.assertEqual(kpriv01_neutered.key.hex(), kpub_branch0_child1.key.hex()) + def test_change_addresses(self): + ''' + Test internal branch address derivation. + ''' + pw = "abc".encode() + acctManager = accounts.createNewAccountManager(testSeed, bytearray(0), pw, nets.mainnet) + # acct = acctManager.account(0) + acct = acctManager.openAccount(0, pw) + for i in range(10): + acct.nextInternalAddress() + def test_gap_handling(self): + internalAddrs = [ + "DskHpgbEb6hqkuHchHhtyojpehFToEtjQSo", + "Dsm4oCLnLraGDedfU5unareezTNT75kPbRb", + "DsdN6A9bWhKKJ7PAGdcDLxQrYPKEnjnDv2N", + "Dsifz8eRHvQrfaPXgHHLMDHZopFJq2pBPU9", + "DsmmzJiTTmpafdt2xx7LGk8ZW8cdAKe53Zx", + "DsVB47P4N23PK5C1RyaJdqkQDzVuCDKGQbj", + "DsouVzScdUUswvtCAv6nxRzi2MeufpWnELD", + "DsSoquT5SiDPfksgnveLv3r524k1v8RamYm", + "DsbVDrcfhbdLy4YaSmThmWN47xhxT6FC8XB", + "DsoSrtGYKruQLbvhA92xeJ6eeuAR1GGJVQA", + ] + + externalAddrs = [ + "DsmP6rBEou9Qr7jaHnFz8jTfrUxngWqrKBw", + "DseZQKiWhN3ceBwDJEgGhmwKD3fMbwF4ugf", + "DsVxujP11N72PJ46wgU9sewptWztYUy3L7o", + "DsYa4UBo379cRMCTpDLxYVfpMvMNBdAGrGS", + "DsVSEmQozEnsZ9B3D4Xn4H7kEedDyREgc18", + "DsifDp8p9mRocNj7XNNhGAsYtfWhccc2cry", + "DsV78j9aF8NBwegbcpPkHYy9cnPM39jWXZm", + "DsoLa9Rt1L6qAVT9gSNE5F5XSDLGoppMdwC", + "DsXojqzUTnyRciPDuCFFoKyvQFd6nQMn7Gb", + "DsWp4nShu8WxefgoPej1rNv4gfwy5AoULfV", + ] + + accounts.DefaultGapLimit = gapLimit = 5 + pw = "abc".encode() + am = accounts.createNewAccountManager(testSeed, bytearray(0), pw, nets.mainnet) + account = am.openAccount(0, pw) + account.gapLimit = gapLimit + listsAreEqual = lambda a, b: len(a) == len(b) and all(x == y for x,y in zip(a,b)) + + self.assertTrue(listsAreEqual(account.internalAddresses, internalAddrs[:gapLimit])) + # The external branch starts with the "last seen" at the zeroth address, so + # has one additional address to start. + self.assertTrue(listsAreEqual(account.externalAddresses, externalAddrs[:gapLimit+1])) + + account.addTxid(internalAddrs[0], "somerandomtxid") + newAddrs = account.generateGapAddresses() + self.assertEqual(len(newAddrs), 1) + self.assertEqual(newAddrs[0], internalAddrs[5]) + + # The zeroth external address is considered "seen", so this should not + # change anything. + account.addTxid(externalAddrs[0], "somerandomtxid") + newAddrs = account.generateGapAddresses() + self.assertEqual(len(newAddrs), 0) + + # Mark the 1st address as seen. + account.addTxid(externalAddrs[1], "somerandomtxid") + newAddrs = account.generateGapAddresses() + self.assertEqual(len(newAddrs), 1) + self.assertEqual(externalAddrs[1], account.currentAddress()) + + # cursor should be at index 0, last seen 1, max index 6, so calling + # nextExternalAddress 5 time should put the cursor at index 6, which is + # the gap limit. + for i in range(5): + account.nextExternalAddress() + self.assertEqual(account.currentAddress(), externalAddrs[6]) + + # one more should wrap the cursor back to 1, not zero, so the current + # address is lastSeenExt(=1) + cursor(=1) = 2 + a1 = account.nextExternalAddress() + self.assertEqual(account.currentAddress(), externalAddrs[2]) + self.assertEqual(a1, account.currentAddress()) + + # Sanity check that internal addresses are wrapping too. + for i in range(20): + account.nextInternalAddress() + addrs = account.internalAddresses + self.assertEqual(addrs[len(addrs)-1], internalAddrs[5]) \ No newline at end of file diff --git a/api.py b/wallet/api.py similarity index 100% rename from api.py rename to wallet/api.py diff --git a/wallet.py b/wallet/wallet.py similarity index 99% rename from wallet.py rename to wallet/wallet.py index e9f95d58..369badb4 100644 --- a/wallet.py +++ b/wallet/wallet.py @@ -8,7 +8,7 @@ from tinydecred.util import tinyjson, helpers from tinydecred.crypto import crypto, mnemonic from tinydecred.pydecred.account import DecredAccount -from tinydecred.accounts import createNewAccountManager +from tinydecred.wallet.accounts import createNewAccountManager log = helpers.getLogger("WLLT") # , logLvl=0) From ed2bb6580ffdcc4a3ffb36ddd170ee9381833e31 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 14 Nov 2019 09:55:01 -0600 Subject: [PATCH 08/12] improved wallet loading sequence. rename http to tinyhttp to avoid Python conflict --- app.py | 62 ++++++------------ crypto/bytearray.py | 24 +++---- crypto/crypto.py | 76 +++++++++++++++++++--- examples/create_testnet_wallet.py | 4 +- examples/send_testnet.py | 8 +-- pydecred/account.py | 15 ++++- pydecred/dcrdata.py | 22 +++---- pydecred/stakepool.py | 20 +++--- pydecred/tests.py | 82 ++++++++++++------------ ui/screens.py | 101 +++++++++++++++--------------- ui/ui.py | 17 ++--- util/{http.py => tinyhttp.py} | 9 +-- util/tinyjson.py | 28 ++++----- wallet/accounts.py | 22 +++---- wallet/accounts_test.py | 4 +- wallet/api.py | 6 +- wallet/wallet.py | 16 ++--- 17 files changed, 274 insertions(+), 242 deletions(-) rename util/{http.py => tinyhttp.py} (84%) diff --git a/app.py b/app.py index d53e548b..3fa280ce 100644 --- a/app.py +++ b/app.py @@ -108,9 +108,11 @@ def __init__(self, qApp): self.loadSettings() # The initialized DcrdataBlockchain will not be connected, as that is a - # blocking operation. Connect will be called in a QThread in `initDCR`. + # blocking operation. It will be called when the wallet is open. self.dcrdata = DcrdataBlockchain(os.path.join(self.netDirectory(), "dcr.db"), cfg.net, self.getNetSetting("dcrdata"), skipConnect=True) + self.registerSignal(ui.WALLET_CONNECTED, self.syncWallet) + # appWindow is the main application window. The TinyDialog class has # methods for organizing a stack of Screen widgets. self.appWindow = screens.TinyDialog(self) @@ -135,29 +137,18 @@ def __init__(self, qApp): # If there is a wallet file, prompt for a password to open the wallet. # Otherwise, show the initialization screen. if os.path.isfile(self.walletFilename()): - def openw(path, pw): - try: - w = Wallet.openFile(path, pw) - w.open(0, pw, self.dcrdata, self.blockchainSignals) - self.appWindow.pop(self.pwDialog) - return w - except Exception as e: - log.warning("exception encountered while attempting to open wallet: %s" % formatTraceback(e)) - self.appWindow.showError("incorrect password") def login(pw): if pw is None or pw == "": self.appWindow.showError("you must enter a password to continue") else: path = self.walletFilename() - self.waitThread(openw, self.finishOpen, path, pw) + self.waitThread(self.openWallet, None, path, pw) self.getPassword(login) else: initScreen = screens.InitializationScreen(self) initScreen.setFadeIn(True) self.appWindow.stack(initScreen) - # Connect to dcrdata in a QThread. - self.makeThread(self.initDCR, self._setDCR) def waiting(self): """ Stack the waiting screen. @@ -180,7 +171,7 @@ def unwaiting(*cba, **cbk): self.appWindow.pop(self.waitingScreen) cb(*cba, **cbk) self.makeThread(tryExecute, unwaiting, f, *a, **k) - def finishOpen(self, wallet): + def openWallet(self, path, pw): """ Callback for the initial wallet load. If the load failed, probably because of a bad password, the provided wallet will be None. @@ -188,10 +179,17 @@ def finishOpen(self, wallet): Args: wallet (Wallet): The newly opened Wallet instance. """ - if wallet == None: - return - self.setWallet(wallet) - self.home() + try: + self.dcrdata.connect() + self.emitSignal(ui.BLOCKCHAIN_CONNECTED) + w = Wallet.openFile(path, pw) + w.open(0, pw, self.dcrdata, self.blockchainSignals) + self.appWindow.pop(self.pwDialog) + self.setWallet(w) + self.home() + except Exception as e: + log.warning("exception encountered while attempting to open wallet: %s" % formatTraceback(e)) + self.appWindow.showError("incorrect password") def getPassword(self, f, *args, **kwargs): """ Calls the provided function with a user-provided password string as its @@ -345,7 +343,7 @@ def setWallet(self, wallet): """ self.wallet = wallet self.emitSignal(ui.BALANCE_SIGNAL, wallet.balance()) - self.tryInitSync() + self.emitSignal(ui.WALLET_CONNECTED) def withUnlockedWallet(self, f, cb, *a, **k): """ Run the provided function with the wallet open. This is the preferred @@ -388,12 +386,12 @@ def confirm(self, msg, cb): Call the callback function only if the user confirms the prompt. """ self.appWindow.stack(self.confirmScreen.withPurpose(msg, cb)) - def tryInitSync(self): + def syncWallet(self): """ If conditions are right, start syncing the wallet. """ wallet = self.wallet - if wallet and wallet.openAccount and self.dcrdata: + if wallet and wallet.openAccount: wallet.lock() self.emitSignal(ui.WORKING_SIGNAL) self.makeThread(wallet.sync, self.doneSyncing) @@ -414,28 +412,6 @@ def balanceSync(self, balance): balance (Balance): The balance to pass to subscribed receivers. """ self.emitSignal(ui.BALANCE_SIGNAL, balance) - def initDCR(self): - """ - Connect to dcrdata. - - Returns: - bool: True on success. On exception, returns None. - """ - try: - self.dcrdata.connect() - return True - except Exception as e: - log.error("unable to initialize dcrdata connection at %s: %s" % (self.dcrdata.baseURI, formatTraceback(e))) - return None - def _setDCR(self, res): - """ - Callback to receive return value from initDCR. - """ - if not res: - self.appWindow.showError("No dcrdata connection available.") - return - self.emitSignal(ui.BLOCKCHAIN_CONNECTED) - self.tryInitSync() def getButton(self, size, text, tracked=True): """ Get a button of the requested size. diff --git a/crypto/bytearray.py b/crypto/bytearray.py index 8c77ba2d..0141101d 100644 --- a/crypto/bytearray.py +++ b/crypto/bytearray.py @@ -30,23 +30,23 @@ def decodeBA(b, copy=False): class ByteArray(object): """ - ByteArray is a bytearray manager that also implements tinyjson marshalling. - It implements a subset of bytearray's bitwise operators and provides some - convenience decodings on the fly, so operations work with various types of - input. Since bytearrays are mutable, ByteArray can also zero the internal + ByteArray is a bytearray manager that also implements tinyjson marshalling. + It implements a subset of bytearray's bitwise operators and provides some + convenience decodings on the fly, so operations work with various types of + input. Since bytearrays are mutable, ByteArray can also zero the internal value without relying on garbage collection. An - important difference between ByteArray and bytearray is - that an integer argument to ByteArray constructor will result in the - shortest possible byte representation of the integer, where for - bytearray an int argument results in a zero-valued bytearray of said - length. To get a zero-valued or zero-padded ByteArray of length n, use the + important difference between ByteArray and bytearray is + that an integer argument to ByteArray constructor will result in the + shortest possible byte representation of the integer, where for + bytearray an int argument results in a zero-valued bytearray of said + length. To get a zero-valued or zero-padded ByteArray of length n, use the `length` keyword argument. """ def __init__(self, b=b'', copy=True, length=None): """ - Set copy to False if you want to share the memory with another bytearray/ByteArray. + Set copy to False if you want to share the memory with another bytearray/ByteArray. If the type of b is not bytearray or ByteArray, copy has no effect. """ if length: @@ -94,7 +94,7 @@ def __and__(self, a): b[bLen-i-1] &= a[aLen-i-1] if i < aLen else 0 return b def __iand__(self, a): - a, aLen, b, bLen = self.decode(a) + a, aLen, b, bLen = self.decode(a) for i in range(bLen): b[bLen-i-1] &= a[aLen-i-1] if i < aLen else 0 return self @@ -169,7 +169,7 @@ def pop(self, n): return b # register the ByteArray class with the json encoder/decoder. -tinyjson.register(ByteArray) +tinyjson.register(ByteArray, "ByteArray") class TestByteArray(unittest.TestCase): def test_operators(self): diff --git a/crypto/crypto.py b/crypto/crypto.py index 136d722f..5bb07c51 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -74,6 +74,16 @@ class ParameterRangeError(Exception): pass def encodeAddress(netID, k): + """ + Base-58 encode the number, with the netID prepended byte-wise. + + Args: + netID (byte-like): The addresses network encoding ID. + k (ByteArray): The pubkey or pubkey-hash or script-hash. + + Returns: + string: Base-58 encoded address. + """ b = ByteArray(netID) b += k b += checksum(b.b) @@ -101,10 +111,30 @@ def string(self): """ return encodeAddress(self.netID, self.pkHash) def address(self): + """ + Address returns the string encoding of a pay-to-pubkey-hash address. + + Returns: + str: The encoded address. + """ return self.string() def scriptAddress(self): + """ + ScriptAddress returns the raw bytes of the address to be used when + inserting the address into a txout's script. + + Returns: + ByteArray: The script address. + """ return self.pkHash.copy() def hash160(self): + """ + Hash160 returns the Hash160(data) where data is the data normally + hashed to 160 bits from the respective address type. + + Returns: + ByteArray: The hash. + """ return self.pkHash.copy() class AddressSecpPubKey: @@ -169,8 +199,22 @@ def address(self): """ return encodeAddress(self.pubkeyHashID, hash160(self.serialize().bytes())) def scriptAddress(self): + """ + ScriptAddress returns the raw bytes of the address to be used when + inserting the address into a txout's script. + + Returns: + ByteArray: The script address. + """ return self.serialize() def hash160(self): + """ + Hash160 returns the Hash160(data) where data is the data normally + hashed to 160 bits from the respective address type. + + Returns: + ByteArray: The hash. + """ return hash160(self.serialize().bytes()) class AddressScriptHash(object): @@ -180,9 +224,6 @@ class AddressScriptHash(object): def __init__(self, netID, scriptHash): self.netID = netID self.scriptHash = scriptHash - @staticmethod - def fromScript(netID, script): - return AddressScriptHash(netID, hash160(script.b)) def string(self): """ A base-58 encoding of the pubkey hash. @@ -194,11 +235,28 @@ def string(self): def address(self): """ Address returns the string encoding of a pay-to-script-hash address. + + Returns: + str: The encoded address. """ return self.string() def scriptAddress(self): + """ + ScriptAddress returns the raw bytes of the address to be used when + inserting the address into a txout's script. + + Returns: + ByteArray: The script address. + """ return self.scriptHash.copy() def hash160(self): + """ + Hash160 returns the Hash160(data) where data is the data normally + hashed to 160 bits from the respective address type. + + Returns: + ByteArray: The hash. + """ return self.scriptHash.copy() def hmacDigest(key, msg, digestmod=hashlib.sha512): @@ -389,7 +447,7 @@ def newAddressPubKeyHash(pkHash, net, algo): Args: pkHash (ByteArray): The hash160 of the public key. - net (obj): The network parameters. + net (object): The network parameters. algo (int): The signature curve. Returns: @@ -413,7 +471,7 @@ def newAddressScriptHash(script, net): Args: script (ByteArray): the redeem script - net (obj): the network parameters + net (object): the network parameters Returns: AddressScriptHash: An address object. @@ -427,7 +485,7 @@ def newAddressScriptHashFromHash(scriptHash, net): Args: pkHash (ByteArray): The hash160 of the public key. - net (obj): The network parameters. + net (object): The network parameters. Returns: AddressScriptHash: An address object. @@ -724,7 +782,7 @@ def deriveChildAddress(self, i, net): Args: i (int): Child number. - net (obj): Network parameters. + net (object): Network parameters. Returns: Address: Child address. @@ -861,7 +919,7 @@ def __fromjson__(obj): return p def __repr__(self): return repr(self.__tojson__()) -tinyjson.register(KDFParams) +tinyjson.register(KDFParams, "KDFParams") class ScryptParams(object): """ @@ -896,7 +954,7 @@ def __fromjson__(obj): def __repr__(self): return repr(self.__tojson__()) -tinyjson.register(ScryptParams) +tinyjson.register(ScryptParams, "ScryptParams") class SecretKey(object): """ diff --git a/examples/create_testnet_wallet.py b/examples/create_testnet_wallet.py index 267c2268..3aa129a3 100644 --- a/examples/create_testnet_wallet.py +++ b/examples/create_testnet_wallet.py @@ -2,11 +2,11 @@ Copyright (c) 2019, The Decred developers This example script will prompt for a password and create a password-encrypted -testnet wallet. The mnemonic seed and an address are printed. +testnet wallet. The mnemonic seed and an address are printed. """ import os from getpass import getpass -from tinydecred.wallet import Wallet +from tinydecred.wallet.wallet import Wallet from tinydecred.pydecred import testnet from tinydecred.util.helpers import mkdir diff --git a/examples/send_testnet.py b/examples/send_testnet.py index 1c9726f8..98f8372a 100644 --- a/examples/send_testnet.py +++ b/examples/send_testnet.py @@ -1,14 +1,14 @@ """ Copyright (c) 2019, The Decred developers -This example script will send 1 DCR from a wallet as created with the -create_testnet_wallet.py example script to the return address from the testnet +This example script will send 1 DCR from a wallet as created with the +create_testnet_wallet.py example script to the return address from the testnet faucet at https://faucet.decred.org/. Before running this script, send the wallet some DCR from the faucet. """ import os from getpass import getpass -from tinydecred.wallet import Wallet +from tinydecred.wallet.wallet import Wallet from tinydecred.pydecred import testnet from tinydecred.pydecred.dcrdata import DcrdataBlockchain @@ -37,7 +37,7 @@ def balance(self, bal): acct = 0 # Every wallet has a zeroth Decred account with wallet.open(acct, password, blockchain, Signals()): wallet.sync() - try: + try: tx = wallet.sendToAddress(value, recipient) # Print the transaction ID and a dcrdata link. print("transaction ID: %s" % tx.id()) diff --git a/pydecred/account.py b/pydecred/account.py index 83b5905c..4a5b5ddb 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -134,7 +134,7 @@ def resolveUTXOs(self, blockchainUTXOs): to hook into the sync to authorize the stake pool. Args: - blockchainUTXOs (list(obj)): A list of Python objects decoded from + blockchainUTXOs (list(object)): A list of Python objects decoded from dcrdata's JSON response from ...addr/utxo endpoint. """ super().resolveUTXOs(blockchainUTXOs) @@ -195,6 +195,14 @@ def setPool(self, pool): """ assert isinstance(pool, StakePool) self.stakePools = [pool] + [p for p in self.stakePools if p.apiKey != pool.apiKey] + bc = self.blockchain + addr = pool.purchaseInfo.ticketAddress + for txid in bc.txsForAddr(addr): + self.addTxid(addr, txid) + for utxo in bc.UTXOs([addr]): + self.addUTXO(utxo) + self.updateStakeStats() + self.signals.balance(self.calcBalance(self.blockchain.tip["height"])) def hasPool(self): """ hasPool will return True if the wallet has at least one pool set. @@ -307,7 +315,7 @@ def purchaseTickets(self, qty, price): req = TicketRequest( minConf = 0, expiry = 0, - spendLimit = int(price*qty*1.1*1e8), # convert to atoms here + spendLimit = int(round(price*qty*1.1*1e8)), # convert to atoms here poolAddress = pi.poolAddress, votingAddress = pi.ticketAddress, ticketFee = 0, # use network default @@ -376,6 +384,7 @@ def sync(self, blockchain, signals): blockchain.subscribeAddresses(watchAddresses) # Signal the new balance. signals.balance(self.calcBalance(self.blockchain.tip["height"])) + return True -tinyjson.register(DecredAccount) \ No newline at end of file +tinyjson.register(DecredAccount, "DecredAccount") \ No newline at end of file diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 0a17793a..8b1019e2 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -14,7 +14,7 @@ import select import atexit import websocket -from tinydecred.util import tinyjson, helpers, database, http +from tinydecred.util import tinyjson, helpers, database, tinyhttp from tinydecred.crypto import crypto from tinydecred.crypto.bytearray import ByteArray from tinydecred.wallet.api import InsufficientFundsError @@ -78,10 +78,10 @@ def __getattr__(self, key): raise DcrDataException("SubpathError", "No subpath %s found in datapath" % (key,)) def __call__(self, *args, **kwargs): - return http.get(self.getCallsignPath(*args, **kwargs), headers=GET_HEADERS) + return tinyhttp.get(self.getCallsignPath(*args, **kwargs), headers=GET_HEADERS) def post(self, data): - return http.post(self.getCallsignPath(), data, headers=POST_HEADERS) + return tinyhttp.post(self.getCallsignPath(), data, headers=POST_HEADERS) def getSocketURIs(uri): uri = urlparse(uri) @@ -122,7 +122,7 @@ def __init__(self, baseURI, emitter=None): root = self.root = DcrdataPath() self.listEntries = [] # /list returns a json list of enpoints with parameters in template format, base/A/{param}/B - endpoints = http.get(self.baseApi + "/list", headers=GET_HEADERS) + endpoints = tinyhttp.get(self.baseApi + "/list", headers=GET_HEADERS) endpoints += InsightPaths def getParam(part): @@ -384,7 +384,7 @@ def __tojson__(self): "revocation": self.revocation, } -tinyjson.register(TicketInfo, tag="dcr.TicketInfo") +tinyjson.register(TicketInfo, "TicketInfo") class UTXO(object): """ @@ -460,7 +460,7 @@ def confirm(self, block, tx, params): Args: block (msgblock.BlockHeader): The block header. tx (dict): The dcrdata transaction. - params (obj): The network parameters. + params (object): The network parameters. """ self.height = block.height self.maturity = block.height + params.CoinbaseMaturity if tx.looksLikeCoinbase() else None @@ -522,7 +522,7 @@ def isLiveTicket(self): """ return self.tinfo and self.tinfo.status in ("immature", "live") -tinyjson.register(UTXO, tag="dcr.UTXO") +tinyjson.register(UTXO, "dcr.UTXO") def makeOutputs(pairs, chain): @@ -645,7 +645,7 @@ def subscribeBlocks(self, receiver): Subscribe to new block notifications. Args: - receiver (func(obj)): A function or method that accepts the block + receiver (func(object)): A function or method that accepts the block notifications. """ self.blockReceiver = receiver @@ -656,7 +656,7 @@ def subscribeAddresses(self, addrs, receiver=None): Args: addrs (list(str)): List of base-58 encoded addresses. - receiver (func(obj)): A function or method that accepts the address + receiver (func(object)): A function or method that accepts the address notifications. """ log.debug("subscribing to addresses %s" % repr(addrs)) @@ -855,7 +855,7 @@ def bestBlock(self): """ return self.dcrdata.block.best() def stakeDiff(self): - return self.dcrdata.stake.diff()["next"] + return int(round(self.dcrdata.stake.diff()["next"]*1e8)) def updateTip(self): """ Update the tip block. If the wallet is subscribed to block updates, @@ -1142,7 +1142,7 @@ def purchaseTickets(self, keysource, utxosource, req): # address this better and prevent address burning. # Calculate the current ticket price. - ticketPrice = int(self.stakeDiff()*1e8) + ticketPrice = self.stakeDiff() # Ensure the ticket price does not exceed the spend limit if set. if req.spendLimit > 0 and ticketPrice > req.spendLimit: diff --git a/pydecred/stakepool.py b/pydecred/stakepool.py index ab88afb0..504f8cd4 100644 --- a/pydecred/stakepool.py +++ b/pydecred/stakepool.py @@ -4,7 +4,7 @@ DcrdataClient.endpointList() for available enpoints. """ -from tinydecred.util import http, tinyjson +from tinydecred.util import tinyhttp, tinyjson from tinydecred.pydecred import txscript from tinydecred.crypto import crypto from tinydecred.crypto.bytearray import ByteArray @@ -53,7 +53,7 @@ def __tojson__(self): def __fromjson__(obj): return PurchaseInfo(obj) -tinyjson.register(PurchaseInfo) +tinyjson.register(PurchaseInfo, "PurchaseInfo") class PoolStats(object): """ @@ -116,7 +116,7 @@ def __tojson__(self): def __fromjson__(obj): return PoolStats(obj) -tinyjson.register(PoolStats) +tinyjson.register(PoolStats, "PoolStats") class StakePool(object): """ @@ -168,7 +168,7 @@ def providers(net): Returns: list(object): The vsp list. """ - vsps = http.get("https://api.decred.org/?c=gsd") + vsps = tinyhttp.get("https://api.decred.org/?c=gsd") network = "testnet" if net.Name == "testnet3" else net.Name return [vsp for vsp in vsps.values() if vsp["Network"] == network] def apiPath(self, command): @@ -202,7 +202,7 @@ def validate(self, addr): """ pi = self.purchaseInfo redeemScript = ByteArray(pi.script) - scriptAddr = crypto.AddressScriptHash.fromScript(self.net.ScriptHashAddrID, redeemScript) + scriptAddr = crypto.newAddressScriptHash(redeemScript, self.net) if scriptAddr.string() != pi.ticketAddress: raise Exception("ticket address mismatch. %s != %s" % (pi.ticketAddress, scriptAddr.string())) # extract addresses @@ -240,7 +240,7 @@ def authorize(self, address, net): raise e # address is not set data = { "UserPubKeyAddr": address } - res = http.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) + res = tinyhttp.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) if resultIsSuccess(res): self.getPurchaseInfo() self.validate(address) @@ -255,7 +255,7 @@ def getPurchaseInfo(self): """ # An error is returned if the address isn't yet set # {'status': 'error', 'code': 9, 'message': 'purchaseinfo error - no address submitted', 'data': None} - res = http.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) + res = tinyhttp.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) if resultIsSuccess(res): pi = PurchaseInfo(res["data"]) # check the script hash @@ -270,7 +270,7 @@ def getStats(self): Returns: Poolstats: The PoolStats object. """ - res = http.get(self.apiPath("stats"), headers=self.headers()) + res = tinyhttp.get(self.apiPath("stats"), headers=self.headers()) if resultIsSuccess(res): self.stats = PoolStats(res["data"]) return self.stats @@ -283,10 +283,10 @@ def setVoteBits(self, voteBits): bool: True on success. Exception raised on error. """ data = { "VoteBits": voteBits } - res = http.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) + res = tinyhttp.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) if resultIsSuccess(res): return True raise Exception("unexpected response from 'voting': %s" % repr(res)) -tinyjson.register(StakePool) +tinyjson.register(StakePool, "StakePool") diff --git a/pydecred/tests.py b/pydecred/tests.py index 37d247f8..19225e1b 100644 --- a/pydecred/tests.py +++ b/pydecred/tests.py @@ -35,7 +35,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 0, wantVote = 0, wantTreasury = 0, - ), + ), test( name = "height 0", params = mainnet, @@ -45,7 +45,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 0, wantVote = 0, wantTreasury = 0, - ), + ), test( name = "height 1 (initial payouts)", params = mainnet, @@ -55,7 +55,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 168000000000000, wantVote = 0, wantTreasury = 0, - ), + ), test( name = "height 2 (first non-special block prior voting start)", params = mainnet, @@ -65,7 +65,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1871749598, wantVote = 0, wantTreasury = 311958266, - ), + ), test( name = "height 4094 (two blocks prior to voting start)", params = mainnet, @@ -75,7 +75,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1871749598, wantVote = 0, wantTreasury = 311958266, - ), + ), test( name = "height 4095 (final block prior to voting start)", params = mainnet, @@ -85,7 +85,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1871749598, wantVote = 187174959, wantTreasury = 311958266, - ), + ), test( name = "height 4096 (voting start), 5 votes", params = mainnet, @@ -95,7 +95,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1871749598, wantVote = 187174959, wantTreasury = 311958266, - ), + ), test( name = "height 4096 (voting start), 4 votes", params = mainnet, @@ -105,7 +105,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1497399678, wantVote = 187174959, wantTreasury = 249566612, - ), + ), test( name = "height 4096 (voting start), 3 votes", params = mainnet, @@ -115,7 +115,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1123049758, wantVote = 187174959, wantTreasury = 187174959, - ), + ), test( name = "height 4096 (voting start), 2 votes", params = mainnet, @@ -125,7 +125,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 0, wantVote = 187174959, wantTreasury = 0, - ), + ), test( name = "height 6143 (final block prior to 1st reduction), 5 votes", params = mainnet, @@ -135,7 +135,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1871749598, wantVote = 187174959, wantTreasury = 311958266, - ), + ), test( name = "height 6144 (1st block in 1st reduction), 5 votes", params = mainnet, @@ -145,7 +145,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1853217423, wantVote = 185321742, wantTreasury = 308869570, - ), + ), test( name = "height 6144 (1st block in 1st reduction), 4 votes", params = mainnet, @@ -155,7 +155,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1482573938, wantVote = 185321742, wantTreasury = 247095656, - ), + ), test( name = "height 12287 (last block in 1st reduction), 5 votes", params = mainnet, @@ -165,7 +165,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1853217423, wantVote = 185321742, wantTreasury = 308869570, - ), + ), test( name = "height 12288 (1st block in 2nd reduction), 5 votes", params = mainnet, @@ -175,7 +175,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1834868736, wantVote = 183486873, wantTreasury = 305811456, - ), + ), test( name = "height 307200 (1st block in 50th reduction), 5 votes", params = mainnet, @@ -185,7 +185,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 1138096413, wantVote = 113809641, wantTreasury = 189682735, - ), + ), test( name = "height 307200 (1st block in 50th reduction), 3 votes", params = mainnet, @@ -195,7 +195,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 682857847, wantVote = 113809641, wantTreasury = 113809641, - ), + ), test( name = "height 10911744 (first zero vote subsidy 1776th reduction), 5 votes", params = mainnet, @@ -205,7 +205,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 9, wantVote = 0, wantTreasury = 1, - ), + ), test( name = "height 10954752 (first zero treasury subsidy 1783rd reduction), 5 votes", params = mainnet, @@ -215,7 +215,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 5, wantVote = 0, wantTreasury = 0, - ), + ), test( name = "height 11003904 (first zero work subsidy 1791st reduction), 5 votes", params = mainnet, @@ -225,7 +225,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= wantWork = 0, wantVote = 0, wantTreasury = 0, - ), + ), test( name = "height 11010048 (first zero full subsidy 1792nd reduction), 5 votes", params = mainnet, @@ -243,7 +243,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= cache = SubsidyCache(t.params) fullSubsidyResult = cache.calcBlockSubsidy(t.height) self.assertEqual(fullSubsidyResult, t.wantFull, t.name) - + # Ensure the PoW subsidy is the expected value. workResult = cache.calcWorkSubsidy(t.height, t.numVotes) self.assertEqual(workResult, t.wantWork, t.name) @@ -251,7 +251,7 @@ def __init__(self, name=None, params=None, height=None, numVotes=None, wantFull= # Ensure the vote subsidy is the expected value. voteResult = cache.calcStakeVoteSubsidy(t.height) self.assertEqual(voteResult, t.wantVote, t.name) - + # Ensure the treasury subsidy is the expected value. treasuryResult = cache.calcTreasurySubsidy(t.height, t.numVotes) self.assertEqual(treasuryResult, t.wantTreasury, t.name) @@ -1110,7 +1110,7 @@ def test_sign_tx(self): cachedHash = None, ) - # Since the script engine is not implmented, hard code the keys and + # Since the script engine is not implmented, hard code the keys and # check that the script signature is the same as produced by dcrd. # For compressed keys @@ -1199,7 +1199,7 @@ def priv(addr): for opCode in (opcode.OP_SSGEN, opcode.OP_SSRTX, opcode.OP_SSTX): pkScript = txscript.payToStakePKHScript(addr, opcode.OP_SSTX) # Just looking to raise an exception for now. - txscript.signTxOutput(mainnet, tx, 0, pkScript, + txscript.signTxOutput(mainnet, tx, 0, pkScript, txscript.SigHashAll, keysource, None, crypto.STEcdsaSecp256k1) @@ -1215,7 +1215,7 @@ def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAd self.scriptAddress = scriptAddress self.f = f self.net = net - + addrPKH = crypto.newAddressPubKeyHash addrSH = crypto.newAddressScriptHash addrSHH = crypto.newAddressScriptHashFromHash @@ -1257,7 +1257,7 @@ def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAd scriptAddress = ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), f = lambda: addrPKH( ByteArray("f15da1cb8d1bcb162c6ab446c95757a6e791c916"), - testnet, + testnet, crypto.STEcdsaSecp256k1 ), net = testnet, @@ -1459,7 +1459,7 @@ def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAd for test in tests: # Decode addr and compare error against valid. err = None - try: + try: decoded = txscript.decodeAddress(test.addr, test.net) except Exception as e: err = e @@ -1485,7 +1485,7 @@ def __init__(self, name="", addr="", saddr="", encoded="", valid=False, scriptAd elif isinstance(decoded, crypto.AddressSecpPubKey): # Ignore the error here since the script # address is checked below. - try: + try: saddr = ByteArray(decoded.string()) except Exception: saddr = test.saddr @@ -1542,10 +1542,10 @@ def pkAddr(b): class test: def __init__(self, name="", script=b'', addrs=None, reqSigs=-1, scriptClass=-1, exception=None): - self.name = name - self.script = script + self.name = name + self.script = script self.addrs = addrs if addrs else [] - self.reqSigs = reqSigs + self.reqSigs = reqSigs self.scriptClass = scriptClass self.exception = exception tests.append(test( @@ -1709,11 +1709,11 @@ def checkAddrs(a, b, name): t.fail("extracted address length mismatch. expected %d, got %d" % (len(a), len(b))) for av, bv in zip(a, b): if av.scriptAddress() != bv.scriptAddress(): - self.fail("scriptAddress mismatch. expected %s, got %s (%s)" % + self.fail("scriptAddress mismatch. expected %s, got %s (%s)" % (av.scriptAddress().hex(), bv.scriptAddress().hex(), name)) for i, t in enumerate(tests): - try: + try: scriptClass, addrs, reqSigs = txscript.extractPkScriptAddrs(scriptVersion, t.script, mainnet) except Exception as e: if t.exception and t.exception in str(e): @@ -1727,11 +1727,11 @@ def checkAddrs(a, b, name): checkAddrs(t.addrs, addrs, t.name) def test_pay_to_addr_script(self): """ - test_pay_to_addr_script ensures the PayToAddrScript function generates + test_pay_to_addr_script ensures the PayToAddrScript function generates the correct scripts for the various types of addresses. """ # 1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX - p2pkhMain = crypto.newAddressPubKeyHash(ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), + p2pkhMain = crypto.newAddressPubKeyHash(ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), mainnet, crypto.STEcdsaSecp256k1) # Taken from transaction: @@ -1750,7 +1750,7 @@ def test_pay_to_addr_script(self): ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), mainnet, ) - # Set the pubkey compressed. See golang TestPayToAddrScript in + # Set the pubkey compressed. See golang TestPayToAddrScript in # dcrd/tscript/standard_test.go p2pkUncompressedMain.pubkeyFormat = crypto.PKFCompressed @@ -1759,7 +1759,7 @@ class BogusAddress(crypto.AddressPubKeyHash): bogusAddress = ( ByteArray(0x0000), - ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), + ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), crypto.STEcdsaSecp256k1 ) @@ -1822,7 +1822,7 @@ def __init__(self, inAddr, expected, err): def test_script_class(self): """ test_script_class ensures all the scripts in scriptClassTests have the expected - class. + class. """ scriptVersion = 0 for test in scriptClassTests(): @@ -1905,7 +1905,7 @@ def test_purchase_ticket(self): from tinydecred.crypto.secp256k1 import curve as Curve from tinydecred.crypto import rando with TemporaryDirectory() as tempDir: - blockchain = dcrdata.DcrdataBlockchain(os.path.join(tempDir, "db.db"), testnet, "https://testnet.decred.org") + blockchain = dcrdata.DcrdataBlockchain(os.path.join(tempDir, "db.db"), testnet, "https://testnet.dcrdata.org") blockchain.connect() def broadcast(txHex): print("test skipping broadcast of transaction: %s" % txHex) @@ -1971,7 +1971,7 @@ def utxosource(amt, filter): poolAddr = crypto.AddressPubKeyHash(testnet.PubKeyHashAddrID, pkHash) scriptHash = crypto.hash160("some script. doesn't matter".encode()) scriptAddr = crypto.AddressScriptHash(testnet.ScriptHashAddrID, scriptHash) - ticketPrice = int(blockchain.stakeDiff()*1e8) + ticketPrice = self.stakeDiff() class request: minConf = 0 expiry = 0 @@ -1997,7 +1997,7 @@ def stakePool(self): stakePool = stakepool.StakePool(self.poolURL, self.apiKey) stakePool.authorize(self.signingAddress, testnet) return stakePool - def test_get_purchase_info(self): + def test_get_purchase_info(self): stakePool = self.stakePool() pi = stakePool.getPurchaseInfo() print(pi.__tojson__()) diff --git a/ui/screens.py b/ui/screens.py index 77f9da90..552a3409 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -44,7 +44,7 @@ def pixmapFromSvg(filename, w, h): class TinyDialog(QtWidgets.QFrame): """ - TinyDialog is a widget for handling Screen instances. This si the primary + TinyDialog is a widget for handling Screen instances. This is the primary window of the TinyDecred application. It has a fixed (tiny!) size. """ maxWidth = 525 @@ -130,7 +130,7 @@ def __init__(self, app): self.msg = None self.borderPen = QtGui.QPen() self.borderPen.setWidth(1) - self.mainFont = QtGui.QFont("Roboto") + self.msgFont = QtGui.QFont("Roboto", 11) self.errorBrush = QtGui.QBrush(QtGui.QColor("#fff1f1")) self.successBrush = QtGui.QBrush(QtGui.QColor("#f1fff1")) self.bgBrush = self.successBrush @@ -269,10 +269,10 @@ def paintEvent(self, e): if self.msg: painter = QtGui.QPainter(self) painter.setPen(self.borderPen) - painter.setFont(self.mainFont) + painter.setFont(self.msgFont) # painter.setBrush(self.bgBrush) - pad = 15 + pad = 5 fullWidth = self.geometry().width() column = QtCore.QRect(0, 0, fullWidth - 4*pad, 10000) @@ -457,6 +457,9 @@ def __init__(self, app): spend.clicked.connect(self.spendClicked) optsLyt.addWidget(spend, 0, 0, Q.ALIGN_LEFT) + self.spinner = Spinner(self.app, 35) + optsLyt.addWidget(self.spinner, 0, 1, Q.ALIGN_RIGHT) + # Open staking window. Button is initally hidden until sync is complete. self.stakeBttn = btn = app.getButton(SMALL, "Staking") btn.setVisible(False) @@ -520,6 +523,7 @@ def setTicketStats(self): balance = self.balance stats = acct.ticketStats() if stats and balance and balance.total > 0: + self.spinner.setVisible(False) self.stakeBttn.setVisible(True) self.statsLbl.setText("%s%% staked" % helpers.formatNumber(stats.value/balance.total*100)) self.ticketStats = stats @@ -701,6 +705,8 @@ def initPasswordCallback(self, pw): else: def create(): try: + app.dcrdata.connect() + app.emitSignal(ui.BLOCKCHAIN_CONNECTED) words, wallet = Wallet.create(app.walletFilename(), pw, cfg.net) wallet.open(0, pw, app.dcrdata, app.blockchainSignals) return words, wallet @@ -751,7 +757,10 @@ def load(pw, userPath): else: try: appWalletPath = app.walletFilename() + app.dcrdata.connect() + app.emitSignal(ui.BLOCKCHAIN_CONNECTED) wallet = Wallet.openFile(userPath, pw) + wallet.open(0, pw, app.dcrdata, app.blockchainSignals) # Save the wallet to the standard location. wallet.path = appWalletPath wallet.save() @@ -773,7 +782,7 @@ def sendToAddress(wallet, val, addr): Send the value in DCR to the provided address. """ try: - return wallet.sendToAddress(round(val*1e8), addr) # raw transaction + return wallet.sendToAddress(int(round(val*1e8)), addr) # raw transaction except Exception as e: log.error("failed to send: %s" % formatTraceback(e)) return False @@ -962,6 +971,8 @@ def pwcb(pw, words): if pw: def create(): try: + app.dcrdata.connect() + app.emitSignal(ui.BLOCKCHAIN_CONNECTED) wallet = Wallet.createFromMnemonic(words, app.walletFilename(), pw, cfg.net) wallet.open(0, pw, app.dcrdata, app.blockchainSignals) return wallet @@ -1247,8 +1258,13 @@ def __init__(self, app, callback): # Display info for a randomly chosen pool (with some filtering), and a # couple of links to aid in selecting a VSP.. self.poolUrl = Q.makeLabel("", 16, a=l, fontFamily="Roboto-Medium") - self.poolUrl.setOpenExternalLinks(True) - self.poolUrl.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse); + self.poolUrl.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + Q.addClickHandler(self.poolUrl, self.poolClicked) + self.poolLink = Q.makeLabel("visit", 14, a=l) + self.poolLink.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + Q.addHoverColor(self.poolLink, "#f3f9ff") + Q.addClickHandler(self.poolLink, self.linkClicked) + scoreLbl = Q.makeLabel("score:", 14) self.score = Q.makeLabel("", 14) feeLbl = Q.makeLabel("fee:", 14) @@ -1256,6 +1272,7 @@ def __init__(self, app, callback): usersLbl = Q.makeLabel("users:", 14) self.users = Q.makeLabel("", 14) stats, _ = Q.makeSeries( Q.HORIZONTAL, + self.poolLink, Q.STRETCH, scoreLbl, self.score, Q.STRETCH, feeLbl, self.fee, Q.STRETCH, usersLbl, self.users @@ -1265,8 +1282,6 @@ def __init__(self, app, callback): lyt.setSpacing(10) Q.addDropShadow(poolWgt) Q.addHoverColor(poolWgt, "#f5ffff") - poolWgt.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - poolWgt.mouseReleaseEvent = self.poolClicked self.layout.addWidget(poolWgt) # A button to select a different pool and a link to the master list on @@ -1303,9 +1318,7 @@ def setPools(self, pools): self.pools = pools tNow = int(time.time()) # only save pools updated within the last day - self.pools = [p for p in pools if tNow - p["LastUpdated"] < 86400] - # sort by performance - self.pools.sort(key=self.scorePool, reverse=True) + self.pools = [p for p in pools if tNow - p["LastUpdated"] < 86400 and self.scorePool(p) > 95] self.randomizePool() def randomizePool(self, e=None): @@ -1314,8 +1327,10 @@ def randomizePool(self, e=None): is based purely on voting record, e.g. voted/(voted+missed). The sorting and some initial filtering was already performed in setPools. """ - count = len(self.pools)//2+1 - pools = self.pools[:count] + pools = self.pools + count = len(pools) + if count == 0: + log.warn("no stake pools returned from server") lastIdx = self.poolIdx if count == 1: self.poolIdx = 0 @@ -1354,55 +1369,37 @@ def authPool(self): err("empty API key") return pool = StakePool(url, apiKey) - def votingAddr(wallet): + def registerPool(wallet): try: addr = wallet.openAccount.votingAddress() + pool.authorize(addr, cfg.net) + app.appWindow.showSuccess("pool authorized") wallet.openAccount.setPool(pool) wallet.save() - return pool, addr, wallet.openAccount.net - except Exception as e: - log.error("error getting voting address: %s" % e) - err("failed to get voting address") - return None - app.withUnlockedWallet(votingAddr, self.continueAuth) - def continueAuth(self, res): - """ - Follows authPool in the pool authorization process. Send the wallet - voting address to the pool and authorize the response. - """ - if not res: - self.callback(False) - return - pool, addr, net = res - app = self.app - def setAddr(): - try: - pool.authorize(addr, net) - app.appWindow.showSuccess("pool authorized") - self.callback(True) + return True except Exception as e: - # log.error("failed to set pool address: %s" % e) - # self.app.appWindow.showError("failed to set address") - # might be okay. - log.error("failed to authorize stake pool: %s" % e) - app.appWindow.showError("pool authorization failed") - self.callback(False) - - self.app.waitThread(setAddr, None) + err("pool authorization failed") + log.error("pool registration error: %s" % formatTraceback(e)) + return False + app.withUnlockedWallet(registerPool, self.callback) def showAll(self, e=None): """ - Connected to the "see all" button clicked signal. Open the full + Connected to the "see all" button clicked signal. Open the fu decred.org VSP list in the browser. """ QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://decred.org/vsp/")) - def poolClicked(self, e=None): + def linkClicked(self): + """ + Callback from the clicked signal on the pool URL QLabel. Opens the + pool's homepage in the users browser. + """ + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.poolUrl.text())) + def poolClicked(self): """ - Callback from the clicked signal on the try-this-pool widget. Opens the - pools homepage in the users browser. + Callback from the clicked signal on the try-this-pool widget. Sets the + url in the QLineEdit. """ - url = self.poolUrl.text() - self.poolIp.setText(url) - QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) + self.poolIp.setText(self.poolUrl.text()) class PoolAccountScreen(Screen): """ @@ -1643,7 +1640,7 @@ def paintEvent(self, e): def getTicketPrice(blockchain): try: - return blockchain.stakeDiff() + return blockchain.stakeDiff()/1e8 except Exception as e: log.error("error fetching ticket price: %s" % e) return False \ No newline at end of file diff --git a/ui/ui.py b/ui/ui.py index 4f889a39..7bfeb97a 100644 --- a/ui/ui.py +++ b/ui/ui.py @@ -7,13 +7,14 @@ PACKAGEDIR = os.path.dirname(os.path.realpath(__file__)) FONTDIR = os.path.join(PACKAGEDIR, "fonts") -TINY = "tiny" -SMALL = "small" +TINY = "tiny" +SMALL = "small" MEDIUM = "medium" -LARGE = "large" +LARGE = "large" -BALANCE_SIGNAL = "balance_signal" -SYNC_SIGNAL = "sync_signal" -WORKING_SIGNAL = "working_signal" -DONE_SIGNAL = "done_signal" -BLOCKCHAIN_CONNECTED = "blockchain_connected" \ No newline at end of file +BALANCE_SIGNAL = "balance_signal" +SYNC_SIGNAL = "sync_signal" +WORKING_SIGNAL = "working_signal" +DONE_SIGNAL = "done_signal" +BLOCKCHAIN_CONNECTED = "blockchain_connected" +WALLET_CONNECTED = "wallet_connected" \ No newline at end of file diff --git a/util/http.py b/util/tinyhttp.py similarity index 84% rename from util/http.py rename to util/tinyhttp.py index 96bc6822..8558a555 100644 --- a/util/http.py +++ b/util/tinyhttp.py @@ -7,9 +7,7 @@ import urllib.request as urlrequest from urllib.parse import urlencode from tinydecred.util import tinyjson -from tinydecred.util.helpers import formatTraceback, getLogger - -log = getLogger("HTTP") +from tinydecred.util.helpers import formatTraceback def get(uri, **kwargs): return request(uri, **kwargs) @@ -17,12 +15,12 @@ def get(uri, **kwargs): def post(uri, data, **kwargs): return request(uri, data, **kwargs) -def request(uri, postData=None, headers=None, urlEncode=False): +def request(uri, postData=None, headers=None, urlEncode=False, supressErr=False): try: headers = headers if headers else {} if postData: if urlEncode: - # encode the data in url query string form, without ?. + # encode the data in url query string form, without ?. encoded = urlencode(postData).encode("utf-8") else: # encode the data as json. @@ -40,5 +38,4 @@ def request(uri, postData=None, headers=None, urlEncode=False): except Exception as e: raise Exception("JSONError", "Failed to decode server response from path %s: %s : %s" % (uri, raw, formatTraceback(e))) except Exception as e: - log.error("error retrieving %s request for %s" % ("POST" if postData else "GET", uri, e)) raise Exception("RequestError", "Error encountered in requesting path %s: %s" % (uri, formatTraceback(e))) \ No newline at end of file diff --git a/util/tinyjson.py b/util/tinyjson.py index bf4bae45..9af5dab9 100644 --- a/util/tinyjson.py +++ b/util/tinyjson.py @@ -8,21 +8,15 @@ _types = {} -def clsKey(cls): - return cls.__qualname__ - -def register(cls, tag=None): +def register(cls, k): """ - Registered types will be checked for compliance with the JSONMarshaller. - When an object of a registered type is dump'ed, it's __tojson__ method + Registered types will be checked for compliance with the JSONMarshaller. + When an object of a registered type is dump'ed, it's __tojson__ method will be called to retreive the JSON-compliant dict. A special attribute _jt_ is quietly added during encoding. When that JSON object is decoded with load, the type is converted using the static __fromjson__ method. """ - if not hasattr(cls, "__fromjson__") or not hasattr(cls, "__tojson__"): - raise KeyError("register: registered types must have a __fromjson__ method") - k = tag if tag else clsKey(cls) cls.__jsontag__ = k if k in _types: raise Exception("tinyjson: mutliple attempts to register class %s" % k) @@ -38,26 +32,26 @@ def decoder(obj): def load(s): """ - Turn the string into an object with the custon decoder. + Turn the string into an object with the custon decoder. """ return json.loads(s, object_hook=decoder) def loadFile(filepath): """ Load the JSON with a decoder. This method uses load, and therefore - the custom decoder which recognizes registered types. + the custom decoder which recognizes registered types. """ with open(filepath, 'r') as f: return load(f.read()) class Encoder(json.JSONEncoder): """ - A custom encoder that works with classes implementing the JSONMarshaller interface. - A class implementing the JSONMarshaller interface will have two methods. + A custom encoder that works with classes implementing the JSONMarshaller interface. + A class implementing the JSONMarshaller interface will have two methods. 1. __fromjson__: @staticmethod. A method that will take a freshly decoded - dict and return an instance of its class. - 2. __tojson__: A method that returns an encodable version of itself, - probably a dict. + dict and return an instance of its class. + 2. __tojson__: A method that returns an encodable version of itself, + probably a dict. """ def default(self, obj): if hasattr(obj.__class__, "__jsontag__"): @@ -69,7 +63,7 @@ def default(self, obj): def dump(thing, **kwargs): """ - Encode the thing to JSON with the JSONCoder. + Encode the thing to JSON with the JSONCoder. """ return json.dumps(thing, cls=Encoder, **kwargs) diff --git a/wallet/accounts.py b/wallet/accounts.py index 3db9d38d..7775e40b 100644 --- a/wallet/accounts.py +++ b/wallet/accounts.py @@ -80,7 +80,7 @@ def newMaster(seed, network): Args: seed (bytes-like): A random seed from which the extended key is made. - network (obj): an object with BIP32 hierarchical deterministic extended + network (object): an object with BIP32 hierarchical deterministic extended key magics as attributes `HDPrivateKeyID` and `HDPublicKeyID`. Returns: @@ -126,7 +126,7 @@ def coinTypes(params): coin types. Args: - params (obj): Network parameters. + params (object): Network parameters. Returns int: Legacy coin type. @@ -183,7 +183,7 @@ def __repr__(self): "Balance(total=%.8f, available=%.8f)" % (self.total*1e-8, self.available*1e-8) ) -tinyjson.register(Balance) +tinyjson.register(Balance, "Balance") UTXO = api.UTXO @@ -733,7 +733,7 @@ def getPrivKeyForAddress(self, addr): privKey = branchKey.child(idx) return crypto.privKeyFromBytes(privKey.key) -tinyjson.register(Account) +tinyjson.register(Account, "wallet.Account") class AccountManager(object): """ @@ -844,7 +844,7 @@ def openAccount(self, acct, pw): Args: acct (int): The acccount index, which is its position in the accounts list. - net (obj): Network parameters. + net (object): Network parameters. pw (byte-like): A UTF-8-encoded user-supplied password for the account. @@ -869,7 +869,7 @@ def acctPrivateKey(self, acct, net, pw): Args: acct (int): The account's index. - net (obj): Network parameters. Not used. + net (object): Network parameters. Not used. pw (SecretKey): The secret key. Returns: @@ -886,7 +886,7 @@ def acctPublicKey(self, acct, net, pw): Args: acct (int): The account's index. - net (obj): Network parameters. Not used. + net (object): Network parameters. Not used. pw (SecretKey): The secret key. Returns: @@ -897,7 +897,7 @@ def acctPublicKey(self, acct, net, pw): account = self.accounts[acct] return account.publicExtendedKey(cryptKeyPub) -tinyjson.register(AccountManager) +tinyjson.register(AccountManager, "AccountManager") def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams, constructor=None): @@ -912,7 +912,7 @@ def createNewAccountManager(seed, pubPassphrase, privPassphrase, chainParams, co such as address generation, without decrypting the private keys. privPassphrase (byte-like): A user-supplied password to protect the private the account private keys. - chainParams (obj): Network parameters. + chainParams (object): Network parameters. Returns: AccountManager: An initialized account manager. @@ -1063,7 +1063,7 @@ def addressForPubkeyBytes(b, net): Args: b (bytes): Public key bytes. - net (obj): Network the address will be used on. + net (object): Network the address will be used on. Returns: crypto.Address: A pubkey-hash address. @@ -1102,7 +1102,7 @@ def test_accounts(self): for n in range(20): acct.nextExternalAddress() v = 5 - satoshis = v*1e8 + satoshis = int(round(v*1e8)) txid = "abcdefghijkl" vout = 2 from tinydecred.pydecred import dcrdata diff --git a/wallet/accounts_test.py b/wallet/accounts_test.py index 249fe896..7946f804 100644 --- a/wallet/accounts_test.py +++ b/wallet/accounts_test.py @@ -15,7 +15,7 @@ def addressForPubkeyBytes(b, net): Args: b (bytes): Public key bytes. - net (obj): Network the address will be used on. + net (object): Network the address will be used on. Returns: crypto.Address: A pubkey-hash address. @@ -54,7 +54,7 @@ def test_accounts(self): for n in range(20): acct.nextExternalAddress() v = 5 - satoshis = v*1e8 + satoshis = int(round(v*1e8)) txid = "abcdefghijkl" vout = 2 from tinydecred.pydecred import dcrdata diff --git a/wallet/api.py b/wallet/api.py index cfdefba6..38807892 100644 --- a/wallet/api.py +++ b/wallet/api.py @@ -85,7 +85,7 @@ def __init__(self, db, params): Args: db (KeyValueDatabase): A key-value database for storing blocks and transactions. - params (obj): Network parameters. + params (object): Network parameters. """ self.db = db self.params = params @@ -98,7 +98,7 @@ def subscribeBlocks(self, receiver): Subscribe to new block notifications. Args: - receiver (func(obj)): A function or method that accepts the block + receiver (func(object)): A function or method that accepts the block notifications. """ raise Unimplemented("subscribeBlocks not implemented") @@ -108,7 +108,7 @@ def subscribeAddresses(self, addrs, receiver): Args: addrs (list(str)): List of base-58 encoded addresses. - receiver (func(obj)): A function or method that accepts the address + receiver (func(object)): A function or method that accepts the address notifications. """ raise Unimplemented("subscribeAddresses not implemented") diff --git a/wallet/wallet.py b/wallet/wallet.py index 369badb4..da411134 100644 --- a/wallet/wallet.py +++ b/wallet/wallet.py @@ -38,7 +38,7 @@ class Wallet(object): def __init__(self): """ Args: - chain (obj): Network parameters to associate with the wallet. Should + chain (object): Network parameters to associate with the wallet. Should probably move this to the account level. """ # The path to the filesystem location of the encrypted wallet file. @@ -81,7 +81,7 @@ def create(path, password, chain, userSeed = None): path (str): Filepath to store wallet. password (str): User provided password. The password will be used to both decrypt the wallet, and unlock any accounts created. - chain (obj): Network parameters for the zeroth account ExtendedKey. + chain (object): Network parameters for the zeroth account ExtendedKey. userSeed (ByteArray): A seed for wallet generate, likely generated from a mnemonic seed word list. @@ -118,7 +118,7 @@ def createFromMnemonic(words, path, password, chain): Args: words (list(str)): mnemonic seed. Assumed to be PGP words. password (str): User password. Passed to Wallet.create. - chain (obj): Network parameters. + chain (object): Network parameters. Returns: Wallet: A wallet initialized from the seed parsed from `words`. @@ -148,8 +148,8 @@ def setAccountHandlers(self, blockchain, signals): Set blockchain params and user defined callbacks for accounts. Args: - blockchain (obj): An api.Blockchain for accounts. - signals (obj): An api.Signals. + blockchain (object): An api.Blockchain for accounts. + signals (object): An api.Signals. """ self.blockchain = blockchain self.signals = signals @@ -190,8 +190,8 @@ def open(self, acct, password, blockchain, signals): acct (int): The account number to open. password (str): Wallet password. Should be the same as used to open the wallet. - blockchain (obj): An api.Blockchain for the account. - signals (obj): An api.Signals. + blockchain (object): An api.Blockchain for the account. + signals (object): An api.Signals. Returns: Wallet: The wallet with the default account open. @@ -320,4 +320,4 @@ def sendToAddress(self, value, address, feeRate=None): self.save() return tx -tinyjson.register(Wallet) \ No newline at end of file +tinyjson.register(Wallet, "wallet.Wallet") \ No newline at end of file From f46c27d5236f1b4bb21cd02c25c91db2f5aba757 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 18 Nov 2019 11:48:14 -0600 Subject: [PATCH 09/12] missing docs. stakepool -> vsp. other fixes --- crypto/crypto.py | 8 +- pydecred/account.py | 46 +++++-- pydecred/calc.py | 25 ++-- pydecred/dcrdata.py | 23 +++- pydecred/stakepool.py | 292 ----------------------------------------- pydecred/tests.py | 8 +- pydecred/txscript.py | 72 ++++++++++- pydecred/vsp.py | 293 ++++++++++++++++++++++++++++++++++++++++++ ui/screens.py | 10 +- util/mpl.py | 6 +- 10 files changed, 444 insertions(+), 339 deletions(-) delete mode 100644 pydecred/stakepool.py create mode 100644 pydecred/vsp.py diff --git a/crypto/crypto.py b/crypto/crypto.py index 5bb07c51..9f0d50d8 100644 --- a/crypto/crypto.py +++ b/crypto/crypto.py @@ -139,9 +139,9 @@ def hash160(self): class AddressSecpPubKey: """ - AddressSecpPubKey represents and address, which is a pubkey hash and it's - base-58 encoding. Argument pubkey should be a ByteArray corresponding the - the serializedCompressed public key (33 bytes). + AddressSecpPubKey represents an address, which is a pubkey hash and its + base-58 encoding. Argument pubkey should be a ByteArray corresponding to the + serializedCompressed public key (33 bytes). """ def __init__(self, serializedPubkey, net): pubkey = Curve.parsePubKey(serializedPubkey) @@ -422,7 +422,7 @@ def newAddressPubKey(decoded, net): if len(decoded) == 33: # First byte is the signature suite and ybit. suite = decoded[0] - suite &= ~(1 << 7) + suite &= 127 ybit = not (decoded[0]&(1<<7) == 0) toAppend = 0x02 if ybit: diff --git a/pydecred/account.py b/pydecred/account.py index 4a5b5ddb..6215898c 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -8,9 +8,9 @@ from tinydecred.wallet.accounts import Account from tinydecred.util import tinyjson, helpers -from tinydecred.crypto.crypto import AddressSecpPubKey +from tinydecred.crypto.crypto import AddressSecpPubKey, CrazyKeyError from tinydecred.pydecred import txscript -from tinydecred.pydecred.stakepool import StakePool +from tinydecred.pydecred.vsp import VotingServiceProvider log = helpers.getLogger("DCRACCT") @@ -31,7 +31,7 @@ def __init__(self, priv, internal): """ Args: priv (func): func(address : string) -> PrivateKey. Retrieves the - associated with the specified address. + private key associated with the specified address. internal (func): func() -> address : string. Get a new internal address. """ @@ -102,6 +102,7 @@ def __init__(self, *a, **k): self.stakePools = [] self.blockchain = None self.signals = None + self._votingKey = None def __tojson__(self): obj = super().__tojson__() return helpers.recursiveUpdate(obj, { @@ -115,6 +116,29 @@ def __fromjson__(obj): acct.tickets = obj["tickets"] acct.stakePools = obj["stakePools"] return acct + def open(self, pw): + """ + Open the Decred account. Runs the parent's method, then performs some + Decred-specific initialization. + """ + super().open(pw) + # The voting key is the first non-crazy stake-branch child. + for i in range(3): + try: + self._votingKey = self.privKey.child(STAKE_BRANCH).child(i).privateKey() + return + except CrazyKeyError: + continue + # It is realistically impossible to reach here. + raise Exception("error finding voting key") + def close(self): + """ + Close the Decred account. Runs the parent's method, then performs some + Decred-specific clean up. + """ + super().close() + self._votingKey.key.zero() + self._votingKey = None def updateStakeStats(self): """ Updates the stake stats object. @@ -173,9 +197,9 @@ def watchAddrs(self): return self.addTicketAddresses(super().watchAddrs()) def votingKey(self): """ - For now, the voting key is the zeroth child + For now, the voting key is the zeroth child. """ - return self.privKey.child(STAKE_BRANCH).child(0).privateKey() + return self._votingKey def votingAddress(self): """ The voting address is the pubkey address (not pubkey-hash) for the @@ -191,9 +215,9 @@ def setPool(self, pool): Set the specified pool as the default. Args: - pool (stakepool.StakePool): The stake pool object. + pool (vsp.VotingServiceProvider): The stake pool object. """ - assert isinstance(pool, StakePool) + assert isinstance(pool, VotingServiceProvider) self.stakePools = [pool] + [p for p in self.stakePools if p.apiKey != pool.apiKey] bc = self.blockchain addr = pool.purchaseInfo.ticketAddress @@ -210,10 +234,11 @@ def hasPool(self): return self.stakePool() != None def stakePool(self): """ - stakePool is the default stakepool.StakePool for the account. + stakePool is the default vsp.VotingServiceProvider for the + account. Returns: - staekpool.StakePool: The default stake pool object. + vsp.VotingServiceProvider: The default stake pool object. """ if self.stakePools: return self.stakePools[0] @@ -221,6 +246,9 @@ def stakePool(self): def ticketStats(self): """ A getter for the stakeStats. + + Returns: + TicketStats: The staking stats. """ return self.stakeStats def blockSignal(self, sig): diff --git a/pydecred/calc.py b/pydecred/calc.py index b35216bc..4b6d3317 100644 --- a/pydecred/calc.py +++ b/pydecred/calc.py @@ -228,7 +228,7 @@ def attackCost(ticketFraction=None, xcRate=None, blockHeight=None, roi=None, """ Calculate the cost of attack, which is the minimum fiat value of equipment, tickets, and rental expenditures required to outpace the main chain. - + The cost of attack can be calculated in direct mode or reverse mode, depending on the parameters provided. Provide a `nethash` and a `ticketPrice` to calculate in direct mode. Omit the `nethash` and `ticketPrice`, and instead provide an `roi` and `apy` to calculate in reverse mode. @@ -276,7 +276,7 @@ def attackCost(ticketFraction=None, xcRate=None, blockHeight=None, roi=None, device = device if device else MODEL_DEVICE if nethash is None: - if roi is None: # mining ROI could be zero + if roi is None: # mining ROI could be zero raise Exception("minimizeY: Either a nethash or an roi must be provided") nethash = ReverseEquations.networkHashrate(device, xcRate, roi, blockHeight, blockTime, powSplit) if rentability or rentalRatio: @@ -309,7 +309,7 @@ def purePowAttackCost(xcRate=None, blockHeight=None, roi=None, blockTime=None, device = device if device else MODEL_DEVICE treasurySplit = treasurySplit if treasurySplit else NETWORK.TREASURY_SPLIT if nethash is None: - if roi is None: # mining ROI could be zero + if roi is None: # mining ROI could be zero raise Exception("minimizeY: Either a nethash or an roi must be provided") nethash = ReverseEquations.networkHashrate(device, xcRate, roi, blockHeight, blockTime, 1-treasurySplit) if rentability or rentalRatio: @@ -343,11 +343,9 @@ class SubsidyCache(object): calculations for blocks and votes, including the max potential subsidy for given block heights, the proportional proof-of-work subsidy, the proportional proof of stake per-vote subsidy, and the proportional treasury subsidy. - - It makes using of caching to avoid repeated calculations. + + It makes use of caching to avoid repeated calculations. """ - # The following fields are protected by the mtx mutex. - # # cache houses the cached subsidies keyed by reduction interval. # # cachedIntervals contains an ordered list of all cached intervals. It is @@ -367,7 +365,7 @@ def __init__(self, params): # be consider valid by consensus. # # totalProportions is the sum of the PoW, PoS, and Treasury proportions. - self.minVotesRequired = (params.TicketsPerBlock // 2) + 1 + self.minVotesRequired = (params.TicketsPerBlock // 2) + 1 self.totalProportions = (params.WorkRewardProportion + params.StakeRewardProportion + params.BlockTaxProportion) @@ -387,8 +385,7 @@ def calcBlockSubsidy(self, height): # Calculate the reduction interval associated with the requested height and # attempt to look it up in cache. When it's not in the cache, look up the - # latest cached interval and subsidy while the mutex is still held for use - # below. + # latest cached interval and subsidy. reqInterval = height // self.params.SubsidyReductionInterval if reqInterval in self.cache: return self.cache[reqInterval] @@ -472,14 +469,14 @@ def calcStakeVoteSubsidy(self, height): CalcStakeVoteSubsidy returns the subsidy for a single stake vote for a block. It is calculated as a proportion of the total subsidy and max potential number of votes per block. - + Unlike the Proof-of-Work and Treasury subsidies, the subsidy that votes receive is not reduced when a block contains less than the maximum number of votes. Consequently, this does not accept the number of votes. However, it is important to note that blocks that do not receive the minimum required number of votes for a block to be valid by consensus won't actually produce any vote subsidy either since they are invalid. - + This function is safe for concurrent access. """ # Votes have no subsidy prior to the point voting begins. The minus one @@ -505,11 +502,11 @@ def calcTreasurySubsidy(self, height, voters): a block. It is calculated as a proportion of the total subsidy and further reduced proportionally depending on the number of votes once the height at which voting begins has been reached. - + Note that passing a number of voters fewer than the minimum required for a block to be valid by consensus along with a height greater than or equal to the height at which voting begins will return zero. - + This function is safe for concurrent access. """ # The first two blocks have special subsidy rules. diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 8b1019e2..45d9da28 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -855,6 +855,12 @@ def bestBlock(self): """ return self.dcrdata.block.best() def stakeDiff(self): + """ + Get the current stake difficulty a.k.a. ticket price. + + Returns: + int: The ticket price. + """ return int(round(self.dcrdata.stake.diff()["next"]*1e8)) def updateTip(self): """ @@ -1113,6 +1119,21 @@ def purchaseTickets(self, keysource, utxosource, req): limit in the request is greater than or equal to 0, tickets that cost more than that limit will return an error that not enough funds are available. + + Args: + keysource (account.KeySource): A source for private keys. + utxosource (func(int, filterFunc) -> list(UTXO)): A source for + UTXOs. The filterFunc is an optional function to filter UTXOs, + and is of the form func(UTXO) -> bool. + req (account.TicketRequest): The ticket data. + + Returns: + tuple: First element is the split transaction. Second is a list of + generated tickets. + list (msgtx.TxOut): The outputs spent for the split transaction. + internalOutputs (msgtx.TxOut): New outputs that fund internal + addresses. + """ self.updateTip() # account minConf is zero for regular outputs for now. Need to make that @@ -1225,8 +1246,6 @@ def purchaseTickets(self, keysource, utxosource, req): # paying themselves with the larger ticket commitment. splitOuts = [] for i in range(req.count): - # No pool used. - # Stake pool used. userAmt = neededPerTicket - poolFeeAmt poolAmt = poolFeeAmt diff --git a/pydecred/stakepool.py b/pydecred/stakepool.py deleted file mode 100644 index 504f8cd4..00000000 --- a/pydecred/stakepool.py +++ /dev/null @@ -1,292 +0,0 @@ -""" -Copyright (c) 2019, Brian Stafford -See LICENSE for details - -DcrdataClient.endpointList() for available enpoints. -""" -from tinydecred.util import tinyhttp, tinyjson -from tinydecred.pydecred import txscript -from tinydecred.crypto import crypto -from tinydecred.crypto.bytearray import ByteArray - -def resultIsSuccess(res): - """ - JSON-decoded stake pool responses have a common base structure that enables - a universal success check. - - Args: - res (object): The freshly-decoded-from-JSON response. - - Returns: - bool: True if result fields indicate success. - """ - return res and isinstance(res, object) and "status" in res and res["status"] == "success" - -class PurchaseInfo(object): - """ - The PurchaseInfo models the response from the 'getpurchaseinfo' endpoint. - This information is required for validating the pool and creating tickets. - """ - def __init__(self, pi): - """ - Args: - pi (object): The response from the 'getpurchaseinfo' request. - """ - get = lambda k, default=None: pi[k] if k in pi else default - self.poolAddress = get("PoolAddress") - self.poolFees = get("PoolFees") - self.script = get("Script") - self.ticketAddress = get("TicketAddress") - self.voteBits = get("VoteBits") - self.voteBitsVersion = get("VoteBitsVersion") - def __tojson__(self): - # using upper-camelcase to match keys in api response - return { - "PoolAddress": self.poolAddress, - "PoolFees": self.poolFees, - "Script": self.script, - "TicketAddress": self.ticketAddress, - "VoteBits": self.voteBits, - "VoteBitsVersion": self.voteBitsVersion, - } - @staticmethod - def __fromjson__(obj): - return PurchaseInfo(obj) - -tinyjson.register(PurchaseInfo, "PurchaseInfo") - -class PoolStats(object): - """ - PoolStats models the response from the 'stats' endpoint. - """ - def __init__(self, stats): - """ - Args: - stats (object): The response from the 'stats' request. - """ - get = lambda k, default=None: stats[k] if k in stats else default - self.allMempoolTix = get("AllMempoolTix") - self.apiVersionsSupported = get("APIVersionsSupported") - self.blockHeight = get("BlockHeight") - self.difficulty = get("Difficulty") - self.expired = get("Expired") - self.immature = get("Immature") - self.live = get("Live") - self.missed = get("Missed") - self.ownMempoolTix = get("OwnMempoolTix") - self.poolSize = get("PoolSize") - self.proportionLive = get("ProportionLive") - self.proportionMissed = get("ProportionMissed") - self.revoked = get("Revoked") - self.totalSubsidy = get("TotalSubsidy") - self.voted = get("Voted") - self.network = get("Network") - self.poolEmail = get("PoolEmail") - self.poolFees = get("PoolFees") - self.poolStatus = get("PoolStatus") - self.userCount = get("UserCount") - self.userCountActive = get("UserCountActive") - self.version = get("Version") - def __tojson__(self): - return { - "AllMempoolTix": self.allMempoolTix, - "APIVersionsSupported": self.apiVersionsSupported, - "BlockHeight": self.blockHeight, - "Difficulty": self.difficulty, - "Expired": self.expired, - "Immature": self.immature, - "Live": self.live, - "Missed": self.missed, - "OwnMempoolTix": self.ownMempoolTix, - "PoolSize": self.poolSize, - "ProportionLive": self.proportionLive, - "ProportionMissed": self.proportionMissed, - "Revoked": self.revoked, - "TotalSubsidy": self.totalSubsidy, - "Voted": self.voted, - "Network": self.network, - "PoolEmail": self.poolEmail, - "PoolFees": self.poolFees, - "PoolStatus": self.poolStatus, - "UserCount": self.userCount, - "UserCountActive": self.userCountActive, - "Version": self.version, - } - @staticmethod - def __fromjson__(obj): - return PoolStats(obj) - -tinyjson.register(PoolStats, "PoolStats") - -class StakePool(object): - """ - A StakePool is a voting service provider, uniquely defined by it's URL. The - StakePool class has methods for interacting with the VSP API. StakePool is - JSON-serializable if used with tinyjson, so can be stored as part of an - Account in the wallet. - """ - def __init__(self, url, apiKey): - """ - Args: - url (string): The stake pool URL. - apiKey (string): The API key assigned to the VSP account during - registration. - """ - self.url = url - # The network parameters are not JSON-serialized, so must be set during - # a call to StakePool.authorize before using the StakePool. - self.net = None - # The signingAddress (also called a votingAddress in other contexts) is - # the P2SH 1-of-2 multi-sig address that spends SSTX outputs. - self.signingAddress = None - self.apiKey = apiKey - self.lastConnection = 0 - self.purchaseInfo = None - self.stats = None - self.err = None - def __tojson__(self): - return { - "url": self.url, - "apiKey": self.apiKey, - "purchaseInfo": self.purchaseInfo, - "stats": self.stats, - } - @staticmethod - def __fromjson__(obj): - sp = StakePool(obj["url"], obj["apiKey"]) - sp.purchaseInfo = obj["purchaseInfo"] - sp.stats = obj["stats"] - return sp - @staticmethod - def providers(net): - """ - A static method to get the current Decred VSP list. - - Args: - net (string): The network name. - - Returns: - list(object): The vsp list. - """ - vsps = tinyhttp.get("https://api.decred.org/?c=gsd") - network = "testnet" if net.Name == "testnet3" else net.Name - return [vsp for vsp in vsps.values() if vsp["Network"] == network] - def apiPath(self, command): - """ - The full URL for the specified command. - - Args: - command (string): The API endpoint specifier. - - Returns: - string: The full URL. - """ - return "%s/api/v2/%s" % (self.url, command) - def headers(self): - """ - Make the API request headers. - - Returns: - object: The headers as a Python object. - """ - return {"Authorization": "Bearer %s" % self.apiKey} - def validate(self, addr): - """ - Validate performs some checks that the PurchaseInfo provided by the - stake pool API is valid for this given voting address. Exception is - raised on failure to validate. - - Args: - addr (string): The base58-encoded pubkey address that the wallet - uses to vote. - """ - pi = self.purchaseInfo - redeemScript = ByteArray(pi.script) - scriptAddr = crypto.newAddressScriptHash(redeemScript, self.net) - if scriptAddr.string() != pi.ticketAddress: - raise Exception("ticket address mismatch. %s != %s" % (pi.ticketAddress, scriptAddr.string())) - # extract addresses - scriptType, addrs, numSigs = txscript.extractPkScriptAddrs(0, redeemScript, self.net) - if numSigs != 1: - raise Exception("expected 2 required signatures, found 2") - found = False - signAddr = txscript.decodeAddress(addr, self.net) - for addr in addrs: - if addr.string() == signAddr.string(): - found = True - break - if not found: - raise Exception("signing pubkey not found in redeem script") - def authorize(self, address, net): - """ - Authorize the stake pool for the provided address and network. Exception - is raised on failure to authorize. - - Args: - address (string): The base58-encoded pubkey address that the wallet - uses to vote. - net (object): The network parameters. - """ - # An error is returned if the address is already set - # {'status': 'error', 'code': 6, 'message': 'address error - address already submitted'} - # First try to get the purchase info directly. - self.net = net - try: - self.getPurchaseInfo() - self.validate(address) - except Exception as e: - if "code" not in self.err or self.err["code"] != 9: - # code 9 is address not set - raise e - # address is not set - data = { "UserPubKeyAddr": address } - res = tinyhttp.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) - if resultIsSuccess(res): - self.getPurchaseInfo() - self.validate(address) - else: - raise Exception("unexpected response from 'address': %s" % repr(res)) - def getPurchaseInfo(self): - """ - Get the purchase info from the stake pool API. - - Returns: - PurchaseInfo: The PurchaseInfo object. - """ - # An error is returned if the address isn't yet set - # {'status': 'error', 'code': 9, 'message': 'purchaseinfo error - no address submitted', 'data': None} - res = tinyhttp.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) - if resultIsSuccess(res): - pi = PurchaseInfo(res["data"]) - # check the script hash - self.purchaseInfo = pi - return self.purchaseInfo - self.err = res - raise Exception("unexpected response from 'getpurchaseinfo': %r" % (res, )) - def getStats(self): - """ - Get the stats from the stake pool API. - - Returns: - Poolstats: The PoolStats object. - """ - res = tinyhttp.get(self.apiPath("stats"), headers=self.headers()) - if resultIsSuccess(res): - self.stats = PoolStats(res["data"]) - return self.stats - raise Exception("unexpected response from 'stats': %s" % repr(res)) - def setVoteBits(self, voteBits): - """ - Set the vote preference on the StakePool. - - Returns: - bool: True on success. Exception raised on error. - """ - data = { "VoteBits": voteBits } - res = tinyhttp.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) - if resultIsSuccess(res): - return True - raise Exception("unexpected response from 'voting': %s" % repr(res)) - -tinyjson.register(StakePool, "StakePool") - diff --git a/pydecred/tests.py b/pydecred/tests.py index 19225e1b..a1e73bab 100644 --- a/pydecred/tests.py +++ b/pydecred/tests.py @@ -4,7 +4,7 @@ import time from tempfile import TemporaryDirectory from tinydecred.pydecred.calc import SubsidyCache -from tinydecred.pydecred import mainnet, testnet, txscript, dcrdata, stakepool +from tinydecred.pydecred import mainnet, testnet, txscript, dcrdata, vsp from tinydecred.pydecred.wire import wire, msgtx from tinydecred.crypto.bytearray import ByteArray from tinydecred.crypto import crypto, opcode @@ -1971,7 +1971,7 @@ def utxosource(amt, filter): poolAddr = crypto.AddressPubKeyHash(testnet.PubKeyHashAddrID, pkHash) scriptHash = crypto.hash160("some script. doesn't matter".encode()) scriptAddr = crypto.AddressScriptHash(testnet.ScriptHashAddrID, scriptHash) - ticketPrice = self.stakeDiff() + ticketPrice = blockchain.stakeDiff() class request: minConf = 0 expiry = 0 @@ -1984,7 +1984,7 @@ class request: txFee = 0 ticket, spent, newUTXOs = blockchain.purchaseTickets(KeySource(), utxosource, request()) -class TestStakePool(unittest.TestCase): +class TestVSP(unittest.TestCase): def setUp(self): self.poolURL = "https://teststakepool.decred.org" self.apiKey = "" @@ -1994,7 +1994,7 @@ def setUp(self): print(" no stake pool credentials provided. skipping stake pool test") raise unittest.SkipTest def stakePool(self): - stakePool = stakepool.StakePool(self.poolURL, self.apiKey) + stakePool = vsp.VotingServiceProvider(self.poolURL, self.apiKey) stakePool.authorize(self.signingAddress, testnet) return stakePool def test_get_purchase_info(self): diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 882a34b2..a798385a 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -2038,6 +2038,15 @@ def getP2PKHOpCode(pkScript): return opNonstake def spendScriptSize(pkScript): + """ + Get the byte-length of the spend script. + + Args: + pkScript (ByteArray): The pubkey script. + + Returns: + int: Byte-length of script. + """ # Unspent credits are currently expected to be either P2PKH or # P2PK, P2PKH/P2SH nested in a revocation/stakechange/vote output. scriptClass = getScriptClass(DefaultScriptVersion, pkScript) @@ -2204,6 +2213,12 @@ def isUnspendable(amount, pkScript): NOTE: This function is only valid for version 0 scripts. Since the function does not accept a script version, the results are undefined for other script versions. + + Args: + pkScript (ByteArray): The pubkey script. + + Returns: + bool: True is script is unspendable. """ # The script is unspendable if starts with OP_RETURN or is guaranteed to # fail at execution due to being larger than the max allowed script size. @@ -2221,6 +2236,13 @@ def isDustOutput(output, relayFeePerKb): isDustOutput determines whether a transaction output is considered dust. Transactions with dust outputs are not standard and are rejected by mempools with default policies. + + Args: + output (wire.TxOut): The transaction output. + relayFeePerKb: Minimum transaction fee allowable. + + Returns: + bool: True if output is a dust output. """ # Unspendable outputs which solely carry data are not checked for dust. if getScriptClass(output.version, output.pkScript) == NullDataTy: @@ -2240,6 +2262,14 @@ def estimateSerializeSizeFromScriptSizes(inputSizes, outputSizes, changeScriptSi worst-case sizes. The estimated size is incremented for an additional change output if changeScriptSize is greater than 0. Passing 0 does not add a change output. + + Args: + intputSizes (list(int)): The sizes of the input scripts. + outputSizes (list(int)): The sizes of the output scripts. + changeScriptSize (int): The size of the change script. + + Returns: + int: The estimated serialized transaction size. """ # Generate and sum up the estimated sizes of the inputs. txInsSize = 0 @@ -2267,8 +2297,18 @@ def stakePoolTicketFee(stakeDiff, relayFee, height, poolFee, subsidyCache, param stakePoolTicketFee determines the stake pool ticket fee for a given ticket from the passed percentage. Pool fee as a percentage is truncated from 0.01% to 100.00%. This all must be done with integers. - See the included doc.go of this package for more information about the - calculation of this fee. + + Args: + stakeDiff (int): The ticket price. + relayFee (int): Transaction fees. + height (int): Current block height. + poolFee (int): The pools fee, as percent. + subsidyCache (calc.SubsidyCache): A subsidy cache. + params (object): Network parameters. + + Returns: + int: The stake pool ticket fee. + """ # Shift the decimal two places, e.g. 1.00% # to 100. This assumes that the proportion @@ -2319,10 +2359,18 @@ def sstxNullOutputAmounts(amounts, changeAmounts, amountTicket): """ sstxNullOutputAmounts takes an array of input amounts, change amounts, and a ticket purchase amount, calculates the adjusted proportion from the purchase - amount, stores it in an array, then returns the array. That is, for any given - SStx, this function calculates the proportional outputs that any single user - should receive. - Returns: (1) Fees (2) Output Amounts + amount, stores it in an array, then returns the array. That is, for any + given SStx, this function calculates the proportional outputs that any + single user should receive. + + Args: + amounts (list(int)): Input values. + changeAmounts (list(int)): The change output values. + amountTicket: Ticket price. + + Returns: + int: Ticket fees. + list(int): Adjusted output amounts. """ lengthAmounts = len(amounts) @@ -2356,6 +2404,18 @@ def makeTicket(params, inputPool, inputMain, addrVote, addrSubsidy, ticketCost, makeTicket creates a ticket from a split transaction output. It can optionally create a ticket that pays a fee to a pool if a pool input and pool address are passed. + + Args: + params (object): Network parameters. + inputPool (ExtendedOutPoint): The pool input's extended outpoint. + inputMain (ExtendedOutPoint): The wallet input's extended outpoint. + addrVote (Address): The voting address. + addrSubsidy (Address): Wallet's stake commitment address. + ticketCost (int): The ticket price. + addrPool (Address): The pool's commitment address. + + Returns: + wire.MsgTx: The ticket. """ mtx = msgtx.MsgTx.new() diff --git a/pydecred/vsp.py b/pydecred/vsp.py new file mode 100644 index 00000000..b58b7fab --- /dev/null +++ b/pydecred/vsp.py @@ -0,0 +1,293 @@ +""" +Copyright (c) 2019, Brian Stafford +See LICENSE for details + +DcrdataClient.endpointList() for available enpoints. +""" +from tinydecred.util import tinyhttp, tinyjson +from tinydecred.pydecred import txscript +from tinydecred.crypto import crypto +from tinydecred.crypto.bytearray import ByteArray + +def resultIsSuccess(res): + """ + JSON-decoded stake pool responses have a common base structure that enables + a universal success check. + + Args: + res (object): The freshly-decoded-from-JSON response. + + Returns: + bool: True if result fields indicate success. + """ + return res and isinstance(res, object) and "status" in res and res["status"] == "success" + +class PurchaseInfo(object): + """ + The PurchaseInfo models the response from the 'getpurchaseinfo' endpoint. + This information is required for validating the pool and creating tickets. + """ + def __init__(self, pi): + """ + Args: + pi (object): The response from the 'getpurchaseinfo' request. + """ + get = lambda k, default=None: pi[k] if k in pi else default + self.poolAddress = get("PoolAddress") + self.poolFees = get("PoolFees") + self.script = get("Script") + self.ticketAddress = get("TicketAddress") + self.voteBits = get("VoteBits") + self.voteBitsVersion = get("VoteBitsVersion") + def __tojson__(self): + # using upper-camelcase to match keys in api response + return { + "PoolAddress": self.poolAddress, + "PoolFees": self.poolFees, + "Script": self.script, + "TicketAddress": self.ticketAddress, + "VoteBits": self.voteBits, + "VoteBitsVersion": self.voteBitsVersion, + } + @staticmethod + def __fromjson__(obj): + return PurchaseInfo(obj) + +tinyjson.register(PurchaseInfo, "PurchaseInfo") + +class PoolStats(object): + """ + PoolStats models the response from the 'stats' endpoint. + """ + def __init__(self, stats): + """ + Args: + stats (object): The response from the 'stats' request. + """ + get = lambda k, default=None: stats[k] if k in stats else default + self.allMempoolTix = get("AllMempoolTix") + self.apiVersionsSupported = get("APIVersionsSupported") + self.blockHeight = get("BlockHeight") + self.difficulty = get("Difficulty") + self.expired = get("Expired") + self.immature = get("Immature") + self.live = get("Live") + self.missed = get("Missed") + self.ownMempoolTix = get("OwnMempoolTix") + self.poolSize = get("PoolSize") + self.proportionLive = get("ProportionLive") + self.proportionMissed = get("ProportionMissed") + self.revoked = get("Revoked") + self.totalSubsidy = get("TotalSubsidy") + self.voted = get("Voted") + self.network = get("Network") + self.poolEmail = get("PoolEmail") + self.poolFees = get("PoolFees") + self.poolStatus = get("PoolStatus") + self.userCount = get("UserCount") + self.userCountActive = get("UserCountActive") + self.version = get("Version") + def __tojson__(self): + return { + "AllMempoolTix": self.allMempoolTix, + "APIVersionsSupported": self.apiVersionsSupported, + "BlockHeight": self.blockHeight, + "Difficulty": self.difficulty, + "Expired": self.expired, + "Immature": self.immature, + "Live": self.live, + "Missed": self.missed, + "OwnMempoolTix": self.ownMempoolTix, + "PoolSize": self.poolSize, + "ProportionLive": self.proportionLive, + "ProportionMissed": self.proportionMissed, + "Revoked": self.revoked, + "TotalSubsidy": self.totalSubsidy, + "Voted": self.voted, + "Network": self.network, + "PoolEmail": self.poolEmail, + "PoolFees": self.poolFees, + "PoolStatus": self.poolStatus, + "UserCount": self.userCount, + "UserCountActive": self.userCountActive, + "Version": self.version, + } + @staticmethod + def __fromjson__(obj): + return PoolStats(obj) + +tinyjson.register(PoolStats, "PoolStats") + +class VotingServiceProvider(object): + """ + A VotingServiceProvider is a voting service provider, uniquely defined by + its URL. The VotingServiceProvider class has methods for interacting with + the VSP API. VotingServiceProvider is JSON-serializable if used with + tinyjson, so can be stored as part of an Account in the wallet. + """ + def __init__(self, url, apiKey): + """ + Args: + url (string): The stake pool URL. + apiKey (string): The API key assigned to the VSP account during + registration. + """ + self.url = url + # The network parameters are not JSON-serialized, so must be set during + # a call to VotingServiceProvider.authorize before using the + # VotingServiceProvider. + self.net = None + # The signingAddress (also called a votingAddress in other contexts) is + # the P2SH 1-of-2 multi-sig address that spends SSTX outputs. + self.signingAddress = None + self.apiKey = apiKey + self.lastConnection = 0 + self.purchaseInfo = None + self.stats = None + self.err = None + def __tojson__(self): + return { + "url": self.url, + "apiKey": self.apiKey, + "purchaseInfo": self.purchaseInfo, + "stats": self.stats, + } + @staticmethod + def __fromjson__(obj): + sp = VotingServiceProvider(obj["url"], obj["apiKey"]) + sp.purchaseInfo = obj["purchaseInfo"] + sp.stats = obj["stats"] + return sp + @staticmethod + def providers(net): + """ + A static method to get the current Decred VSP list. + + Args: + net (string): The network name. + + Returns: + list(object): The vsp list. + """ + vsps = tinyhttp.get("https://api.decred.org/?c=gsd") + network = "testnet" if net.Name == "testnet3" else net.Name + return [vsp for vsp in vsps.values() if vsp["Network"] == network] + def apiPath(self, command): + """ + The full URL for the specified command. + + Args: + command (string): The API endpoint specifier. + + Returns: + string: The full URL. + """ + return "%s/api/v2/%s" % (self.url, command) + def headers(self): + """ + Make the API request headers. + + Returns: + object: The headers as a Python object. + """ + return {"Authorization": "Bearer %s" % self.apiKey} + def validate(self, addr): + """ + Validate performs some checks that the PurchaseInfo provided by the + stake pool API is valid for this given voting address. Exception is + raised on failure to validate. + + Args: + addr (string): The base58-encoded pubkey address that the wallet + uses to vote. + """ + pi = self.purchaseInfo + redeemScript = ByteArray(pi.script) + scriptAddr = crypto.newAddressScriptHash(redeemScript, self.net) + if scriptAddr.string() != pi.ticketAddress: + raise Exception("ticket address mismatch. %s != %s" % (pi.ticketAddress, scriptAddr.string())) + # extract addresses + scriptType, addrs, numSigs = txscript.extractPkScriptAddrs(0, redeemScript, self.net) + if numSigs != 1: + raise Exception("expected 2 required signatures, found 2") + found = False + signAddr = txscript.decodeAddress(addr, self.net) + for addr in addrs: + if addr.string() == signAddr.string(): + found = True + break + if not found: + raise Exception("signing pubkey not found in redeem script") + def authorize(self, address, net): + """ + Authorize the stake pool for the provided address and network. Exception + is raised on failure to authorize. + + Args: + address (string): The base58-encoded pubkey address that the wallet + uses to vote. + net (object): The network parameters. + """ + # An error is returned if the address is already set + # {'status': 'error', 'code': 6, 'message': 'address error - address already submitted'} + # First try to get the purchase info directly. + self.net = net + try: + self.getPurchaseInfo() + self.validate(address) + except Exception as e: + if "code" not in self.err or self.err["code"] != 9: + # code 9 is address not set + raise e + # address is not set + data = { "UserPubKeyAddr": address } + res = tinyhttp.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True) + if resultIsSuccess(res): + self.getPurchaseInfo() + self.validate(address) + else: + raise Exception("unexpected response from 'address': %s" % repr(res)) + def getPurchaseInfo(self): + """ + Get the purchase info from the stake pool API. + + Returns: + PurchaseInfo: The PurchaseInfo object. + """ + # An error is returned if the address isn't yet set + # {'status': 'error', 'code': 9, 'message': 'purchaseinfo error - no address submitted', 'data': None} + res = tinyhttp.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) + if resultIsSuccess(res): + pi = PurchaseInfo(res["data"]) + # check the script hash + self.purchaseInfo = pi + return self.purchaseInfo + self.err = res + raise Exception("unexpected response from 'getpurchaseinfo': %r" % (res, )) + def getStats(self): + """ + Get the stats from the stake pool API. + + Returns: + Poolstats: The PoolStats object. + """ + res = tinyhttp.get(self.apiPath("stats"), headers=self.headers()) + if resultIsSuccess(res): + self.stats = PoolStats(res["data"]) + return self.stats + raise Exception("unexpected response from 'stats': %s" % repr(res)) + def setVoteBits(self, voteBits): + """ + Set the vote preference on the VotingServiceProvider. + + Returns: + bool: True on success. Exception raised on error. + """ + data = { "VoteBits": voteBits } + res = tinyhttp.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) + if resultIsSuccess(res): + return True + raise Exception("unexpected response from 'voting': %s" % repr(res)) + +tinyjson.register(VotingServiceProvider, "VotingServiceProvider") + diff --git a/ui/screens.py b/ui/screens.py index 552a3409..f02957f5 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -11,7 +11,7 @@ from tinydecred.wallet.wallet import Wallet from tinydecred.util import helpers from tinydecred.pydecred import constants as DCR -from tinydecred.pydecred.stakepool import StakePool +from tinydecred.pydecred.vsp import VotingServiceProvider UI_DIR = os.path.dirname(os.path.realpath(__file__)) log = helpers.getLogger("APPUI") # , logLvl=0) @@ -1300,7 +1300,7 @@ def getPools(self): net = self.app.dcrdata.params def get(): try: - return StakePool.providers(net) + return VotingServiceProvider.providers(net) except Exception as e: log.error("error retrieving stake pools: %s" % e) return False @@ -1368,7 +1368,7 @@ def authPool(self): if not apiKey: err("empty API key") return - pool = StakePool(url, apiKey) + pool = VotingServiceProvider(url, apiKey) def registerPool(wallet): try: addr = wallet.openAccount.votingAddress() @@ -1502,7 +1502,7 @@ def setWidgets(self, pools): Set the displayed pool widgets. Args: - pools list(StakePool): pools to display + pools list(VotingServiceProvider): pools to display """ Q.clearLayout(self.poolsLyt, delete=True) for pool in pools: @@ -1522,7 +1522,7 @@ def selectActivePool(self, pool): Set the current active pool. Args: - pool (StakePool): The new active pool. + pool (VotingServiceProvider): The new active pool. """ self.app.appWindow.showSuccess("new pool selected") self.app.wallet.selectedAccount.setPool(pool) diff --git a/util/mpl.py b/util/mpl.py index 307f0585..85838623 100644 --- a/util/mpl.py +++ b/util/mpl.py @@ -11,7 +11,7 @@ from mpl_toolkits.mplot3d import Axes3D # leave this even if the linter complains from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from tinydecred.util import helpers -import tinydecred.ui as UI +import tinydecred.ui.ui as UI from tinydecred.pydecred import constants as C @@ -56,7 +56,7 @@ def getFont(font, size): MPL_FONTS[font] = {} if size not in MPL_FONTS[font]: MPL_FONTS[font][size] = FontManager.FontProperties( - fname=os.path.join(UI.PACKAGEDIR, "fonts", "%s.ttf" % font), + fname=os.path.join(UI.PACKAGEDIR, "fonts", "%s.ttf" % font), size=size ) return MPL_FONTS[font][size] @@ -115,7 +115,7 @@ def setAxesFont(font, size, *axes): class TexWidget(FigureCanvas): """A Qt5 compatible widget with a Tex equation""" def __init__(self, equation, fontSize=20): - self.equation = equation + self.equation = equation self.fig = Figure() self.fig.subplots_adjust(**NO_SUBPLOT_MARGINS) super().__init__(self.fig) From 295c11e63ac531b34d8c5948958a30b09e018b48 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 19 Nov 2019 04:24:32 -0600 Subject: [PATCH 10/12] fix sync callback balance = 0 case --- ui/screens.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ui/screens.py b/ui/screens.py index f02957f5..605f4161 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -381,7 +381,7 @@ def __init__(self, app): # Update the home screen when the balance signal is received. app.registerSignal(ui.BALANCE_SIGNAL, self.balanceUpdated) - app.registerSignal(ui.SYNC_SIGNAL, self.setTicketStats) + app.registerSignal(ui.SYNC_SIGNAL, self.walletSynced) layout = self.layout layout.setAlignment(Q.ALIGN_LEFT) @@ -515,18 +515,24 @@ def balanceUpdated(self, bal): self.balance = bal if self.ticketStats: self.setTicketStats() + def walletSynced(self): + """ + Connected to the ui.SYNC_SIGNAL. Remove loading spinner and set ticket + stats. + """ + acct = self.app.wallet.selectedAccount + self.ticketStats = acct.ticketStats() + self.setTicketStats() + self.spinner.setVisible(False) + self.stakeBttn.setVisible(True) def setTicketStats(self): """ Set the staking statistics. """ - acct = self.app.wallet.selectedAccount - balance = self.balance - stats = acct.ticketStats() - if stats and balance and balance.total > 0: - self.spinner.setVisible(False) - self.stakeBttn.setVisible(True) - self.statsLbl.setText("%s%% staked" % helpers.formatNumber(stats.value/balance.total*100)) - self.ticketStats = stats + staked = 0 + if self.ticketStats and self.balance.total > 0: + staked = self.ticketStats.value/self.balance.total + self.statsLbl.setText("%s%% staked" % helpers.formatNumber(staked*100)) def spendClicked(self, e=None): """ Display a form to send funds to an address. A Qt Slot, but any event From 7e7a203f7223257b9134e458b479e8656863ac2e Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 22 Nov 2019 07:33:08 -0600 Subject: [PATCH 11/12] propagate broadcast exception. allow crappy pools on testnet --- pydecred/account.py | 6 ------ pydecred/dcrdata.py | 2 +- pydecred/vsp.py | 3 ++- ui/screens.py | 10 ++++++++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pydecred/account.py b/pydecred/account.py index 6215898c..2363f358 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -360,12 +360,6 @@ def purchaseTickets(self, qty, price): self.addMempoolTx(tx) # Store the txids. self.tickets.extend([tx.txid() for tx in txs[1]]) - # Remove spent utxos from cache. - self.spendUTXOs(spentUTXOs) - # Add new UTXOs to set. These may be replaced with network-sourced - # UTXOs once the wallet receives an update from the BlockChain. - for utxo in newUTXOs: - self.addUTXO(utxo) return txs[1] def sync(self, blockchain, signals): """ diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 45d9da28..10bdcbe4 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -927,7 +927,7 @@ def broadcast(self, txHex): return True except Exception as e: log.error("broadcast error: %s" % e) - return False + raise e def pubsubSignal(self, sig): """ Process a notifictation from the block explorer. diff --git a/pydecred/vsp.py b/pydecred/vsp.py index b58b7fab..15ecc76b 100644 --- a/pydecred/vsp.py +++ b/pydecred/vsp.py @@ -236,7 +236,8 @@ def authorize(self, address, net): self.getPurchaseInfo() self.validate(address) except Exception as e: - if "code" not in self.err or self.err["code"] != 9: + alreadyRegistered = isinstance(self.err, dict) and "code" in self.err and self.err["code"] == 9 + if not alreadyRegistered: # code 9 is address not set raise e # address is not set diff --git a/ui/screens.py b/ui/screens.py index 605f4161..9bf6c694 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -1323,8 +1323,12 @@ def setPools(self, pools): return self.pools = pools tNow = int(time.time()) - # only save pools updated within the last day - self.pools = [p for p in pools if tNow - p["LastUpdated"] < 86400 and self.scorePool(p) > 95] + # Only save pools updated within the last day, but allow bad pools for + # testing. + # TODO: Have 3 tinydecred network constants retreivable through cfg + # instead of checking the network config's Name attribute. + if cfg.net.Name == "mainnet": + self.pools = [p for p in pools if tNow - p["LastUpdated"] < 86400 and self.scorePool(p) > 95] self.randomizePool() def randomizePool(self, e=None): @@ -1354,6 +1358,8 @@ def scorePool(self, pool): """ Get the pools performance score, as a float percentage. """ + if pool["Voted"] == 0: + return 0 return pool["Voted"]/(pool["Voted"]+pool["Missed"])*100 def authPool(self): From de0a97a742877245b15c9154a1c9ed2c891d88ce Mon Sep 17 00:00:00 2001 From: Brian Date: Sat, 23 Nov 2019 06:06:58 -0600 Subject: [PATCH 12/12] calc stake stats internally. more sync improvements --- pydecred/account.py | 17 ++++++++++++++++- pydecred/dcrdata.py | 2 +- ui/screens.py | 22 ++++++---------------- wallet/accounts.py | 24 ++++++++++++++---------- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/pydecred/account.py b/pydecred/account.py index 2363f358..0964771b 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -115,6 +115,7 @@ def __fromjson__(obj): acct = Account.__fromjson__(obj, cls=DecredAccount) acct.tickets = obj["tickets"] acct.stakePools = obj["stakePools"] + acct.updateStakeStats() return acct def open(self, pw): """ @@ -139,6 +140,20 @@ def close(self): super().close() self._votingKey.key.zero() self._votingKey = None + def calcBalance(self, tipHeight): + tot = 0 + avail = 0 + staked = 0 + for utxo in self.utxoscan(): + tot += utxo.satoshis + if utxo.isTicket(): + staked += utxo.satoshis + if utxo.isSpendable(tipHeight): + avail += utxo.satoshis + self.balance.total = tot + self.balance.available = avail + self.balance.staked = staked + return self.balance def updateStakeStats(self): """ Updates the stake stats object. @@ -374,7 +389,7 @@ def sync(self, blockchain, signals): # First, look at addresses that have been generated but not seen. Run in # loop until the gap limit is reached. requestedTxs = 0 - addrs = self.unseenAddrs() + addrs = self.gapAddrs() while addrs: for addr in addrs: for txid in blockchain.txsForAddr(addr): diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 10bdcbe4..36f5bf94 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -443,9 +443,9 @@ def parse(obj): amount = obj["amount"] if "amount" in obj else 0, satoshis = obj["satoshis"] if "satoshis" in obj else 0, maturity = obj["maturity"] if "maturity" in obj else None, - scriptClass = obj["scriptClass"] if "scriptClass" in obj else None, tinfo = obj["tinfo"] if "tinfo" in obj else None, ) + utxo.parseScriptClass() return utxo def parseScriptClass(self): """ diff --git a/ui/screens.py b/ui/screens.py index 9bf6c694..0461f7b5 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -461,8 +461,7 @@ def __init__(self, app): optsLyt.addWidget(self.spinner, 0, 1, Q.ALIGN_RIGHT) # Open staking window. Button is initally hidden until sync is complete. - self.stakeBttn = btn = app.getButton(SMALL, "Staking") - btn.setVisible(False) + btn = app.getButton(SMALL, "Staking") btn.setMinimumWidth(110) btn.clicked.connect(self.openStaking) optsLyt.addWidget(btn, 0, 1, Q.ALIGN_RIGHT) @@ -512,9 +511,9 @@ def balanceUpdated(self, bal): self.totalBalance.setText("{0:,.2f}".format(dcr)) self.totalBalance.setToolTip("%.8f" % dcr) self.availBalance.setText("%s spendable" % availStr.rstrip('0').rstrip('.')) + staked = bal.staked/bal.total if bal.total > 0 else 0 + self.statsLbl.setText("%s%% staked" % helpers.formatNumber(staked*100)) self.balance = bal - if self.ticketStats: - self.setTicketStats() def walletSynced(self): """ Connected to the ui.SYNC_SIGNAL. Remove loading spinner and set ticket @@ -522,17 +521,7 @@ def walletSynced(self): """ acct = self.app.wallet.selectedAccount self.ticketStats = acct.ticketStats() - self.setTicketStats() self.spinner.setVisible(False) - self.stakeBttn.setVisible(True) - def setTicketStats(self): - """ - Set the staking statistics. - """ - staked = 0 - if self.ticketStats and self.balance.total > 0: - staked = self.ticketStats.value/self.balance.total - self.statsLbl.setText("%s%% staked" % helpers.formatNumber(staked*100)) def spendClicked(self, e=None): """ Display a form to send funds to an address. A Qt Slot, but any event @@ -945,10 +934,11 @@ def __init__(self, app): self.edit = edit = QtWidgets.QTextEdit() edit.setAcceptRichText(False) edit.setMaximumWidth(300) - edit.setFixedHeight(225) + edit.setFixedHeight(200) edit.setStyleSheet("QLabel{border: 1px solid #777777; padding: 10px;}") # edit.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.TextSelectableByKeyboard) row, lyt = Q.makeWidget(QtWidgets.QWidget, "horizontal") + row.setContentsMargins(2, 2, 2, 2) self.layout.addWidget(row) lyt.addStretch(1) lyt.addWidget(edit) @@ -1039,7 +1029,6 @@ def __init__(self, app): # Register for a few key signals. self.app.registerSignal(ui.BLOCKCHAIN_CONNECTED, self.blockchainConnected) self.app.registerSignal(ui.BALANCE_SIGNAL, self.balanceSet) - self.app.registerSignal(ui.SYNC_SIGNAL, self.setStats) # ticket price is a single row reading `Ticket Price: XX.YY DCR`. lbl = Q.makeLabel("Ticket Price: ", 16) @@ -1151,6 +1140,7 @@ def balanceSet(self, balance): """ self.balance = balance self.setBuyStats() + self.setStats() def setBuyStats(self): """ diff --git a/wallet/accounts.py b/wallet/accounts.py index 7775e40b..bfb969fc 100644 --- a/wallet/accounts.py +++ b/wallet/accounts.py @@ -164,24 +164,27 @@ class Balance(object): for this wallet. The `available` sum is the same, but without those which appear to be from immature coinbase or stakebase transactions. """ - def __init__(self, total=0, available=0): + def __init__(self, total=0, available=0, staked=0): self.total = total self.available = available + self.staked = staked def __tojson__(self): return { "total": self.total, "available": self.available, + "staked": self.staked, } @staticmethod def __fromjson__(obj): return Balance( total = obj["total"], - available = obj["available"] + available = obj["available"], + staked = obj["staked"] if "staked" in obj else 0 ) def __repr__(self): return ( - "Balance(total=%.8f, available=%.8f)" % - (self.total*1e-8, self.available*1e-8) + "Balance(total=%.8f, available=%.8f, staked=%.8f)" % + (self.total*1e-8, self.available*1e-8, self.staked*1e-8) ) tinyjson.register(Balance, "Balance") @@ -272,7 +275,7 @@ def __fromjson__(obj, cls=None): acct.balance = obj["balance"] acct.gapLimit = obj["gapLimit"] acct.lastSeenExt = acct.lastSeen(acct.externalAddresses) - acct.lastSeenInt = acct.lastSeen(acct.internalAddresses) + acct.lastSeenInt = acct.lastSeen(acct.internalAddresses, default=-1) setNetwork(acct) return acct def addrTxs(self, addr): @@ -574,7 +577,7 @@ def nextInternalAddress(self): self.nextBranchAddress(self.intPub, intAddrs) addr = intAddrs[idx] return addr - def lastSeen(self, addrs): + def lastSeen(self, addrs, default=0): """ Find the index of the last seen address in the list of addresses. The last seen address is taken as the last address for which there is an @@ -586,7 +589,7 @@ def lastSeen(self, addrs): Returns: int: The highest index of all seen addresses in the list. """ - lastSeen = -1 + lastSeen = default for i, addr in enumerate(addrs): if addr in self.txs: lastSeen = i @@ -632,10 +635,11 @@ def watchAddrs(self): a = a.union(self.externalAddresses) a = a.union((a for a in self.internalAddresses if a not in self.txs)) return filterCrazyAddress(a) - def unseenAddrs(self): + def gapAddrs(self): return filterCrazyAddress( - [a for a in self.internalAddresses if a not in self.txs] + - [a for a in self.externalAddresses if a not in self.txs]) + self.internalAddresses[self.lastSeenInt:] + + self.externalAddresses[self.lastSeenExt:] + ) def currentAddress(self): """ Get the external address at the cursor. The cursor is not moved.