def readVarInt(b, pver): """ readVarInt reads a variable length integer from b and returns it as an int. Args: b ByteArray: the encoded integer. pver int: the protocol version (unused). """ data = { 0xFF: dict( pop_bytes=8, minRv=0x100000000, ), 0xFE: dict( pop_bytes=4, minRv=0x10000, ), 0xFD: dict( pop_bytes=2, minRv=0xFD, ), } discriminant = b.pop(1).int() if discriminant not in data.keys(): return discriminant rv = b.pop(data[discriminant]["pop_bytes"]).unLittle().int() # The encoding is not canonical if the value could have been # encoded using fewer bytes. minRv = data[discriminant]["minRv"] if rv < minRv: raise DecredError("ReadVarInt noncanon error: {} - {} <= {}".format( rv, discriminant, minRv)) return rv
def request(url, postData=None, headers=None, urlEncode=False, context=None): # GET method used when encoded data is None. encoded = None if postData: if urlEncode: # Encode the data in URL query string form. encoded = urlencode(postData).encode("utf-8") else: # Encode the data as JSON. encoded = json.dumps(postData).encode("utf-8") headers = headers if headers else {} req = urlrequest.Request(url, headers=headers, data=encoded) try: raw = urlrequest.urlopen(req, context=context).read().decode() except Exception as err: raise DecredError( f"Error in requesting URL {url}: {formatTraceback(err)}") try: # Try to decode the response as JSON, but fall back to just # returning the string. return json.loads(raw) except json.JSONDecodeError: return raw
def btcEncode(self, pver): """ btcEncode encodes the receiver using the Decred protocol encoding. This is part of the Message API. Args: pver (int): The protocol version. Returns: ByteArray: The encoded MsgVersion. """ if not validateUserAgent(self.userAgent): raise DecredError(f"bad user agent {self.userAgent}") b = ByteArray() b += ByteArray(self.protocolVersion, length=4).littleEndian() b += ByteArray(self.services, length=8).littleEndian() b += ByteArray(self.timestamp, length=8).littleEndian() b += netaddress.writeNetAddress(self.addrYou, False) b += netaddress.writeNetAddress(self.addrMe, False) b += ByteArray(self.nonce, length=8).littleEndian() b += wire.writeVarString(pver, self.userAgent) b += ByteArray(self.lastBlock, length=4).littleEndian() b += [0] if self.disableRelayTx else [1] return b
def readNetAddress(b, hasStamp): """ Reads an encoded NetAddress from b depending on the protocol version and whether or not the timestamp is included per hasStamp. Some messages like version do not include the timestamp. Args: b (ByteArray): The encoded NetAddress. hasStamp (bool): Whether or not the NetAddress has a timestamp. Returns: NetAddress: The decoded NetAddress. """ expLen = 30 if hasStamp else 26 if len(b) != expLen: raise DecredError( f"readNetAddress wrong length (hasStamp={hasStamp}) expected {expLen}, got {len(b)}" ) # NOTE: The Decred protocol uses a uint32 for the timestamp so it will # stop working somewhere around 2106. Also timestamp wasn't added until # protocol version >= NetAddressTimeVersion stamp = b.pop(4).unLittle().int() if hasStamp else 0 services = b.pop(8).unLittle().int() ip = b.pop(16) if ip[:12] == ipv4to16prefix: ip = ip[12:] # Sigh. Decred protocol mixes little and big endian. port = b.pop(2).int() return NetAddress(ip=ip, port=port, services=services, stamp=stamp,)
def authorize(self, address): """ Authorize the stake pool for the provided address and network. DecredError is raised on failure to authorize. Args: address (string): The base58-encoded pubkey address that the wallet uses to vote. """ try: self.getPurchaseInfo() self.validate(address) except DecredError as e: # code 9 is address not set addressNotSet = (isinstance(self.err, dict) and "code" in self.err and self.err["code"] == 9) if not addressNotSet: 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 DecredError("unexpected response from 'address': %s" % repr(res))
def __init__(self, pkHash=None, netParams=None, sigType=STEcdsaSecp256k1): """ Args: pkHash (ByteArray): The hashed pubkey. netParams (module): The network parameters. sigType (int): The signature type. """ if sigType == STEd25519: # nocover raise NotImplementedError("Edwards not implemented") elif sigType == STSchnorrSecp256k1: # nocover raise NotImplementedError("Schnorr not implemented") elif sigType != STEcdsaSecp256k1: raise NotImplementedError(f"unsupported signature type {sigType}") super().__init__(netParams) pkh_len = len(pkHash) if pkh_len != RIPEMD160_SIZE: raise DecredError( f"AddressPubKeyHash expected {RIPEMD160_SIZE} bytes, got {pkh_len}" ) self.sigType = sigType self.netID = netParams.PubKeyHashAddrID self.pkHash = pkHash
def tx(self, txid): """ Get the MsgTx. Retreive it from the blockchain if necessary. Args: txid (str): A hex encoded transaction ID to fetch. Returns: MsgTx: The transaction. """ hashKey = hashFromHex(txid).bytes() try: return self.txDB[hashKey] except database.NoValueError: try: # Grab the hex encoded transaction txHex = self.dcrdata.tx.hex(txid) msgTx = msgtx.MsgTx.deserialize(ByteArray(txHex)) self.txDB[hashKey] = msgTx return msgTx except Exception as e: log.warning( "unable to retrieve tx data from dcrdata at %s: %s" % (self.dcrdata.baseURL, e)) raise DecredError("failed to retrieve transaction")
def __init__(self, scriptHash, netParams): if len(scriptHash) != RIPEMD160_SIZE: raise DecredError(f"incorrect script hash length {len(scriptHash)}") super().__init__(netParams) self.netID = netParams.ScriptHashAddrID self.scriptHash = scriptHash
def readNBits(self, n): """ Read n number of LSB bits of data from the bit stream in big endian format. Args: n (int): The number of bits to read. Returns: int: The bits interpreted as a big-endian integer. """ if n > 64 or n < 0: raise DecredError(f"n > 64 or < 0 not allowed. given {n}") if n == 0: return 0 if len(self.b) == 0: raise EncodingError("end of bytes") value = 0 # If byte is partially read, read the rest. if self.next != 1 << 7: while n > 0: if self.next == 0: self.next = 1 << 7 self.b = self.b[1:] break n -= 1 if self.b[0] & self.next != 0: value |= 1 << n self.next >>= 1 if n == 0: return value # Read 8 bits at a time. while n >= 8: if len(self.b) == 0: raise EncodingError("end of bytes") n -= 8 value |= self.b[0] << n self.b = self.b[1:] if len(self.b) == 0: if n != 0: raise EncodingError("bytes exhausted") return value # Read the remaining bits. while n > 0: n -= 1 if self.b[0] & self.next != 0: value |= 1 << n self.next >>= 1 return value
def decodePrefix(self, b, pver): """ decodePrefix decodes a transaction prefix and stores the contents in the embedded msgTx. """ count = wire.readVarInt(b, pver) # Prevent more input transactions than could possibly fit into a # message. It would be possible to cause memory exhaustion and panics # without a sane upper bound on this count. if count > maxTxInPerMessage: raise DecredError( "MsgTx.decodePrefix: too many input transactions to fit into" " max message size [count %d, max %d]" % (count, maxTxInPerMessage)) # TxIns. txIns = self.txIn = [TxIn(None, 0) for i in range(count)] for txIn in txIns: readTxInPrefix(b, pver, self.serType, self.version, txIn) count = wire.readVarInt(b, pver) # Prevent more output transactions than could possibly fit into a # message. It would be possible to cause memory exhaustion and panics # without a sane upper bound on this count. if count > maxTxOutPerMessage: raise DecredError( "MsgTx.decodePrefix: too many output transactions to fit into" " max message size [count %d, max %d]" % (count, maxTxOutPerMessage)) # TxOuts. totalScriptSize = 0 txOuts = self.txOut = [TxOut(None, None) for i in range(count)] for txOut in txOuts: # The pointer is set now in case a script buffer is borrowed # and needs to be returned to the pool on error. b = readTxOut(b, pver, self.version, txOut) totalScriptSize += len(txOut.pkScript) # Locktime and expiry. self.lockTime = b.pop(4).unLittle().int() self.expiry = b.pop(4).unLittle().int() return b, totalScriptSize
def serializeCompressed(self): fmt = PUBKEY_COMPRESSED if not isEven(self.y): fmt |= 0x1 b = ByteArray(fmt) b += ByteArray(self.x, length=COORDINATE_LEN) if len(b) != PUBKEY_COMPRESSED_LEN: raise DecredError("invalid compressed pubkey length %d", len(b)) return b
def parseCoinType(coinType): """ Parse the coin type. If coinType is a string, it will be converted to the BIP0044 ID. If it is already an integer, it is returned as is. Args: coinType (int or str): The asset. BIP0044 ID or ticker symbol. Returns: int: The BIP0044 ID. """ if isinstance(coinType, str): ticker = coinType.lower() if ticker not in SymbolIDs: raise DecredError(f"ticker symbol {ticker} not found") coinType = SymbolIDs[ticker] if not isinstance(coinType, int): raise DecredError(f"unsupported type for coinType {type(coinType)}") return coinType
def parse(name): """ Get the network parameters based on the network name. """ # Set testnet to DCR for now. If more coins are added, a better solution # will be needed. try: return the_nets[name] except KeyError: raise DecredError(f"unrecognized network name {name}")
def decodeWitness(self, b, pver, isFull): """ Witness only; generate the TxIn list and fill out only the sigScripts. Args: b ByteArray: the encoded witnesses. pver int: the protocol version. isFull book: whether this is a full transaction. """ totalScriptSize = 0 count = wire.readVarInt(b, pver) # Prevent more input transactions than could possibly fit into a # message, or memory exhaustion and panics could happen. if count > maxTxInPerMessage: raise DecredError( "MsgTx.decodeWitness: too many input transactions to fit into" f" max message size [count {count}, max {maxTxInPerMessage}]") if isFull: # We're decoding witnesses from a full transaction, so make sure # the number of signature scripts is the same as the number of # TxIns we currently have, then fill in the signature scripts. if count != len(self.txIn): raise DecredError( "MsgTx.decodeWitness: non equal witness and prefix txin" f" quantities (witness {count}, prefix {len(self.txIn)})") # Read in the witnesses, and copy them into the already generated # by decodePrefix TxIns. if self.txIn is None or len(self.txIn) == 0: self.txIn = [TxIn(None, 0) for i in range(count)] for txIn in self.txIn: b = readTxInWitness(b, pver, self.version, txIn) totalScriptSize += len(txIn.signatureScript) else: self.txIn = [TxIn(None, 0) for i in range(count)] for txIn in self.txIn: b = readTxInWitness(b, pver, self.version, txIn) totalScriptSize += len(txIn.signatureScript) self.txOut = [] return b, totalScriptSize
def updateTip(self): """ Update the tip block. If the wallet is subscribed to block updates, this can be used sparingly. """ try: self.tipHeight = self.bestBlock()["height"] except Exception as e: log.error("failed to retrieve tip from blockchain: %s" % formatTraceback(e)) raise DecredError("no tip data retrieved")
def readTxInPrefix(b, pver, serType, ver, ti): if serType == wire.TxSerializeOnlyWitness: raise DecredError( "readTxInPrefix: tried to read a prefix input for a witness only tx" ) # Outpoint. b, ti.previousOutPoint = readOutPoint(b, pver, ver) # Sequence. ti.sequence = b.pop(4).unLittle().int()
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 DecredError("unexpected response from 'stats': %s" % repr(res))
def checkOutput(output, fee): """ checkOutput performs simple consensus and policy tests on a transaction output. Args: output (TxOut): The output to check fee (float): The transaction fee rate (/kB). Raises: DecredError if an output is deemed invalid. """ if output.value < 0: raise DecredError("transaction output amount is negative") if output.value > txscript.MaxAmount: raise DecredError("transaction output amount exceeds maximum value") if output.value == 0: raise DecredError("zero-value output") # need to implement these if txscript.isDustOutput(output, fee): raise DecredError("policy violation: transaction output is dust")
def decode(words): """ DecodeMnemonics returns the decoded value that is encoded by words. Any words that are whitespace are empty are skipped. """ _, byteMap = pgWords() decoded = [0] * len(words) idx = 0 for word in words: word = word.strip().lower() if word == "": continue if word not in byteMap: raise DecredError("unknown words in mnemonic key: %s" % word) b = byteMap[word] if int(b % 2) != idx % 2: raise DecredError( f"word {word} is not valid at position {idx}, check for missing words" ) decoded[idx] = b // 2 idx += 1 return ByteArray(decoded[:idx])
def checkSeedLength(length): """ Check that seed length is correct. Args: length int: the seed length to be checked. Raises: DecredError if length is not between MinSeedBytes and MaxSeedBytes included. """ if length < MinSeedBytes or length > MaxSeedBytes: raise DecredError(f"Invalid seed length {length}")
def create(walletDir, pw, network, signals=None): """ Create a new wallet. Will not overwrite an existing wallet file. All arguments are the same as the SimpleWallet constructor. """ netParams = nets.parse(network) _, dbPath, _ = paths(walletDir, netParams.Name) if Path(dbPath).is_file(): raise DecredError("wallet already exists at %s" % dbPath) wallet = SimpleWallet(walletDir, pw, network, signals, True) words = wallet.words wallet.words.clear() return wallet, words
def b58CheckDecode(s): """ Decode the base-58 encoded address, parsing the version bytes and the pubkey hash. An exception is raised if the checksum is invalid or missing. Args: s (str): The base-58 encoded address. Returns: ByteArray: Decoded bytes minus the leading version and trailing checksum. int: The version (leading two) bytes. """ decoded = b58decode(s) if len(decoded) < 6: raise DecredError("decoded lacking version/checksum") version = decoded[:2] included_cksum = decoded[len(decoded) - 4:] computed_cksum = checksum(decoded[:len(decoded) - 4]) if included_cksum != computed_cksum: raise DecredError("checksum error") payload = ByteArray(decoded[2:len(decoded) - 4]) return payload, version
def decodeBlob(b): """ decodeBlob decodes a versioned blob into its version and the pushes extracted from its data. Args: b (bytes-like): The bytes to decode. Returns: int: The blob version (the version passed to BuildyBytes). list(bytes-like): The data pushes. """ if len(b) == 0: raise DecredError("zero length blob not allowed") return b[0], extractPushes(b[1:])
def parsePubKey(self, pubKeyB): """ parsePubKey parses a secp256k1 public key encoded according to the format specified by ANSI X9.62-1998, which means it is also compatible with the SEC (Standards for Efficient Cryptography) specification which is a subset of the former. In other words, it supports the uncompressed and compressed formats as follows: Compressed: <format byte = 0x02/0x03><32-byte X coordinate> Uncompressed: <format byte = 0x04><32-byte X coordinate><32-byte Y coordinate> It does not support the hybrid format, however. """ if len(pubKeyB) == 0: raise DecredError("empty pubkey") fmt = pubKeyB[0] ybit = (fmt & 0x1) == 0x1 fmt &= 0xFF ^ 0x01 ifunc = lambda b: int.from_bytes(b, byteorder="big") pkLen = len(pubKeyB) if pkLen == PUBKEY_LEN: if PUBKEY_UNCOMPRESSED != fmt: raise DecredError("invalid magic in pubkey: %d" % pubKeyB[0]) x = ifunc(pubKeyB[1:33]) y = ifunc(pubKeyB[33:]) elif pkLen == PUBKEY_COMPRESSED_LEN: # format is 0x2 | solution, <X coordinate> # solution determines which solution of the curve we use. # / y^2 = x^3 + Curve.B if PUBKEY_COMPRESSED != fmt: raise DecredError("invalid magic in compressed pubkey: %d" % pubKeyB[0]) x = ifunc(pubKeyB[1:33]) y = self.decompressPoint(x, ybit) else: # wrong! raise DecredError("invalid pub key length %d" % len(pubKeyB)) if x > self.P: raise DecredError("pubkey X parameter is >= to P") if y > self.P: raise DecredError("pubkey Y parameter is >= to P") if not self.isAffineOnCurve(x, y): raise DecredError("pubkey [%d, %d] isn't on secp256k1 curve" % (x, y)) return PublicKey(self, x, y)
def child(self, name, **k): """ Create a child Bucket, which is really just a Bucket with a name which derives from this Bucket's name. Args: name (str): The child Bucket name. **k: Keyword arguments are passed directly to the Bucket constructor. """ if "$" in name: raise DecredError( "illegal character. '$' not allowed in table name") compoundName = "{parent}${child}".format(parent=self.name, child=name) return Bucket(self.conn, compoundName, **k)
def setVoteBits(self, voteBits): """ Set the vote preference on the VotingServiceProvider. Returns: bool: True on success. DecredError raised on error. """ data = {"VoteBits": voteBits} res = tinyhttp.post(self.apiPath("voting"), data, headers=self.headers(), urlEncode=True) if resultIsSuccess(res): self.purchaseInfo.voteBits = voteBits return True raise DecredError("unexpected response from 'voting': %s" % repr(res))
def modInv(a, m): """ Modular inverse based on https://stackoverflow.com/a/9758173/1124661. Raises an exception if impossible. Args: a (int): An integer. m (int): The modulus. Returns: int: The modular inverse. """ g, x, y = egcd(a, m) if g != 1: raise DecredError("modular inverse does not exist") return x % m
def child(self, name, **k): """ Creates the root Bucket. Args: name (str): The root node name. **k: Keyword arguments are passed directly to the Bucket constructor. Returns: Bucket: The root bucket. """ if "$" in name: raise DecredError( "illegal character. '$' not allowed in table name") return Bucket(self.conn, name, **k)
def decodeStringIP(ip): """ Parse an IP string to bytes. Args: ip (str): The string-encoded IP address. Returns: bytes-like: The byte-encoded IP address. """ try: return socket.inet_pton(socket.AF_INET, ip) except OSError: pass try: return socket.inet_pton(socket.AF_INET6, ip) except OSError: raise DecredError(f"failed to decode IP {ip}")
def getPurchaseInfo(self): """ Get the purchase info from the stake pool API. Returns: PurchaseInfo: The PurchaseInfo object. """ self.err = None res = tinyhttp.get(self.apiPath("getpurchaseinfo"), headers=self.headers()) if resultIsSuccess(res): pi = PurchaseInfo.parse(res["data"]) # check the script hash self.purchaseInfo = pi return self.purchaseInfo self.err = res raise DecredError("unexpected response from 'getpurchaseinfo': %r" % (res, ))