From e6479b434f1d78949fb150253d5e3a909e8c5911 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Fri, 11 Jun 2021 19:31:54 -0500 Subject: [PATCH] implement btc spv wallet --- decred/decred/btc/__init__.py | 0 decred/decred/btc/addrlib.py | 828 +++++++++ decred/decred/btc/btcwallet/__init__.py | 0 decred/decred/btc/btcwallet/btcwallet.py | 851 +++++++++ decred/decred/btc/btcwallet/golink.py | 80 + .../btc/btcwallet/libbtcwallet/build-nix.sh | 1 + .../btc/btcwallet/libbtcwallet/build-win.sh | 1 + .../decred/btc/btcwallet/libbtcwallet/go.mod | 15 + .../decred/btc/btcwallet/libbtcwallet/go.sum | 371 ++++ .../btc/btcwallet/libbtcwallet/golink.go | 196 +++ .../btc/btcwallet/libbtcwallet/interface.go | 89 + .../btcwallet/libbtcwallet/libbtcwallet.go | 1556 +++++++++++++++++ .../decred/btc/btcwallet/libbtcwallet/log.go | 48 + .../btc/btcwallet/libbtcwallet/test-data.json | 245 +++ .../btc/btcwallet/libbtcwallet/testwallet.go | 1206 +++++++++++++ .../btc/btcwallet/libbtcwallet/types.go | 109 ++ .../btc/btcwallet/libbtcwallet/wallet.go | 268 +++ decred/decred/btc/wire/msgtx.py | 742 ++++++++ decred/decred/btc/wire/wire.py | 79 + decred/decred/crypto/crypto.py | 69 +- decred/decred/crypto/gcs.py | 3 + decred/decred/crypto/secp256k1/curve.py | 42 +- decred/decred/dcr/addrlib.py | 25 +- decred/decred/dcr/wire/msgtx.py | 28 +- decred/pyproject.toml | 1 + .../integration/btc/test_libbtcwallet.py | 420 +++++ decred/tests/unit/btc/test_addrlib.py | 627 +++++++ decred/tests/unit/btc/wire/test_msgtx.py | 569 ++++++ 28 files changed, 8414 insertions(+), 55 deletions(-) create mode 100644 decred/decred/btc/__init__.py create mode 100644 decred/decred/btc/addrlib.py create mode 100644 decred/decred/btc/btcwallet/__init__.py create mode 100644 decred/decred/btc/btcwallet/btcwallet.py create mode 100644 decred/decred/btc/btcwallet/golink.py create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/build-nix.sh create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/build-win.sh create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/go.mod create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/go.sum create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/golink.go create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/interface.go create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/libbtcwallet.go create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/log.go create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/test-data.json create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/testwallet.go create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/types.go create mode 100644 decred/decred/btc/btcwallet/libbtcwallet/wallet.go create mode 100644 decred/decred/btc/wire/msgtx.py create mode 100644 decred/decred/btc/wire/wire.py create mode 100644 decred/tests/integration/btc/test_libbtcwallet.py create mode 100644 decred/tests/unit/btc/test_addrlib.py create mode 100644 decred/tests/unit/btc/wire/test_msgtx.py diff --git a/decred/decred/btc/__init__.py b/decred/decred/btc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/decred/decred/btc/addrlib.py b/decred/decred/btc/addrlib.py new file mode 100644 index 00000000..46ec874e --- /dev/null +++ b/decred/decred/btc/addrlib.py @@ -0,0 +1,828 @@ +""" +Copyright (c) 2019, Brian Stafford +Copyright (c) 2019-2020, The Decred developers +See LICENSE for details + +Cryptographic functions. +""" + +from typing import ByteString, Union, Optional, Tuple +import hashlib + +from base58 import b58encode, b58decode +import bech32 + +from decred import DecredError +from decred.crypto.crypto import ( + RIPEMD160_SIZE, + PKFCompressed, + PKFUncompressed, + PKFHybrid, +) +from decred.crypto.secp256k1.curve import curve as Secp256k1, PrivateKey +from decred.btc import nets +from decred.util.encode import BuildyBytes, ByteArray, decodeBlob, unblobCheck + + +PubKeyBytesLenCompressed = 33 +PubKeyBytesLenUncompressed = 65 +PubKeyBytesLenHybrid = 65 +pubkeyCompressed = 0x2 # y_bit + x coord +pubkeyUncompressed = 0x4 # x coord + y coord +pubkeyHybrid = 0x6 # y_bit + x coord + y coord +PrivKeyBytesLen = 32 +compressMagic = 0x01 + + +class Address: + """ + A parent class for all addresses. This class specifies an API that all + child classes should implement. + """ + + def __init__(self, netParams): + """ + Args: + chainParams (module): The network parameters. + """ + self.netName = netParams.Name + + @staticmethod + def blob(addr: 'Address'): + """Satisfies the encode.Blobber API""" + aEnc = addr.string().encode() + netEnc = addr.netName.encode() + return BuildyBytes(0).addData(netEnc).addData(aEnc).b + + @staticmethod + def unblob(b: Union[ByteString, ByteArray]): + """Satisfies the encode.Blobber API""" + ver, d = decodeBlob(b) + unblobCheck("Address", ver, len(d), {0: 2}) + return decodeAddress(d[1].decode(), nets.parse(d[0].decode())) + + def __eq__(self, a: 'Address'): + """Check that other address is equivalent to this address.""" + raise NotImplementedError("__eq__ must be implemented by child class") + + def string(self) -> str: + """ + The base-58 encoding of the address. + + Note that string() differs subtly from address(): string() will return + the value as a string without any conversion, while address() may + convert destination types (for example, converting pubkeys to P2PKH + addresses) before encoding as a payment address string. + + Returns: + str: The encoded address. + """ + raise NotImplementedError("string must be implemented by child class") + + def encodeAddress(self) -> str: + """ + encodeAddress returns the string encoding of the payment address + associated with the Address value. See the comment on string for how + this method differs from string. + + Returns: + str: The encoded address. + """ + + raise NotImplementedError("encodeAddress must be implemented by child class") + + def scriptAddress(self) -> ByteArray: + """ + The raw bytes of the address to be used when inserting the address into + a txout's script. + + Returns: + ByteArray: The script address. + """ + + raise NotImplementedError("scriptAddress must be implemented by child class") + + def isForNet(self, chainParams: object) -> bool: + """ + isForNet returns whether or not the address is associated with the + passed bitcoin network. + + Returns: + bool: True if address is for the supplied network. + """ + + raise NotImplementedError("isForNet must be implemented by child class") + + +class AddressPubKeyHash(Address): + """ + Address based on a pubkey hash. + """ + + def __init__(self, pkHash: Optional[ByteArray] = None, netParams: Optional[object] = None): + """ + Args: + pkHash (ByteArray): The hashed pubkey. + netParams (module): The network parameters. + """ + super().__init__(netParams) + pkh_len = len(pkHash) + if pkh_len != RIPEMD160_SIZE: + raise DecredError( + f"AddressPubKeyHash expected {RIPEMD160_SIZE} bytes, got {pkh_len}" + ) + + self.netID = netParams.PubKeyHashAddrID + self.pkHash = pkHash + + def __eq__(self, a: Union[str, Address]) -> bool: + """Check that other address is equivalent to this address.""" + if isinstance(a, str): + return a == self.string() + elif isinstance(a, AddressPubKeyHash): + return ( + a.pkHash == self.pkHash + and a.netID == self.netID + ) + return False + + def string(self) -> str: + """ + A base-58 encoding of the pubkey hash. + + Returns: + str: The encoded address. + """ + return encodeAddressBase58(self.pkHash, self.netID) + + def encodeAddress(self) -> str: + """ + The string encoding of a pay-to-pubkey-hash address. + + Returns: + str: The encoded address. + """ + return self.string() + + def scriptAddress(self) -> ByteArray: + """ + 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 isForNet(self, chainParams: object) -> bool: + """ + isForNet returns whether or not the address is associated with the + passed bitcoin network. + + Returns: + bool: True if address is for the supplied network. + """ + return self.netID == chainParams.PubKeyHashAddrID + + def hash160(self) -> ByteArray: + """ + For AddressPubKeyHash, hash160 is the same as scriptAddress. + + Returns: + ByteArray: The hash. + """ + return self.pkHash.copy() + + +class AddressScriptHash(Address): + """ + AddressScriptHash is an Address for a pay-to-script-hash (P2SH) 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 + + @staticmethod + def fromScript(script, netParams): + """ + Create a new AddressScriptHash from a redeem script. + + Args: + script (ByteArray): the redeem script + netParams (module): the network parameters + + Returns: + AddressScriptHash: An address object. + """ + return AddressScriptHash(hash160(script.b), netParams) + + def __eq__(self, a: Union[str, Address]) -> bool: + """Check that other address is equivalent to this address.""" + if isinstance(a, str): + return a == self.string() + elif isinstance(a, AddressScriptHash): + return a.scriptHash == self.scriptHash and a.netID == self.netID + return False + + def string(self) -> str: + """ + A base-58 encoding of the pubkey hash. + + Returns: + str: The encoded address. + """ + return encodeAddressBase58(self.scriptHash, self.netID) + + def encodeAddress(self) -> str: + """ + The string encoding of a pay-to-pubkey-hash address. + + Returns: + str: The encoded address. + """ + return self.string() + + def scriptAddress(self) -> ByteArray: + """ + 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 isForNet(self, chainParams: object) -> bool: + """ + isForNet returns whether or not the address is associated with the + passed bitcoin network. + + Returns: + bool: True if address is for the supplied network. + """ + return self.netID == chainParams.ScriptHashAddrID + + def hash160(self) -> ByteArray: + """ + For AddressPubKeyHash, hash160 is the same as scriptAddress. + + Returns: + ByteArray: The hash. + """ + return self.scriptHash.copy() + + +class AddressPubKey(Address): + """ + An address based on an unhashed public key. + """ + + def __init__(self, serializedPubkey, netParams): + """ + Args: + serializedPubkey (ByteArray): Corresponds to the serialized + compressed public key (33 bytes). + netParams (module): The network parameters. + """ + super().__init__(netParams) + + pubkey = Secp256k1.parsePubKey(serializedPubkey) + # Set the format of the pubkey. 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. + + pkFormat = PKFUncompressed + fmt = serializedPubkey[0] + if fmt in (0x02, 0x03): + pkFormat = PKFCompressed + elif fmt in (0x06, 0x07): + pkFormat = PKFHybrid + + self.pubkeyFormat = pkFormat + self.netID = netParams.PubKeyHashAddrID + self.pubkeyHashID = netParams.PubKeyHashAddrID + self.pubkey = pubkey + + def __eq__(self, a): + """Check that other address is equivalent to this address.""" + if isinstance(a, str): + return a == self.encodeAddress() or a == self.string() + elif isinstance(a, AddressPubKey): + return self.pubkey.serializeCompressed() == a.pubkey.serializeCompressed() + return False + + def serialize(self): + """ + The serialization of the public key according to the format associated + with the address. + + Returns: + ByteArray: The serialized publid key. + """ + fmt = self.pubkeyFormat + if fmt == PKFUncompressed: + return self.pubkey.serializeUncompressed() + elif fmt == PKFCompressed: + return self.pubkey.serializeCompressed() + elif fmt == PKFHybrid: + return self.pubkey.serializeHybrid() + raise NotImplementedError(f"unknown pubkey format {fmt}") + + def string(self): + """ + A base-58 encoding of the pubkey. + + Returns: + str: The encoded address. + """ + return self.serialize().hex() + + def encodeAddress(self): + """ + 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. + + Returns: + str: base-58 encoded p2pkh address. + """ + return encodeAddressBase58(hash160(self.serialize().bytes()), self.pubkeyHashID) + + def scriptAddress(self): + """ + 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 isForNet(self, netParams: object) -> bool: + """ + isForNet returns whether or not the address is associated with the + passed bitcoin network. + + Returns: + bool: True if address is for the supplied network. + """ + return self.netID == netParams.PubKeyHashAddrID + + def hash160(self): + """ + The hash160 of the serialized pubkey. + + Returns: + ByteArray: The hash. + """ + return hash160(self.serialize().bytes()) + + def addressPubKeyHash(self) -> AddressPubKeyHash: + """ + AddressPubKeyHash returns the pay-to-pubkey address converted to a + pay-to-pubkey-hash address. 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 Bitcoin addresses + are pay-to-pubkey-hash constructed from the uncompressed public key. + """ + return AddressPubKeyHash(self.hash160(), self.pubkeyHashID) + + +class AddressWitnessPubKeyHash(Address): + """ + Address based on a witness pubkey hash. + """ + + def __init__(self, witnessProg: ByteArray, netParams: object): + """ + Args: + witnessProg (ByteArray): The witness program. Just the pubkey hash. + hrp (module or str): The bech32 prefix or the network parameters. + """ + super().__init__(netParams) + self.hrp = netParams.Bech32HRPSegwit + self.witnessVersion = 0 + self.witnessProgram = witnessProg + + def __eq__(self, a: Union[str, Address]) -> bool: + """Check that other address is equivalent to this address.""" + if isinstance(a, str): + return a.lower() == self.string() + elif isinstance(a, AddressWitnessPubKeyHash): + return ( + a.witnessProgram == self.witnessProgram + and a.hrp == self.hrp + and a.witnessVersion == self.witnessVersion + ) + return False + + def encodeAddress(self) -> str: + """ + The string encoding of a pay-to-witness-pubkey-hash address. + + Returns: + str: The encoded address. + """ + return encodeSegWitAddress(self.hrp, self.witnessVersion, self.witnessProgram) + + def scriptAddress(self) -> ByteArray: + """ + 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.witnessProgram + + def isForNet(self, chainParams: object) -> bool: + """ + isForNet returns whether or not the address is associated with the + passed bitcoin network. + + Returns: + bool: True if address is for the supplied network. + """ + return self.hrp == chainParams.Bech32HRPSegwit + + def string(self) -> str: + """ + A bech32 encoding of the pubkey. + + Returns: + str: The encoded address. + """ + return self.encodeAddress() + + def hash160(self) -> ByteArray: + """ + The hash160 of the serialized pubkey. + + Returns: + ByteArray: The hash. + """ + return self.witnessProgram + + +class AddressWitnessScriptHash(Address): + """ + Address based on a witness script hash. + """ + + def __init__(self, witnessProg: ByteArray, netParams: object): + """ + Args: + witnessProg (ByteArray): The witness program. Just the pubkey hash. + hrp (module or str): The bech32 prefix or the network parameters. + """ + super().__init__(netParams) + if len(witnessProg) != 32: + raise DecredError(f"witness program must be 32 bytes for p2wsh. got {len(witnessProg)}") + + self.hrp = netParams.Bech32HRPSegwit + self.witnessProgram = witnessProg + self.witnessVersion = 0 + + def __eq__(self, a: Union[str, Address]) -> bool: + """Check that other address is equivalent to this address.""" + if isinstance(a, str): + return a.lower() == self.string() + elif isinstance(a, AddressWitnessScriptHash): + return ( + a.witnessProgram == self.witnessProgram and + a.hrp == self.hrp and + a.witnessVersion == self.witnessVersion + ) + return False + + def encodeAddress(self) -> str: + """ + The string encoding of a pay-to-witness-script-hash address. + + Returns: + str: The encoded address. + """ + return encodeSegWitAddress(self.hrp, self.witnessVersion, self.witnessProgram) + + def scriptAddress(self) -> ByteArray: + """ + 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.witnessProgram + + def isForNet(self, chainParams: object) -> bool: + """ + isForNet returns whether or not the address is associated with the + passed bitcoin network. + + Returns: + bool: True if address is for the supplied network. + """ + return self.hrp == chainParams.Bech32HRPSegwit + + def string(self) -> str: + """ + A bech32 encoding of the script-hash. + + Returns: + str: The encoded address. + """ + return self.encodeAddress() + + +class WIF: + def __init__(self, privKey: PrivateKey, compressPubKey: bool, netID: Union[object, int]): + if hasattr(netID, "PrivateKeyID"): + netID = netID.PrivateKeyID + self.privKey = privKey + self.compressPubKey = compressPubKey + self.netID = netID + + def dict(self) -> dict: + return dict( + privKey=self.privKey.key.hex(), + compressPubKey=self.compressPubKey, + ) + + @staticmethod + def decode(wif: str) -> 'WIF': + decoded = ByteArray(b58decode(wif)) + decodedLen = len(decoded) + compress = False + + # Length of base58 decoded WIF must be 32 bytes + an optional 1 byte + # (0x01) if compressed, plus 1 byte for netID + 4 bytes of checksum. + if decodedLen == 1 + PrivKeyBytesLen + 1 + 4: + if decoded[33] != compressMagic: + raise DecredError("malformed 38-byte private key") + compress = True + elif decodedLen != 1 + PrivKeyBytesLen + 4: + raise DecredError("malformed private key") + + # Checksum is first four bytes of double SHA256 of the identifier byte + # and privKey. Verify this matches the final 4 bytes of the decoded + # private key. + if compress: + tosum = decoded[:1+PrivKeyBytesLen+1] + else: + tosum = decoded[:1+PrivKeyBytesLen] + + cksum = checksum(tosum.b) + if cksum != decoded[decodedLen-4:]: + raise DecredError("checksum mismatch") + + netID = decoded[0] + privKeyBytes = decoded[1: 1+PrivKeyBytesLen] + privKey = PrivateKey.fromBytes(privKeyBytes) + return WIF(privKey=privKey, compressPubKey=compress, netID=netID) + + def isForNet(self, netParams: object) -> bool: + return self.netID == netParams.PrivateKeyID + + def string(self) -> str: + a = ByteArray(self.netID) + # Pad and append bytes manually, instead of using Serialize, to + # avoid another call to make. + a += ByteArray(self.privKey.key, length=PrivKeyBytesLen) + if self.compressPubKey: + a += compressMagic + + a += checksum(a.b) + return b58encode(a.b).decode("utf-8") + + def serializePubKey(self) -> ByteArray: + if self.compressPubKey: + return self.privKey.pub.serializeCompressed() + return self.privKey.pub.serializeUncompressed() + + +def encodeAddressBase58(k, netID): + """ + Base-58 encode the number, with the netID prepended byte-wise. + + Args: + k (ByteArray): The pubkey or pubkey-hash or script-hash. + netID (byte-like): The addresses network encoding ID. + + Returns: + string: Base-58 encoded address. + """ + b = ByteArray(netID) + b += k + b += checksum(b.b) + return b58encode(b.bytes()).decode() + + +def decodeAddress(addr: str, netParams: object): + """ + DecodeAddress decodes the base-58 encoded address and returns the Address if + it is a valid encoding for a known address type and is for the provided + network. + + Args: + addr (str): Base-58 encoded address. + netParams (module): The network parameters. + """ + oneIndex = addr.find("1") + if oneIndex > 1: + hrp = addr[:oneIndex].lower() + if hrp == netParams.Bech32HRPSegwit: + witnessVer, witnessProg = decodeSegWitAddress(netParams.Bech32HRPSegwit, addr) + + # We currently only support P2WPKH and P2WSH, which is + # witness version 0. + if witnessVer != 0: + raise DecredError(f"unsupported witness version {witnessVer}") + + witnessLen = len(witnessProg) + if witnessLen == 20: + return AddressWitnessPubKeyHash(witnessProg, netParams) + elif witnessLen == 32: + return AddressWitnessScriptHash(witnessProg, netParams) + else: + raise DecredError(f"unsupported witness program length {witnessLen}") + + # Serialized public keys are either 65 bytes (130 hex chars) if + # uncompressed/hybrid or 33 bytes (66 hex chars) if compressed. + if len(addr) == 130 or len(addr) == 66: + serializedPubKey = ByteArray(addr) + return AddressPubKey(serializedPubKey, netParams) + + # Switch on decoded length to determine the type. + hash160, netID = b58CheckDecode(addr) + + if len(hash160) == RIPEMD160_SIZE: # P2PKH or P2SH + isP2PKH = netID == netParams.PubKeyHashAddrID + isP2SH = netID == netParams.ScriptHashAddrID + if isP2PKH and isP2SH: + raise DecredError("address collision") + elif isP2PKH: + return AddressPubKeyHash(hash160, netParams) + elif isP2SH: + return AddressScriptHash(hash160, netParams) + else: + raise DecredError("unknown address type") + + raise DecredError(f"decoded address is of unknown size {len(hash160)}") + + +def encodeSegWitAddress(hrp: str, witnessVersion: int, witnessProgram: ByteArray) -> str: + """ + encodeSegWitAddress creates a bech32 encoded address string representation + from witness version and witness program. + """ + # # Group the address bytes into 5 bit groups, as this is what is used to + # # encode each character in the address string. + # convertedI = bech32.convertbits(witnessProgram.b, 8, 5, True) + # if not convertedI: + # raise DecredError("convertbits error") + + # converted = ByteArray(convertedI) + + # Concatenate the witness version and program, and encode the resulting + # bytes using bech32 encoding. + bech = bech32.encode(hrp, witnessVersion, witnessProgram.b) + if not bech: + raise DecredError("bech32.encode error") + + # Check validity by decoding the created address. + version, program = decodeSegWitAddress(hrp, bech) + + if version != witnessVersion or program != witnessProgram: + raise DecredError("invalid segwit address") + + return bech + + +def decodeSegWitAddress(hrp: str, addr: str) -> Tuple[int, ByteArray]: + """ + decodeSegWitAddress parses a bech32 encoded segwit address string and + returns the witness version and witness program byte representation. + """ + # Decode the bech32 encoded address. + ver, dataI = bech32.decode(hrp, addr) + if not dataI: + raise DecredError("prefix mismatch") + + return ver, ByteArray(dataI) + + +# def decodeAddressPubKey(decoded, netParams): +# """ +# decodeAddressPubKey decodes a pubkey-type address from the serialized +# pubkey. + +# Args: +# decoded (bytes): A 33 bytes decoded pubkey such as would be decoded +# from a base58 string. The first byte indicates the signature suite. +# For compressed secp256k1 pubkeys, use AddressPubKey directly. +# netParams (module): The network parameters. +# """ +# if len(decoded) != 33: +# raise NotImplementedError(f"unable to decode pubkey of length {len(decoded)}") +# # First byte is the signature suite and ybit. +# suite = decoded[0] +# suite &= 127 +# ybit = not (decoded[0] & (1 << 7) == 0) +# toAppend = 0x02 +# if ybit: +# toAppend = 0x03 + +# if suite == STEcdsaSecp256k1: +# b = ByteArray(toAppend) + decoded[1:] +# return AddressPubKey(b, netParams) +# elif suite == STEd25519: +# raise NotImplementedError("Edwards signatures not implemented") +# elif suite == STSchnorrSecp256k1: +# raise NotImplementedError("Schnorr signatures not implemented") +# else: +# raise NotImplementedError(f"unknown address type {suite}") + + +# def deriveChildAddress(branchXPub, i, netParams): +# """ +# The base-58 encoded address for the i'th child. + +# Args: +# i (int): Child number. +# netParams (module): Network parameters. + +# Returns: +# str: Child address, as a base-58 encoded string. +# """ +# child = branchXPub.child(i) +# return AddressPubKeyHash( +# hash160(child.publicKey().serializeCompressed().b), netParams, +# ).string() + + +def b58CheckDecode(s: str) -> Tuple[ByteArray, int]: + """ + 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) < 5: + raise DecredError("decoded lacking version/checksum") + version = decoded[0] + 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[1: len(decoded) - 4]) + return payload, version + + +def checksum(b: ByteArray): + """ + This checksum is used to validate base-58 addresses. + + Args: + b (byte-like): Bytes to obtain a checksum for. + + Returns: + bytes: The first 4 bytes of a double sha256 hash of input. + """ + v = hashlib.sha256(b).digest() + return hashlib.sha256(v).digest()[:4] + + +def hash160(b): + """ + A RIPEMD160 hash of the blake256 hash of the input. + + Args: + b (byte-like): The bytes to hash. + + Returns: + ByteArray: A 20-byte hash. + """ + h = hashlib.new("ripemd160") + h.update(hashlib.sha256(b).digest()) + return ByteArray(h.digest()) + + +def isEven(i): + return i % 2 == 0 diff --git a/decred/decred/btc/btcwallet/__init__.py b/decred/decred/btc/btcwallet/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/decred/decred/btc/btcwallet/btcwallet.py b/decred/decred/btc/btcwallet/btcwallet.py new file mode 100644 index 00000000..5054f63e --- /dev/null +++ b/decred/decred/btc/btcwallet/btcwallet.py @@ -0,0 +1,851 @@ +import json +from typing import Callable, Dict, List, Optional, Union + +from decred.crypto.secp256k1.curve import curve as Curve, PublicKey, PrivateKey +from decred import DecredError +from decred.btc import addrlib +from decred.btc.wire import msgtx +from decred.btc.btcwallet.golink import Go, registerFeeder +from decred.util.encode import ByteArray + + +JSONType = Union[str, int, float, bool, None, Dict[str, 'JSONType'], List['JSONType']] + + +class WalletException(DecredError): + def __init__(self, e): + """ + Virtually any error from the Go side will be returned as a WalletException. + """ + super().__init__(e["error"]) + + +def isError(e): + """ + All messages pass through a single interface. If any error is encountered + while running the specified command on the Go side, the response will have + a non-nil "error" property. + """ + return isinstance(e, dict) and e.get("error") + + +class OutputSelectionPolicy: + def __init__(self, acct: int, requiredConfs: int): + self.account = acct + self.requiredConfs = requiredConfs + + +class BlockIdentity: + def __init__(self, blockHash: ByteArray, blockHeight: int): + self.hash = blockHash + self.height = blockHeight + + +class TransactionOutput: + def __init__( + self, + outPoint: msgtx.OutPoint, + output: msgtx.TxOut, + outputKind: int, + containingBlock: BlockIdentity, + receiveTime: int, + ): + self.outPoint = outPoint + self.output = output + self.outputKind = outputKind + self.containingBlock = containingBlock + self.receiveTime = receiveTime + + +class Balances: + def __init__(self, total: int, spendable: int, immatureReward: int): + self.total = total + self.spendable = spendable + self.immatureReward = immatureReward + + +class AuthoredTx: + def __init__( + self, + tx: msgtx.MsgTx, + prevScripts: List[ByteArray], + prevInputValues: List[int], + totalInput: int, + changeIndex: int, + ): + self.tx = tx + self.prevScripts = prevScripts + self.prevInputValues = prevInputValues + self.totalInput = totalInput + self.changeIndex = changeIndex + + +class KeyScope: + def __init__(self, purpose: int, coin: int): + self.purpose = purpose + self.coin = coin + + def dict(self) -> dict: + return dict( + coin=self.coin, + purpose=self.purpose, + ) + + +KeyScopeBIP0044 = KeyScope(44, 0) + + +def parseScope(scope): + return scope.dict() if scope else KeyScopeBIP0044.dict() + + +class ManagedAddress: + def __init__( + self, + acct: int, + addr: addrlib.Address, + addrHash: ByteArray, + imported: bool, + internal: bool, + compressed: bool, + addrType: int, + ): + self.acct = acct + self.addr = addr + self.addrHash = addrHash + self.imported = imported + self.internal = internal + self.compressed = compressed + self.addrType = addrType + + +class AccountProperties: + def __init__( + self, + accountNumber: int, + accountName: str, + externalKeyCount: int, + internalKeyCount: int, + importedKeyCount: int, + ): + self.accountNumber = accountNumber + self.accountName = accountName + self.externalKeyCount = externalKeyCount + self.internalKeyCount = internalKeyCount + self.importedKeyCount = importedKeyCount + + +class ListTransactionsResult: + def __init__( + self, + abandoned: bool, + account: str, + amount: int, + category: str, + confirmations: int, + time: int, + timeReceived: int, + trusted: bool, + txid: str, + vout: int, + address: Optional[Union[addrlib.Address, None]] = None, + bip125: Optional[str] = "", + blockHash: Optional[Union[ByteArray, None]] = None, + blockHeight: Optional[Union[int, None]] = None, + blockIndex: Optional[Union[int, None]] = None, + blockTime: Optional[Union[int, None]] = None, + fee: Optional[int] = 0, + generated: Optional[Union[bool, None]] = None, + involvesWatchOnly: Optional[Union[bool, None]] = None, + label: Optional[Union[str, None]] = None, + walletConflicts: Optional[Union[List[str], None]] = None, + comment: Optional[Union[str, None]] = None, + otherAccount: Optional[Union[str, None]] = None, + ): + + self.abandoned = abandoned + self.account = account + self.address = address + self.amount = amount + self.bip125 = bip125 + self.blockHash = blockHash + self.blockHeight = blockHeight + self.blockIndex = blockIndex + self.blockTime = blockTime + self.category = category + self.confirmations = confirmations + self.fee = fee + self.generated = generated + self.involvesWatchOnly = involvesWatchOnly + self.label = label + self.time = time + self.timeReceived = timeReceived + self.trusted = trusted + self.txid = txid + self.vout = vout + self.walletConflicts = walletConflicts + self.comment = comment + self.otherAccount = otherAccount + + +class AccountResult(AccountProperties): + def __init__(self, totalBalance: int, **k): + super().__init__(**k) + self.totalBalance = totalBalance + + +class AccountsResult: + def __init__( + self, + accounts: List[AccountResult], + currentBlockHash: ByteArray, + currentBlockHeight: int, + ): + self.accounts = accounts + self.currentBlockHash = currentBlockHash + self.currentBlockHeight = currentBlockHeight + + +class AccountBalanceResult: + def __init__( + self, + accountNumber: int, + accountName: str, + accountBalance: int, + ): + self.accountNumber = accountNumber + self.accountName = accountName + self.accountBalance = accountBalance + + +class ListUnspentResult: + def __init__( + self, + txid: str, + vout: int, + address: addrlib.Address, + account: str, + scriptPubKey: ByteArray, + amount: int, + confirmations: int, + spendable: bool, + redeemScript: Optional[Union[ByteArray, None]] = None, + ): + self.txid = txid + self.vout = vout + self.address = address + self.account = account + self.scriptPubKey = scriptPubKey + self.amount = amount + self.confirmations = confirmations + self.spendable = spendable + self.redeemScript = redeemScript + + +class BlockStamp: + def __init__( + self, + height: int, + blockHash: ByteArray, + timeStamp: int, + ): + self.height = height + self.blockHash = blockHash + self.timeStamp = timeStamp + + def dict(self) -> dict: + return dict( + height=self.height, + hash=self.blockHash.hex(), + timestamp=self.timeStamp, + ) + + +class AccountTotalReceivedResult: + def __init__( + self, + accountNumber: int, + accountName: str, + totalReceived: int, + lastConfirmation: int, + ): + self.accountNumber = accountNumber + self.accountName = accountName + self.totalReceived = totalReceived + self.lastConfirmation = lastConfirmation + + +class SyncStatus: + def __init__(self, target: int, height: int, syncing: bool): + self.target = target + self.height = height + self.syncing = syncing + + def dict(self): + return dict( + target=self.target, + height=self.height, + syncing=self.syncing, + ) + + +def WalletExists(walletDir: str, net: object) -> bool: + if hasattr(net, "Name"): + net = net.Name + return Go("walletExists", dict( + net=net, + dir=walletDir, + )) + + +def CreateWallet(seed: ByteArray, pw: ByteArray, walletDir: str, netParams: object): + res = Go('createWallet', dict( + pw=pw.hex(), + seed=seed.hex(), + dir=walletDir, + net=netParams.Name, + )) + if isError(res): + raise WalletException(res) + + +class BTCWallet: + """ + BTCWallet is a Bitcoin wallet that works based on C bindings to + github.com/btcsuite/btcwallet. + + All methods are named identically to the btcwallet.Wallet counterparts, with + the exception that the leading letter is lower-cased. + """ + def __init__( + self, + walletDir: str, + netParams: object, + feeder: Callable[[JSONType], None], + debugLevel: Optional[int] = 3, # Info + connectPeers: Optional[Union[List[str], None]] = None, + test: Optional[bool] = False, + ): + self.netParams = netParams + self.feeder = feeder + registerFeeder(self.feed) + self.go("init", dict( + dir=walletDir, + net=netParams.Name, + logLevel=debugLevel, # Debug + connectPeers=connectPeers, + test=test, + )) + + def feed(self, msg: ByteArray): + msg["symbol"] = "btc" + self.feeder(msg) + + def go(self, func: str, thing: Optional[JSONType] = "") -> JSONType: + res = Go(func, thing) + if isError(res): + raise WalletException(res) + return res + + def makeMultiSigScript(self, addrs: List[addrlib.Address], nRequired: int) -> ByteArray: + hexScript = self.go("makeMultiSigScript", dict( + addrs=[a.string() for a in addrs], + nRequired=nRequired, + )) + return ByteArray(hexScript) + + def importP2SHRedeemScript(self, script: ByteArray) -> addrlib.AddressScriptHash: + addrStr = self.go("importP2SHRedeemScript", script.hex()) + return addrlib.decodeAddress(addrStr, self.netParams) + + def unspentOutputs(self, policy: OutputSelectionPolicy) -> List[TransactionOutput]: + res = self.go("unspentOutputs", dict(account=policy.account, requiredConfirmations=policy.requiredConfs)) + outputs = [] + for to in res: + outPt = to["outPoint"] + op = to["output"] + blk = to["containingBlock"] + + outputs.append(TransactionOutput( + outPoint=msgtx.OutPoint( + txHash=ByteArray(outPt["hash"]), + idx=outPt["index"], + ), + output=msgtx.TxOut( + value=op["value"], + pkScript=ByteArray(op["script"]), + ), + outputKind=to["outputKind"], + containingBlock=BlockIdentity( + blockHash=blk["hash"], + blockHeight=blk["index"], + ), + receiveTime=to["receiveTime"], + )) + + return outputs + + def start(self): + self.go("start") + + def stop(self): + self.go("stop") + + def shuttingDown(self) -> bool: + return self.go("shuttingDown") + + def waitForShutdown(self): + self.go("waitForShutdown") + + def synchronizingToNetwork(self) -> bool: + return self.go("synchronizingToNetwork") + + def chainSynced(self) -> bool: + return self.go("chainSynced") + + def setChainSynced(self, synced: bool): + self.go("setChainSynced", synced) + + def createSimpleTx( + self, + acct: int, + outputs: List[msgtx.TxOut], + minConf: int, + satPerKb: float, + dryRun: bool, + ) -> AuthoredTx: + res = self.go("createSimpleTx", dict( + account=acct, + outputs=[dict(script=to.pkScript.hex(), value=to.value) for to in outputs], + minconf=minConf, + satPerKb=satPerKb, + dryRun=dryRun, + )) + + return AuthoredTx( + tx=msgtx.MsgTx.deserialize(ByteArray(res["tx"])), + prevScripts=[ByteArray(s) for s in res["prevScripts"]], + prevInputValues=res["prevInputValues"], + totalInput=res["totalInput"], + changeIndex=res["changeIndex"], + ) + + def unlock(self, passphrase: str, timeout: int): + self.go("unlock", dict(passphrase=ByteArray(passphrase.encode("utf-8")).hex(), timeout=timeout)) + + def lock(self): + self.go("lock") + + def locked(self) -> bool: + return self.go("locked") + + def changePrivatePassphrase(self, old: str, new: str): + self.go("changePrivatePassphrase", dict( + old=ByteArray(old.encode("utf-8")).hex(), + new=ByteArray(new.encode("utf-8")).hex(), + )) + + def changePublicPassphrase(self, old: str, new: str): + self.go("changePublicPassphrase", dict( + old=ByteArray(old.encode("utf-8")).hex(), + new=ByteArray(new.encode("utf-8")).hex(), + )) + + def changePassphrases(self, publicOld: str, publicNew: str, privateOld: str, privateNew: str): + self.go("changePassphrases", dict( + public=dict( + old=ByteArray(publicOld.encode("utf-8")).hex(), + new=ByteArray(publicNew.encode("utf-8")).hex(), + ), + private=dict( + old=ByteArray(privateOld.encode("utf-8")).hex(), + new=ByteArray(privateNew.encode("utf-8")).hex(), + ), + )) + + def accountAddresses(self, acct: int) -> List[addrlib.Address]: + return [addrlib.decodeAddress(a, self.netParams) for a in self.go("accountAddresses", acct)] + + def calculateBalance(self, confirms: int) -> int: + # func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) + return self.go("calculateBalance", confirms) + + def calculateAccountBalances(self, acct: int, confirms: int) -> Balances: + res = self.go("calculateAccountBalances", dict( + account=acct, + confirms=confirms, + )) + + return Balances( + total=res["total"], + spendable=res["spendable"], + immatureReward=res["immatureReward"], + ) + + def currentAddress(self, acct: int, scope: Union[KeyScope, None] = None) -> addrlib.Address: + return addrlib.decodeAddress(self.go("currentAddress", dict( + account=acct, + scope=parseScope(scope), + )), self.netParams) + + def pubKeyForAddress(self, addr: addrlib.Address) -> PublicKey: + pkb = ByteArray(self.go("pubKeyForAddress", addr.string())) + return Curve.parsePubKey(pkb) + + def privKeyForAddress(self, addr: addrlib.Address) -> PrivateKey: + return PrivateKey.fromBytes(ByteArray(self.go("privKeyForAddress", addr.string()))) + + def labelTransaction(self, h: ByteArray, label: str, overwrite: bool): + self.go("labelTransaction", dict( + hash=h.hex(), + label=label, + overwrite=overwrite, + )) + + def haveAddress(self, addr: addrlib.Address) -> bool: + return self.go("haveAddress", addr.string()) + + def accountOfAddress(self, addr: addrlib.Address) -> int: + return self.go("accountOfAddress", addr.string()) + + def addressInfo(self, addr: addrlib.Address) -> ManagedAddress: + res = self.go("addressInfo", addr.string()) + + return ManagedAddress( + acct=res["account"], + addr=addrlib.decodeAddress(res["address"], self.netParams), + addrHash=ByteArray(res["addrHash"]), + imported=res["imported"], + internal=res["internal"], + compressed=res["compressed"], + addrType=res["addrType"], + ) + + def accountNumber(self, accountName: str, scope: Union[KeyScope, None] = None) -> int: + return self.go("accountNumber", dict( + accountName=accountName, + scope=parseScope(scope), + )) + + def accountName(self, accountNumber: int, scope: Union[KeyScope, None] = None) -> str: + return self.go("accountName", dict( + accountNumber=accountNumber, + scope=parseScope(scope), + )) + + def accountProperties(self, accountNumber: int, scope: Union[KeyScope, None] = None) -> AccountProperties: + res = self.go("accountProperties", dict( + accountNumber=accountNumber, + scope=parseScope(scope), + )) + + return AccountProperties( + accountNumber=res["accountNumber"], + accountName=res["accountName"], + externalKeyCount=res["externalKeyCount"], + internalKeyCount=res["internalKeyCount"], + importedKeyCount=res["importedKeyCount"], + ) + + def renameAccount(self, accountNumber: int, newName: str, scope: Union[KeyScope, None] = None): + self.go("renameAccount", dict( + accountNumber=accountNumber, + newName=newName, + scope=scope.dict(), + )) + + def nextAccount(self, accountName: str, scope: Union[KeyScope, None] = None) -> int: + return self.go("nextAccount", dict( + accountName=accountName, + scope=parseScope(scope), + )) + + def requestListTransactions(self, func: str, params: JSONType) -> List[ListTransactionsResult]: + results = [] + + for row in self.go(func, params): + + results.append(ListTransactionsResult( + abandoned=row["abandoned"], + account=row["account"], + amount=row["amount"], + category=row["category"], + confirmations=row["confirmations"], + time=row["time"], + timeReceived=row["timereceived"], + trusted=row["trusted"], + txid=row["txid"], + vout=row["vout"], + address=row.get("address"), + bip125=row.get("bip125"), + blockHash=row.get("blockhash"), + blockHeight=row.get("blockheight"), + blockIndex=row.get("blockindex"), + blockTime=row.get("blocktime"), + fee=row.get("fee"), + generated=row.get("generated"), + involvesWatchOnly=row.get("involveswatchonly"), + label=row.get("label"), + walletConflicts=row.get("walletconflicts"), + comment=row.get("comment"), + otherAccount=row.get("otheraccount"), + )) + + return results + + def listSinceBlock(self, start: int, end: int, syncHeight: int) -> List[ListTransactionsResult]: + return self.requestListTransactions("listSinceBlock", dict( + start=start, + end=end, + syncHeight=syncHeight, + )) + + def listTransactions(self, skip: int, count: int) -> List[ListTransactionsResult]: + return self.requestListTransactions("listTransactions", { + "from": skip, # from is a Python keyword + "count": count, + }) + + def listAddressTransactions(self, addrs: List[str]) -> List[ListTransactionsResult]: + return self.requestListTransactions("listAddressTransactions", addrs) + + def listAllTransactions(self) -> List[ListTransactionsResult]: + return self.requestListTransactions("listAllTransactions", "") + + def accounts(self, scope: Union[KeyScope, None] = None) -> AccountsResult: + res = self.go("accounts", dict( + coin=scope.coin, + purpose=scope.purpose if scope else KeyScopeBIP0044, + )) + + accounts = [AccountResult( + totalBalance=acct["totalBalance"], + accountNumber=acct["accountNumber"], + accountName=acct["accountName"], + externalKeyCount=acct["externalKeyCount"], + internalKeyCount=acct["internalKeyCount"], + importedKeyCount=acct["importedKeyCount"], + ) for acct in res["accounts"]] + + return AccountsResult( + accounts=accounts, + currentBlockHash=ByteArray(res["currentBlockHash"]), + currentBlockHeight=res["currentBlockHeight"], + ) + + def accountBalances(self, requiredConfs: int, scope: Union[KeyScope, None] = None) -> List[AccountBalanceResult]: + rows = self.go("accountBalances", dict( + requiredConfs=requiredConfs, + scope=parseScope(scope), + )) + + return [AccountBalanceResult( + accountNumber=row["accountNumber"], + accountName=row["accountName"], + accountBalance=row["accountBalance"], + ) for row in rows] + + def listUnspent(self, minConf: int, maxConf: int, addresses: List[str]) -> List[ListUnspentResult]: + rows = self.go("listUnspent", dict( + minConf=minConf, + maxConf=maxConf, + addresses=addresses, + )) + + results = [] + + for row in rows: + redeemScript = row.get("redeemScript") + if redeemScript: + redeemScript = ByteArray(redeemScript) + results.append(ListUnspentResult( + txid=row["txid"], + vout=row["vout"], + address=row["address"], + account=row["account"], + scriptPubKey=ByteArray(row["scriptPubKey"]), + amount=row["amount"], + confirmations=row["confirmations"], + spendable=row["spendable"], + redeemScript=redeemScript, + )) + + return results + + def dumpPrivKeys(self) -> List[PrivateKey]: + return [PrivateKey.fromBytes(ByteArray(s))for s in self.go("dumpPrivKeys")] + + def dumpWIFPrivateKey(self, addr: addrlib.Address) -> addrlib.WIF: + return addrlib.WIF.decode(self.go("dumpWIFPrivateKey", addr.string())) + + def importPrivateKey(self, wif: addrlib.WIF, bs: BlockStamp, rescan: bool, scope: Union[KeyScope, None] = None) -> str: + return self.go("importPrivateKey", dict( + wif=wif.dict(), + blockStamp=bs.dict(), + rescan=rescan, + keyScope=parseScope(scope), + )) + + def lockedOutpoint(self, op: msgtx.OutPoint) -> bool: + return self.go("lockedOutpoint", op.dict()) + + def unlockOutpoint(self, op: msgtx.OutPoint): + return self.go("unlockOutpoint", op.dict()) + + def lockOutpoint(self, op: msgtx.OutPoint): + return self.go("lockOutpoint", op.dict()) + + def resetLockedOutpoints(self): + return self.go("resetLockedOutpoints") + + def lockedOutpoints(self) -> List[msgtx.OutPoint]: + rows = self.go("lockedOutpoints") + return [msgtx.OutPoint( + txHash=reversed(ByteArray(row["txid"])), + idx=row["vout"] + ) for row in rows] + + def leaseOutput(self, lockID: ByteArray, op: msgtx.OutPoint) -> int: + return self.go("leaseOutput", dict( + id=lockID.hex(), + op=op.dict(), + )) + + def releaseOutput(self, lockID: ByteArray, op: msgtx.OutPoint): + return self.go("releaseOutput", dict( + id=lockID.hex(), + op=op.dict(), + )) + + def sortedActivePaymentAddresses(self) -> List[str]: + return self.go("sortedActivePaymentAddresses") + + def _newAddressFunc(self, func: str, acct: int, scope: Union[KeyScope, None] = None) -> addrlib.Address: + a = self.go(func, dict( + account=acct, + scope=parseScope(scope), + )) + return addrlib.decodeAddress(a, self.netParams) + + def newAddress(self, acct: int, scope: Union[KeyScope, None] = None) -> addrlib.Address: + return self._newAddressFunc("newAddress", acct, scope) + + def newChangeAddress(self, acct: int, scope: Union[KeyScope, None] = None) -> addrlib.Address: + return self._newAddressFunc("newChangeAddress", acct, scope) + + def totalReceivedForAccounts(self, minConf: int, scope: Union[KeyScope, None] = None) -> List[AccountTotalReceivedResult]: + rows = self.go("totalReceivedForAccounts", dict( + minConf=minConf, + scope=parseScope(scope), + )) + return [AccountTotalReceivedResult( + accountNumber=row["accountNumber"], + accountName=row["accountName"], + totalReceived=row["totalReceived"], + lastConfirmation=row["lastConfirmation"], + ) for row in rows] + + def totalReceivedForAddr(self, addr: addrlib.Address, minConf: int) -> int: + return self.go("totalReceivedForAddr", dict( + addr=addr.string(), + minConf=minConf, + )) + + def sendOutputs(self, outputs: List[msgtx.TxOut], acct: int, minConf: int, satPerKb: float, label: str) -> msgtx.MsgTx: + msgTxB = ByteArray(self.go("sendOutputs", dict( + outputs=[dict(script=to.pkScript.hex(), value=to.value) for to in outputs], + account=acct, + minConf=minConf, + satPerKb=satPerKb, + label=label, + ))) + return msgtx.MsgTx.deserialize(msgTxB) + + def signTransaction( + self, + tx: msgtx.MsgTx, + hashType: int, + additionalPrevScripts: Dict[str, ByteArray], # key is outpoint ID txid:index, e.g. a27e1f:1 + additionalKeysByAddress: Dict[str, addrlib.WIF], + p2shRedeemScriptsByAddress: Dict[str, ByteArray], + ): + resp = self.go("signTransaction", dict( + tx=tx.serialize().hex(), + hashType=hashType, + additionalPrevScripts={k: v.hex() for k, v in additionalPrevScripts.items()}, + additionalKeysByAddress={k: v.dict() for k, v in additionalKeysByAddress.items()}, + p2shRedeemScriptsByAddress={k: v.hex() for k, v in p2shRedeemScriptsByAddress.items()}, + )) + + sigErrs = resp["sigErrs"] + if sigErrs: + raise DecredError(f"signature errors: {sigErrs}") + + signedTx = msgtx.MsgTx.deserialize(ByteArray(resp["signedTx"])) + tx.txIn = signedTx.txIn # "sign" the input transaction + + def publishTransaction(self, tx: msgtx.MsgTx, label: str): + self.go("publishTransaction", dict( + tx=tx.serialize().hex(), + label=label, + )) + + def syncStatus(self) -> SyncStatus: + res = self.go("syncStatus") + return SyncStatus(target=res["target"], height=res["height"], syncing=res["syncing"]) + + +# TODO +# func (w *Wallet) SubmitRescan(job *RescanJob) <-chan error + +# TODO +# func (w *Wallet) Rescan(addrs []btcutil.Address, unspent []wtxmgr.Credit) error + +# TODO +# func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel <-chan struct{}) (*GetTransactionsResult, error) + +# TODO: Just pass the name and grab the python versions Python-side. +# func (w *Wallet) ChainParams() *chaincfg.Params + +# INTERNAL USE ONLY +# func (w *Wallet) SynchronizeRPC(chainClient chain.Interface) + +# INTERNAL USE ONLY +# func (w *Wallet) ChainClient() chain.Interface + +# We can also get direct access to these methods in SPV mode. +# +# Neutrino ChainService methods +# ============================= +# BestBlock() (*headerfs.BlockStamp, error) +# GetBlockHash(height int64) (*chainhash.Hash, error) +# GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) +# GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) +# GetBlockHeight(hash *chainhash.Hash) (int32, error) +# BanPeer(addr string, reason banman.Reason) error +# IsBanned(addr string) bool +# AddPeer(sp *ServerPeer) +# AddBytesSent(bytesSent uint64) +# AddBytesReceived(bytesReceived uint64) +# NetTotals() (uint64, uint64) +# SendTransaction(tx *wire.MsgTx) error +# UpdatePeerHeights(latestBlkHash *chainhash.Hash, latestHeight int32, updateSource *ServerPeer) +# ChainParams() chaincfg.Params +# Start() error +# Stop() error +# IsCurrent() bool +# PeerByAddr(addr string) *ServerPeer +# ConnectedCount() int32 +# ConnectedPeers() (<-chan query.Peer, func(), error) +# OutboundGroupCount(key string) int +# AddedNodeInfo() []*ServerPeer +# Peers() []*ServerPeer +# DisconnectNodeByAddr(addr string) error +# DisconnectNodeByID(id int32) error +# RemoveNodeByAddr(addr string) error +# RemoveNodeByID(id int32) error +# ConnectNode(addr string, permanent bool) error +# ForAllPeers(closure func(sp *ServerPeer)) +# GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...QueryOption) (*gcs.Filter, error) +# GetBlock(blockHash chainhash.Hash, options ...QueryOption) (*btcutil.Block, error) +# GetUtxo(options ...RescanOption) (*SpendReport, error) diff --git a/decred/decred/btc/btcwallet/golink.py b/decred/decred/btc/btcwallet/golink.py new file mode 100644 index 00000000..39576647 --- /dev/null +++ b/decred/decred/btc/btcwallet/golink.py @@ -0,0 +1,80 @@ +import atexit +import ctypes +import json +import logging +import os +import platform +from typing import Any, Dict, List, Union, Callable + +log = logging.getLogger("GOBRIDGE") + +JSONType = Union[str, int, float, bool, None, Dict[str, Any], List[Any]] + +FileDir = os.path.dirname(os.path.realpath(__file__)) + +WINDOWS = platform.system() == "Windows" +LIB_EXT = "dll" if WINDOWS else "so" + +devLibPath = os.path.join(FileDir, "libbtcwallet", "libbtcwallet."+LIB_EXT) + +if os.path.isfile(devLibPath): + lib = ctypes.cdll.LoadLibrary(devLibPath) +else: + lib = ctypes.cdll.LoadLibrary("libbtcwallet."+LIB_EXT) + + +goFreeCharPtr = lib.FreeCharPtr +goFreeCharPtr.argtypes = [ctypes.c_char_p] + +goCall = lib.Call +goCall.restype = ctypes.c_void_p +goCall.argtypes = [ctypes.c_char_p] + + +def Go(funcName: str, params: JSONType) -> JSONType: + b = GoRaw(funcName, params) + return json.loads(b or 'true') # empty response indicates success + + +def GoRaw(funcName: str, params: JSONType) -> bytes: + b = json.dumps(dict( + function=funcName, + params=params, + )) + r = goCall(b.encode("utf-8")) + try: + return ctypes.cast(r, ctypes.c_char_p).value + except Exception as e: + log.error("Go error: %s", e) + finally: + goFreeCharPtr(ctypes.cast(r, ctypes.c_char_p)) + + +def delink(): + Go("exit", "") + + +feeders = [] + +logFeedID = 0 + + +def registerFeeder(f: Callable[[bytes], None]): + feeders.append(f) + + +@ctypes.CFUNCTYPE(None, ctypes.c_char_p) +def feedRelay(msgB: bytes): + msg = json.loads(msgB) + feedID = msg["feedID"] + if feedID == logFeedID: + log.info(msg["payload"]) + return + for feeder in feeders: + feeder(msg) + + +lib.Feed(feedRelay) + +# Extra 'exit' calls are free, so call it prodigiously. +atexit.register(delink) diff --git a/decred/decred/btc/btcwallet/libbtcwallet/build-nix.sh b/decred/decred/btc/btcwallet/libbtcwallet/build-nix.sh new file mode 100644 index 00000000..aa6957a9 --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/build-nix.sh @@ -0,0 +1 @@ +go build -buildmode=c-shared -o libbtcwallet.so \ No newline at end of file diff --git a/decred/decred/btc/btcwallet/libbtcwallet/build-win.sh b/decred/decred/btc/btcwallet/libbtcwallet/build-win.sh new file mode 100644 index 00000000..69868e9d --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/build-win.sh @@ -0,0 +1 @@ +go build -buildmode=c-shared -o libbtcwallet.dll \ No newline at end of file diff --git a/decred/decred/btc/btcwallet/libbtcwallet/go.mod b/decred/decred/btc/btcwallet/libbtcwallet/go.mod new file mode 100644 index 00000000..80fcc2c7 --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/go.mod @@ -0,0 +1,15 @@ +module example.com/libbtcwallet + +go 1.13 + +require ( + github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728 + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d + github.com/btcsuite/btcwallet v0.11.1-0.20210217230627-39cbb7bdd98a + github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 + github.com/btcsuite/btcwallet/walletdb v1.3.4 + github.com/btcsuite/btcwallet/wtxmgr v1.2.0 + github.com/decred/dcrd/chaincfg/v3 v3.0.0 + github.com/lightninglabs/neutrino v0.11.0 +) diff --git a/decred/decred/btc/btcwallet/libbtcwallet/go.sum b/decred/decred/btc/btcwallet/libbtcwallet/go.sum new file mode 100644 index 00000000..7a50454e --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/go.sum @@ -0,0 +1,371 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +decred.org/cspp v0.2.0/go.mod h1:KVnB49sueBFCldRa/ivZCaWZbrPNEiXWwxHCf1jTYKI= +decred.org/cspp v0.3.0 h1:2AkSsWzA7HIMZImfw0gT82Gdp8OXIM4NsBn7vna22uE= +decred.org/cspp v0.3.0/go.mod h1:UygjYilC94dER3BEU65Zzyoqy9ngJfWCD2rdJqvUs2A= +decred.org/dcrdex v0.1.5 h1:jVBQusvdH+kp0O5xGH82sD+XIbg4UG8OjLvefXDILwE= +decred.org/dcrdex v0.1.5/go.mod h1:1Ka70gzWkj9Dzx2srGoc2enTbhPJ990SoUb/OoFi1xg= +decred.org/dcrwallet v1.6.0 h1:AyyarDNewxOEXPB8CmXioD7Dk3x6omG1hVbE9Hil9CY= +decred.org/dcrwallet/v2 v2.0.0-20210212190322-d4196f151e05 h1:pmsaZKmk9nKM0z0zCbykBppCtpXMPHfbDq/XUVpgQ0g= +decred.org/dcrwallet/v2 v2.0.0-20210212190322-d4196f151e05/go.mod h1:58C+PbYDcrNIOBa8X0yY4R6POuy86eWxJTTCzEchf0E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728 h1:kF1MN22IdIZ1I7VMgS5WuihFuYaWOoP69Btm4bLUBxY= +github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.20.1-beta.0.20200615134404-e4f59022a387/go.mod h1:Yktc19YNjh/Iz2//CX0vfRTS4IJKM/RKO5YZ9Fn+Pgo= +github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M= +github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= +github.com/btcsuite/btcwallet v0.11.0 h1:XhwqdhEchy5a0q6R+y3F82roD2hYycPCHovgNyJS08w= +github.com/btcsuite/btcwallet v0.11.0/go.mod h1:qtPAohN1ioo0pvJt/j7bZM8ANBWlYWVCVFL0kkijs7s= +github.com/btcsuite/btcwallet v0.11.1-0.20201119022810-664f77ded195 h1:QjMLhLVZlpMjgWH5FK0dvwcXAhO2/80g4opgUDa9mAQ= +github.com/btcsuite/btcwallet v0.11.1-0.20201119022810-664f77ded195/go.mod h1:owv9oZqM0HnUW+ByF7VqOgfs2eb0ooiePW/+Tl/i/Nk= +github.com/btcsuite/btcwallet v0.11.1-0.20210217230627-39cbb7bdd98a h1:Nl3xXfJ5fJdxUTh60Ox/bynb/73291jnrF9whbQD3kg= +github.com/btcsuite/btcwallet v0.11.1-0.20210217230627-39cbb7bdd98a/go.mod h1:P1U4LKSB/bhFQdOM7ab1XqNoBGFyFAe7eKObEBD9mIo= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/walletdb v1.0.0 h1:mheT7vCWK5EP6rZzhxsQ7ms9+yX4VE8bwiJctECBeNw= +github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/walletdb v1.3.2/go.mod h1:GZCMPNpUu5KE3ASoVd+k06p/1OW8OwNGCCaNWRto2cQ= +github.com/btcsuite/btcwallet/walletdb v1.3.3 h1:u6e7vRIKBF++cJy+hOHaMGg+88ZTwvpaY27AFvtB668= +github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/walletdb v1.3.4 h1:ExdPQSfYRLoYMEENsjWyl4w0PePLm9w3wg69nsRS2xc= +github.com/btcsuite/btcwallet/walletdb v1.3.4/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0 h1:aIHgViEmZmZfe0tQQqF1xyd2qBqFWxX5vZXkkbjtbeA= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= +github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA= +github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= +github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/decred/base58 v1.0.0/go.mod h1:LLY1p5e3g91byL/UO1eiZaYd+uRoVRarybgcoymu9Ks= +github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c= +github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= +github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= +github.com/decred/dcrd v1.3.0 h1:EEXm7BdiROfazDtuFsOu9mfotnyy00bgCuVwUqaszFo= +github.com/decred/dcrd/addrmgr v1.2.0/go.mod h1:QlZF9vkzwYh0qs25C76SAFZBRscjETga/K28GEE6qIc= +github.com/decred/dcrd/blockchain/stake/v2 v2.0.0/go.mod h1:jv/rKMcZ87lhvVkHot/tElxeAYEUJ3mnKPHJ7WPq86U= +github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4= +github.com/decred/dcrd/blockchain/stake/v4 v4.0.0-20210129192908-660d0518b4cf h1:oj/l33YM35vDT55tkiDfv02eMxL01sYxdYiLB4YptVY= +github.com/decred/dcrd/blockchain/stake/v4 v4.0.0-20210129192908-660d0518b4cf/go.mod h1:zALtZt59lCrhoj6dVMptHHAMw1hq0Zz9s2ZULWjhtZs= +github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE= +github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0 h1:9gUuH0u/IZNPWBK9K3CxgAWPG7nTqVSsZefpGY4Okns= +github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0/go.mod h1:t2qaZ3hNnxHZ5kzVJDgW5sp47/8T5hYJt7SR+/JtRhI= +github.com/decred/dcrd/blockchain/v2 v2.1.0/go.mod h1:DBmX26fUDTQocIozF44Ydo5+m+QzaC6aMYMBFFsCOJs= +github.com/decred/dcrd/blockchain/v4 v4.0.0-20210129200153-14fd1a785bf2/go.mod h1:AwWyfS769nQVXTbfwrSVsAMAwkpoU1N//6XYkYwXJUU= +github.com/decred/dcrd/certgen v1.1.0/go.mod h1:ivkPLChfjdAgFh7ZQOtl6kJRqVkfrCq67dlq3AbZBQE= +github.com/decred/dcrd/certgen v1.1.1/go.mod h1:ivkPLChfjdAgFh7ZQOtl6kJRqVkfrCq67dlq3AbZBQE= +github.com/decred/dcrd/chaincfg v1.5.1 h1:u1Xbq0VTnAXIHW5ECqrWe0VYSgf5vWHqpSiwoLBzxAQ= +github.com/decred/dcrd/chaincfg v1.5.1/go.mod h1:FukMzTjkwzjPU+hK7CqDMQe3NMbSZAYU5PAcsx1wlv0= +github.com/decred/dcrd/chaincfg v1.5.2 h1:dd6l9rqcpxg2GF5neBmE2XxRc5Lqda45fWmN4XOJRW8= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1/go.mod h1:OVfvaOsNLS/A1y4Eod0Ip/Lf8qga7VXCQjUQLbkY0Go= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/chaincfg/v2 v2.0.2/go.mod h1:hpKvhLCDAD/xDZ3V1Pqpv9fIKVYYi11DyxETguazyvg= +github.com/decred/dcrd/chaincfg/v2 v2.1.0/go.mod h1:hpKvhLCDAD/xDZ3V1Pqpv9fIKVYYi11DyxETguazyvg= +github.com/decred/dcrd/chaincfg/v2 v2.3.0/go.mod h1:7qUJTvn+y/kswSRZ4sT2+EmvlDTDyy2InvNFtX/hxk0= +github.com/decred/dcrd/chaincfg/v3 v3.0.0 h1:+TFbu7ZmvBwM+SZz5mrj6cun9ts/6DAL5sqnsaFBHGQ= +github.com/decred/dcrd/chaincfg/v3 v3.0.0/go.mod h1:EspyubQ7D2w6tjP7rBGDIE7OTbuMgBjR2F2kZFnh31A= +github.com/decred/dcrd/connmgr/v3 v3.0.0/go.mod h1:cPI43Aggp1lOhrVG75eJ3c3BwuFx0NhT77FK34ky+ak= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/ripemd160 v1.0.0/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= +github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc= +github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= +github.com/decred/dcrd/database/v2 v2.0.0/go.mod h1:Sj2lvTRB0mfSu9uD7ObfwCY/eJ954GFU/X+AndJIyfE= +github.com/decred/dcrd/database/v2 v2.0.1/go.mod h1:ZOaWTv3IlNqCA+y7q3q5EozgmiDOmNwCSq3ntZn2CDo= +github.com/decred/dcrd/database/v2 v2.0.3-0.20210129190127-4ebd135a82f1 h1:+oUVvEK/+TQeJqJs0bbnVcs2IvFkL4Z8nIKupeFDV3A= +github.com/decred/dcrd/database/v2 v2.0.3-0.20210129190127-4ebd135a82f1/go.mod h1:C5nb1qImTy2sxAfV1KJFW6KHae+NbD6lSMJl58KY7XM= +github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= +github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= +github.com/decred/dcrd/dcrec/edwards v1.0.0 h1:UDcPNzclKiJlWqV3x1Fl8xMCJrolo4PB4X9t8LwKDWU= +github.com/decred/dcrd/dcrec/edwards v1.0.0/go.mod h1:HblVh1OfMt7xSxUL1ufjToaEvpbjpWvvTAUx4yem8BI= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHuvLQsj25wd7Wb4= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.1/go.mod h1:lhu4eZFSfTJWUnR3CFRcpD+Vta0KUAqnhTsTksHXgy0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 h1:awk7sYJ4pGWmtkiGHFfctztJjHMKGLV8jctGQhAbKe0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2/go.mod h1:CHTUIVfmDDd0KFVFpNX1pFVCBUegxW387nN0IGwNKR0= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= +github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw= +github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210127014238-b33b46cf1a24 h1:L718+uXQkWq02GqdBdEAUIbBPh/Il/kudTsOEf0pItQ= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210127014238-b33b46cf1a24/go.mod h1:UkVqoxmJlLgUvBjJD+GdJz6mgdSdf3UjX83xfwUAYDk= +github.com/decred/dcrd/dcrjson/v3 v3.0.1/go.mod h1:fnTHev/ABGp8IxFudDhjGi9ghLiXRff1qZz/wvq12Mg= +github.com/decred/dcrd/dcrjson/v3 v3.1.0 h1:Y2VjCXCNWbNIa52wMKEuNiU+9rUgnjYb5c1JQW6PuzM= +github.com/decred/dcrd/dcrjson/v3 v3.1.0/go.mod h1:fnTHev/ABGp8IxFudDhjGi9ghLiXRff1qZz/wvq12Mg= +github.com/decred/dcrd/dcrutil v1.3.0 h1:LtKIiDnq925yJT/4OpIKKiU9/WaxfD9LfhxrpLSi0Qs= +github.com/decred/dcrd/dcrutil v1.3.0/go.mod h1:7fUT70QAarhDwQK62g92uDbbYpjXlXngpy5RBiecufo= +github.com/decred/dcrd/dcrutil/v2 v2.0.0/go.mod h1:gUshVAXpd51DlcEhr51QfWL2HJGkMDM1U8chY+9VvQg= +github.com/decred/dcrd/dcrutil/v2 v2.0.1/go.mod h1:JdEgF6eh0TTohPeiqDxqDSikTSvAczq0J7tFMyyeD+k= +github.com/decred/dcrd/dcrutil/v3 v3.0.0/go.mod h1:iVsjcqVzLmYFGCZLet2H7Nq+7imV9tYcuY+0lC2mNsY= +github.com/decred/dcrd/dcrutil/v4 v4.0.0-20210129181600-6ae0142d3b28 h1:KrZXi4s+2wyCkjREQMkoT+C4eB4yVLUU/Xtos+8n7Sc= +github.com/decred/dcrd/dcrutil/v4 v4.0.0-20210129181600-6ae0142d3b28/go.mod h1:xe59jKcMx5G/dbRmsZ8+FzY+WQDE/7YBP3k3uzJTtmI= +github.com/decred/dcrd/gcs v1.1.0/go.mod h1:yBjhj217Vw5lw3aKnCdHip7fYb9zwMos8bCy5s79M9w= +github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o= +github.com/decred/dcrd/gcs/v3 v3.0.0-20210129195202-a4265d63b619 h1:YEx0oEkwh9uBPVzzZTkemyKieBpQwmHSI+BdW0VHoAA= +github.com/decred/dcrd/gcs/v3 v3.0.0-20210129195202-a4265d63b619/go.mod h1:aGuAajYbDJB2oal17G371wiosGgVCc5d5FlT2EwZtoE= +github.com/decred/dcrd/hdkeychain/v2 v2.1.0/go.mod h1:DR+lD4uV8G0i3c9qnUJwjiGaaEWK+nSrbWCz1BRHBL8= +github.com/decred/dcrd/hdkeychain/v3 v3.0.0 h1:hOPb4c8+K6bE3a/qFtzt2Z2yzK4SpmXmxvCTFp8vMxI= +github.com/decred/dcrd/hdkeychain/v3 v3.0.0/go.mod h1:Vz7PJSlLzhqmOR2lmjGD9JqAZgmUnM8P6r8hg7U4Zho= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/decred/dcrd/lru v1.1.0 h1:QwT6v8LFKOL3xQ3qtucgRk4pdiawrxIfCbUXWpm+JL4= +github.com/decred/dcrd/lru v1.1.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/decred/dcrd/rpc/jsonrpc/types v1.0.1/go.mod h1:dJUp9PoyFYklzmlImpVkVLOr6j4zKuUv66YgemP2sd8= +github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c= +github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.1.0/go.mod h1:krn89ZOgSa8yc7sA4WpDK95p61NnjNWFkNlMnGrKbMc= +github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0-20210129200153-14fd1a785bf2 h1:JB8FF348nqY9LAcy2JgmFrpkcfZ402uyHX6+W5TUCWk= +github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0-20210129200153-14fd1a785bf2/go.mod h1:9izQEJ5wU0ZwYHESMaaOIvE6H6y3IvDsQL3ByYGn9oc= +github.com/decred/dcrd/rpcclient/v5 v5.0.0/go.mod h1:lg7e2kpulSpynHkS2JXJ+trQ4PWHaHLQcp/Q0eSIvBc= +github.com/decred/dcrd/txscript v1.1.0 h1:MwkLXdc4Yq83oeNNEQJdlBTkNlorKXn8Nd5W2JXyMZg= +github.com/decred/dcrd/txscript v1.1.0/go.mod h1:gbcq6gpGfKddPmZSKp+17ils2cLzUqHopXf8H5rCY7Y= +github.com/decred/dcrd/txscript/v2 v2.0.0/go.mod h1:WStcyYYJa+PHJB4XjrLDRzV96/Z4thtsu8mZoVrU6C0= +github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m8GRJLz24SDBKOi0= +github.com/decred/dcrd/txscript/v3 v3.0.0/go.mod h1:pdvnlD4KGdDoc09cvWRJ8EoRQUaiUz41uDevOWuEfII= +github.com/decred/dcrd/txscript/v4 v4.0.0-20210129190127-4ebd135a82f1 h1:dzmyfANMqtSWmnu2FQcgG5vicV0otdkNDJ5c1sWxu8o= +github.com/decred/dcrd/txscript/v4 v4.0.0-20210129190127-4ebd135a82f1/go.mod h1:EnS4vtxTESoI59geLo9M8AUOvIprJy+O4gSVsQp6/h4= +github.com/decred/dcrd/wire v1.2.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM= +github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM= +github.com/decred/dcrd/wire v1.4.0 h1:KmSo6eTQIvhXS0fLBQ/l7hG7QLcSJQKSwSyzSqJYDk0= +github.com/decred/dcrd/wire v1.4.0/go.mod h1:WxC/0K+cCAnBh+SKsRjIX9YPgvrjhmE+6pZlel1G7Ro= +github.com/decred/dcrwallet/deployments/v2 v2.0.0/go.mod h1:fY1HV1vIeeY5bHjrMknUhB/ZOVIfthBiUlSgRqFFKrg= +github.com/decred/dcrwallet/errors/v2 v2.0.0/go.mod h1:2HYvtRuCE9XqDNCWhKmBuzLG364xUgcUIsJu02r0F5Q= +github.com/decred/dcrwallet/rpc/client/dcrd v1.0.0/go.mod h1:qrJri+p+cn+obQ8nkW5hTtagPcOnCqKPGBq1t02gBc0= +github.com/decred/dcrwallet/rpc/jsonrpc/types v1.3.0/go.mod h1:Xvekb43GtfMiRbyIY4ZJ9Uhd9HRIAcnp46f3q2eIExU= +github.com/decred/dcrwallet/rpc/jsonrpc/types v1.4.0/go.mod h1:Xvekb43GtfMiRbyIY4ZJ9Uhd9HRIAcnp46f3q2eIExU= +github.com/decred/dcrwallet/validate v1.1.1/go.mod h1:T++tlVcCOh2oSrEq4r5CKCvmftaQdq9uZwO7jSNYZaw= +github.com/decred/dcrwallet/wallet/v3 v3.2.1/go.mod h1:SJ+++gtMdcUeqMv6iIO3gVGlGJfM+4iY2QSaAakhbUw= +github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U= +github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= +github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtAonQ= +github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4= +github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7 h1:Ug59miTxVKVg5Oi2S5uHlKOIV5jBx4Hb2u0jIxxDaSs= +github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/bitset v1.0.0 h1:Ws0PXV3PwXqWK2n7Vz6idCdrV/9OrBXgHEJi27ZB9Dw= +github.com/jrick/bitset v1.0.0/go.mod h1:ZOYB5Uvkla7wIEY4FEssPVi3IQXa02arznRaYaAEPe4= +github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/jrick/wsrpc/v2 v2.0.0/go.mod h1:naH/fojac6vQWYgAA0e7b9TX/bShsWoVL7CwrdvFmUk= +github.com/jrick/wsrpc/v2 v2.2.0/go.mod h1:naH/fojac6vQWYgAA0e7b9TX/bShsWoVL7CwrdvFmUk= +github.com/jrick/wsrpc/v2 v2.3.2/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqYTwDPfU= +github.com/jrick/wsrpc/v2 v2.3.4 h1:+GzRtp/TyXaSB61pN92lIAVyvdVv0RSqniIEB/rPx1Q= +github.com/jrick/wsrpc/v2 v2.3.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqYTwDPfU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/neutrino v0.11.0 h1:lPpYFCtsfJX2W5zI4pWycPmbbBdr7zU+BafYdLoD6k0= +github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg= +github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo= +github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= +github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= +github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= +github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= +github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs= +go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 h1:mBVYJnbrXLA/ZCBTCe7PtEgAUP+1bg92qTaFoPHdz+8= +google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/decred/decred/btc/btcwallet/libbtcwallet/golink.go b/decred/decred/btc/btcwallet/libbtcwallet/golink.go new file mode 100644 index 00000000..e702c6ce --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/golink.go @@ -0,0 +1,196 @@ +package main + +/* +#include + +typedef void (*pyfunc) (char *); + +static inline void call_py_func(pyfunc ptr, char *b) { + (ptr)(b); +} +*/ +import "C" +import ( + "encoding/json" + "fmt" + "os" + "sync" + "sync/atomic" + "unsafe" + + "github.com/btcsuite/btclog" +) + +// CallData is the type sent for all golink calls. +type CallData struct { + Function string `json:"function"` + Params json.RawMessage `json:"params"` +} + +func callError(s string, a ...interface{}) *C.char { + b, _ := json.Marshal(&struct { + Error string `json:"error"` + }{ + Error: fmt.Sprintf(s, a...), + }) + return C.CString(string(b)) +} + +var ( + wllt *Wallet + wlltMtx sync.RWMutex +) + +func theWallet() *Wallet { + wlltMtx.RLock() + defer wlltMtx.RUnlock() + return wllt +} + +var utilityFuncs = map[string]walletRouter{ + "walletExists": walletExistsUtility, + "createWallet": createWalletUtility, + "init": initUtility, + "exit": exitUtility, +} + +// Call is used to invoke a registered function. +//export Call +func Call(msg *C.char, msgLen C.int) *C.char { + jsonStr := C.GoString(msg) + cd := new(CallData) + err := json.Unmarshal([]byte(jsonStr), cd) + if err != nil { + return callError("json Unmarshal error: %v", err) + } + + f, ok := utilityFuncs[cd.Function] + if ok { + s, err := f(cd.Params) + if err != nil { + return callError("%s error: %v", cd.Function, err) + } + return C.CString(s) + } + + w := theWallet() + + if w == nil { + return callError("wallet not initialized") + } + + f = w.router(cd.Function) + if f == nil { + return callError("no function %q", cd.Function) + } + s, err := f(cd.Params) + if err != nil { + return callError("%s error: %v", cd.Function, err) + } + return C.CString(s) +} + +var feeders = map[C.pyfunc]struct{}{} + +// Feed allows the user to subscribe a function to receive asynchronous +// updates like log messages and streaming notifications. +//export Feed +func Feed(fn C.pyfunc) { + if theWallet() != nil { + panic("do not register golink Feed after the wallet is initialized") + } + feeders[fn] = struct{}{} +} + +// FreeCharPtr frees the memory associated with a *C.char. +//export FreeCharPtr +func FreeCharPtr(b *C.char) { + C.free(unsafe.Pointer(b)) +} + +const ( + logFeedID uint32 = iota + walletFeedID +) + +type feedMessage struct { + FeedID uint32 `json:"feedID"` + Subject string `json:"subject"` + Payload interface{} `json:"payload"` +} + +var feedChan = make(chan *feedMessage, 16) +var inited uint32 + +type initParams struct { + walletInitParams + LogLevel uint32 `json:"logLevel"` +} + +func initUtility(raw json.RawMessage) (string, error) { + + if !atomic.CompareAndSwapUint32(&inited, 0, 1) || theWallet() != nil { + return "", fmt.Errorf("already initialized") + } + + init := new(initParams) + err := json.Unmarshal(raw, init) + if err != nil { + return "", err + } + + w, err := newWallet(&init.walletInitParams) + if err != nil { + return "", fmt.Errorf("wallet init error: %v", err) + } + + wlltMtx.Lock() + wllt = w + wlltMtx.Unlock() + + // Just log to stdout for now. I thought sending logs + // through the feed would be a good idea, but now I'm thinking + // a separate log file and stdout when available. + // initializeLogging(btclog.Level(init.LogLevel)) + + go func() { + for { + select { + case msg := <-feedChan: + for feeder := range feeders { + msgB, err := json.Marshal(msg) + if err != nil { + log.Errorf("JSON Marshal error: %v", err) + continue + } + cStr := C.CString(string(msgB)) + C.call_py_func(feeder, cStr) + FreeCharPtr(cStr) + } + case <-w.ctx.Done(): + return + } + } + }() + + return `true`, nil +} + +var isShutdown uint32 + +func exitUtility(_ json.RawMessage) (string, error) { + w := theWallet() + if !atomic.CompareAndSwapUint32(&isShutdown, 0, 1) || w == nil { + return "", nil + } + w.Stop() + w.WaitForShutdown() + w.shutdown() // cancel the context + return "", nil +} + +func main() {} + +func init() { + useLogBackend(btclog.NewBackend(os.Stdout), btclog.LevelInfo) +} diff --git a/decred/decred/btc/btcwallet/libbtcwallet/interface.go b/decred/decred/btc/btcwallet/libbtcwallet/interface.go new file mode 100644 index 00000000..2dac0d5a --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/interface.go @@ -0,0 +1,89 @@ +package main + +import ( + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +type btcWallet interface { + MakeMultiSigScript(addrs []btcutil.Address, nRequired int) ([]byte, error) + ImportP2SHRedeemScript(script []byte) (*btcutil.AddressScriptHash, error) + // FundPsbt(packet *psbt.Packet, account uint32, feeSatPerKB btcutil.Amount) (int32, error) + // FinalizePsbt(packet *psbt.Packet) error + // SubmitRescan(job *RescanJob) <-chan error + // Rescan(addrs []btcutil.Address, unspent []wtxmgr.Credit) error + // ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut, inputIndex int, + // sigHashes *txscript.TxSigHashes, hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness, []byte, error) + UnspentOutputs(policy wallet.OutputSelectionPolicy) ([]*wallet.TransactionOutput, error) + // FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, int64, error) + Start() + // SynchronizeRPC(chainClient chain.Interface) + // ChainClient() chain.Interface + Stop() + ShuttingDown() bool + WaitForShutdown() + SynchronizingToNetwork() bool + ChainSynced() bool + SetChainSynced(synced bool) + CreateSimpleTx(account uint32, outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount, dryRun bool) (*txauthor.AuthoredTx, error) + Unlock(passphrase []byte, lock <-chan time.Time) error + Lock() + Locked() bool + ChangePrivatePassphrase(old, new []byte) error + ChangePublicPassphrase(old, new []byte) error + ChangePassphrases(publicOld, publicNew, privateOld, privateNew []byte) error + AccountAddresses(account uint32) (addrs []btcutil.Address, err error) + CalculateBalance(confirms int32) (btcutil.Amount, error) + CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) + CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) + PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) + LabelTransaction(hash chainhash.Hash, label string, overwrite bool) error + PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) + HaveAddress(a btcutil.Address) (bool, error) + AccountOfAddress(a btcutil.Address) (uint32, error) + AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress, error) + AccountNumber(scope waddrmgr.KeyScope, accountName string) (uint32, error) + AccountName(scope waddrmgr.KeyScope, accountNumber uint32) (string, error) + AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) + RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error + NextAccount(scope waddrmgr.KeyScope, name string) (uint32, error) + ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) + ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) + ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjson.ListTransactionsResult, error) + ListAllTransactions() ([]btcjson.ListTransactionsResult, error) + // GetTransactions(startBlock, endBlock *BlockIdentifier, cancel <-chan struct{}) (*GetTransactionsResult, error) + Accounts(scope waddrmgr.KeyScope) (*wallet.AccountsResult, error) + AccountBalances(scope waddrmgr.KeyScope, requiredConfs int32) ([]wallet.AccountBalanceResult, error) + ListUnspent(minconf, maxconf int32, addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) + DumpPrivKeys() ([]string, error) + DumpWIFPrivateKey(addr btcutil.Address) (string, error) + ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF, bs *waddrmgr.BlockStamp, rescan bool) (string, error) + LockedOutpoint(op wire.OutPoint) bool + LockOutpoint(op wire.OutPoint) + UnlockOutpoint(op wire.OutPoint) + ResetLockedOutpoints() + LockedOutpoints() []btcjson.TransactionInput + LeaseOutput(id wtxmgr.LockID, op wire.OutPoint) (time.Time, error) + ReleaseOutput(id wtxmgr.LockID, op wire.OutPoint) error + SortedActivePaymentAddresses() ([]string, error) + NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) + NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) + TotalReceivedForAccounts(scope waddrmgr.KeyScope, minConf int32) ([]wallet.AccountTotalReceivedResult, error) + TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) + SendOutputs(outputs []*wire.TxOut, account uint32, minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) + SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte, + additionalKeysByAddress map[string]*btcutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error) + PublishTransaction(tx *wire.MsgTx, label string) error + // ChainParams() *chaincfg.Params + // Database() walletdb.DB +} diff --git a/decred/decred/btc/btcwallet/libbtcwallet/libbtcwallet.go b/decred/decred/btc/btcwallet/libbtcwallet/libbtcwallet.go new file mode 100644 index 00000000..ca564f77 --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/libbtcwallet.go @@ -0,0 +1,1556 @@ +package main + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/neutrino" +) + +type walletRouter func(json.RawMessage) (string, error) + +func encode(thing interface{}) (string, error) { + b, err := json.Marshal(thing) + if err != nil { + return "", err + } + return string(b), nil +} + +func serializeMsgTx(tx *wire.MsgTx) ([]byte, error) { + var buf bytes.Buffer + err := tx.Serialize(&buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func convertByteSliceSlice(inB [][]byte) []Bytes { + outB := make([]Bytes, 0, len(inB)) + for _, b := range inB { + outB = append(outB, b) + } + return outB +} + +func convertAmounts(inAmts []btcutil.Amount) []int64 { + outAmts := make([]int64, 0, len(inAmts)) + for _, amt := range inAmts { + outAmts = append(outAmts, int64(amt)) + } + return outAmts +} + +func zero(b []byte) { + for i := range b { + b[i] = 0 + } +} + +func outpointFromJSON(raw json.RawMessage) (*wire.OutPoint, error) { + var op hashIndex + err := json.Unmarshal(raw, &op) + if err != nil { + return nil, err + } + + h := chainhash.Hash{} + err = h.SetBytes(op.Hash) + if err != nil { + return nil, err + } + return wire.NewOutPoint(&h, uint32(op.Index)), nil +} + +func parseNet(netName string) (*chaincfg.Params, error) { + switch netName { + case chaincfg.MainNetParams.Name: + return &chaincfg.MainNetParams, nil + case chaincfg.TestNet3Params.Name: + return &chaincfg.TestNet3Params, nil + case chaincfg.SimNetParams.Name: + return &chaincfg.SimNetParams, nil + case chaincfg.RegressionNetParams.Name: + return &chaincfg.RegressionNetParams, nil + } + return nil, fmt.Errorf("net %s not known", netName) +} + +type walletSpecs struct { + Net string `json:"net"` + Dir string `json:"dir"` +} + +type walletInitParams struct { + walletSpecs + Test bool `json:"test"` + ConnectPeers []string `json:"connectPeers"` +} + +// Wallet wraps *wallet.Wallet and translates routed calls. +type Wallet struct { + btcWallet + ctx context.Context + shutdown context.CancelFunc + neutrino *neutrino.ChainService + params *chaincfg.Params + handlers map[string]func(json.RawMessage) (string, error) +} + +func newWallet(init *walletInitParams) (*Wallet, error) { + params, err := parseNet(init.Net) + if err != nil { + return nil, err + } + + ctx, shutdown := context.WithCancel(context.Background()) + + var wI btcWallet + var chainService *neutrino.ChainService + if init.Test { + wI, err = newTestWallet(init.Dir) + if err != nil { + return nil, err + } + } else { + var btcw *wallet.Wallet + btcw, chainService, err = loadWallet(&walletConfig{ + DBDir: init.Dir, + Net: params, + ConnectPeers: init.ConnectPeers, + }) + if err != nil { + return nil, err + } + + wI = btcw + go notesLoop(ctx, btcw) + } + + w := &Wallet{ + btcWallet: wI, + ctx: ctx, + shutdown: shutdown, + neutrino: chainService, + params: params, + } + w.prepHandlers() + + return w, nil +} + +func (w *Wallet) decodeJSONAddr(raw json.RawMessage) (btcutil.Address, error) { + var addrStr string + err := json.Unmarshal(raw, &addrStr) + if err != nil { + return nil, err + } + return btcutil.DecodeAddress(addrStr, w.params) +} + +// func (w *Wallet) MakeMultiSigScript(addrs []btcutil.Address, nRequired int) ([]byte, error) +type makeMultiSigScriptParams struct { + Addrs []string `json:"addrs"` + NRequired int `json:"nRequired"` +} + +func (w *Wallet) makeMultiSigScript(raw json.RawMessage) (string, error) { + params := new(makeMultiSigScriptParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + addrs := make([]btcutil.Address, 0, len(params.Addrs)) + for _, s := range params.Addrs { + addr, err := btcutil.DecodeAddress(s, w.params) + if err != nil { + return "", err + } + addrs = append(addrs, addr) + } + script, err := w.MakeMultiSigScript(addrs, params.NRequired) + if err != nil { + return "", err + } + return encode(hex.EncodeToString(script)) +} + +// func (w *Wallet) ImportP2SHRedeemScript(script []byte) (*btcutil.AddressScriptHash, error) +func (w *Wallet) importP2SHRedeemScript(scriptHexB json.RawMessage) (string, error) { + var script Bytes + err := json.Unmarshal(scriptHexB, &script) + if err != nil { + return "", err + } + + addr, err := w.ImportP2SHRedeemScript(script) + if err != nil { + return "", err + } + return encode(addr.String()) +} + +// TODO +// func (w *Wallet) SubmitRescan(job *RescanJob) <-chan error + +// TODO +// func (w *Wallet) Rescan(addrs []btcutil.Address, unspent []wtxmgr.Credit) error + +// func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOutput, error +type outputSelectionPolicy struct { + Account uint32 `json:"account"` + RequiredConfirmations int32 `json:"requiredConfirmations"` +} + +type transactionOutput struct { + OutPoint hashIndex `json:"outPoint"` + Output scriptValue `json:"output"` + OutputKind byte `json:"outputKind"` + ContainingBlock hashIndex `json:"containingBlock"` + ReceiveTime int64 `json:"receiveTime"` +} + +func (w *Wallet) unspentOutputs(ospB json.RawMessage) (string, error) { + osp := new(outputSelectionPolicy) + err := json.Unmarshal(ospB, osp) + if err != nil { + return "", nil + } + + woutputs, err := w.UnspentOutputs(wallet.OutputSelectionPolicy{ + Account: osp.Account, + RequiredConfirmations: osp.RequiredConfirmations, + }) + + outputs := make([]*transactionOutput, 0, len(woutputs)) + for _, wto := range woutputs { + + outputs = append(outputs, &transactionOutput{ + OutPoint: hashIndex{ + Hash: wto.OutPoint.Hash[:], + Index: int64(wto.OutPoint.Index), + }, + Output: scriptValue{ + Script: wto.Output.PkScript, + Value: int64(wto.Output.Value), + }, + OutputKind: byte(wto.OutputKind), + ContainingBlock: hashIndex{ + Hash: wto.ContainingBlock.Hash[:], + Index: int64(wto.ContainingBlock.Height), + }, + ReceiveTime: wto.ReceiveTime.Unix(), + }) + } + + return encode(outputs) +} + +// func (w *Wallet) Start() // called by loader.OpenExistingWallet +func (w *Wallet) start(_ json.RawMessage) (string, error) { + w.Start() + return "", nil +} + +// INTERNAL USE ONLY +// // SynchronizeRPC docs say the API is unstable, but they still use it and so do +// // we. +// func (w *Wallet) SynchronizeRPC(chainClient chain.Interface) + +// INTERNAL USE ONLY +// func (w *Wallet) ChainClient() chain.Interface + +// func (w *Wallet) Stop() +func (w *Wallet) stop(_ json.RawMessage) (string, error) { + w.Stop() + return "", nil +} + +// Stop cancels the context before calling Stop on the embedded wallet. +func (w *Wallet) Stop() { + w.shutdown() + w.btcWallet.Stop() +} + +// func (w *Wallet) ShuttingDown() bool +func (w *Wallet) shuttingDown(_ json.RawMessage) (string, error) { + return encode(w.ShuttingDown()) +} + +// func (w *Wallet) WaitForShutdown() +func (w *Wallet) waitForShutdown(_ json.RawMessage) (string, error) { + w.WaitForShutdown() + return "", nil +} + +// func (w *Wallet) SynchronizingToNetwork() bool +func (w *Wallet) synchronizingToNetwork(_ json.RawMessage) (string, error) { + return encode(w.SynchronizingToNetwork()) +} + +// func (w *Wallet) ChainSynced() bool +func (w *Wallet) chainSynced(_ json.RawMessage) (string, error) { + return encode(w.ChainSynced()) +} + +// func (w *Wallet) SetChainSynced(synced bool) +func (w *Wallet) setChainSynced(syncedB json.RawMessage) (string, error) { + var synced bool + err := json.Unmarshal(syncedB, &synced) + if err != nil { + return "", err + } + w.SetChainSynced(synced) + return "", nil +} + +// func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount, dryRun bool)(*txauthor.AuthoredTx, error) { +type createSimpleTxParams struct { + Account uint32 `json:"account"` + Outputs []scriptValue `json:"outputs"` + MinConf int32 `json:"minconf"` + SatsPerKB float64 `json:"satPerKb"` + DryRun bool `json:"dryRun"` +} + +type authoredTx struct { + Tx Bytes `json:"tx"` + PrevScripts []Bytes `json:"prevScripts"` + PrevInputValues []int64 `json:"prevInputValues"` + TotalInput uint64 `json:"totalInput"` + ChangeIndex int `json:"changeIndex"` // negative if no change +} + +func (w *Wallet) createSimpleTx(raw json.RawMessage) (string, error) { + params := new(createSimpleTxParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + outputs := make([]*wire.TxOut, 0, len(params.Outputs)) + for _, txOut := range params.Outputs { + outputs = append(outputs, wire.NewTxOut(txOut.Value, txOut.Script)) + } + satsPerKB, err := btcutil.NewAmount(params.SatsPerKB) + if err != nil { + return "", err + } + + wTx, err := w.CreateSimpleTx(params.Account, outputs, params.MinConf, satsPerKB, params.DryRun) + if err != nil { + return "", err + } + + txB, err := serializeMsgTx(wTx.Tx) + if err != nil { + return "", err + } + + return encode(&authoredTx{ + Tx: txB, + PrevScripts: convertByteSliceSlice(wTx.PrevScripts), + PrevInputValues: convertAmounts(wTx.PrevInputValues), + TotalInput: uint64(int64(wTx.TotalInput)), + ChangeIndex: wTx.ChangeIndex, + }) +} + +// func (w *Wallet) Unlock(passphrase []byte, lock <-chan time.Time) error +type unlockParams struct { + Passphrase Bytes `json:"passphrase"` + Timeout int64 `json:"timeout"` +} + +func (w *Wallet) unlock(raw json.RawMessage) (string, error) { + params := new(unlockParams) + defer zero(params.Passphrase) + err := json.Unmarshal(raw, params) + if err != nil { + return "", nil + } + timeout := time.Second * time.Duration(params.Timeout) + return "", w.Unlock(params.Passphrase, time.After(timeout)) + +} + +// func (w *Wallet) Lock() +func (w *Wallet) lock(_ json.RawMessage) (string, error) { + w.Lock() + return "", nil +} + +// func (w *Wallet) Locked() bool +func (w *Wallet) locked(_ json.RawMessage) (string, error) { + return encode(w.Locked()) +} + +// func (w *Wallet) ChangePrivatePassphrase(old, new []byte) error +type changePassphraseParams struct { + New Bytes `json:"new"` + Old Bytes `json:"old"` +} + +func (w *Wallet) changePrivatePassphrase(raw json.RawMessage) (string, error) { + params := new(changePassphraseParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", nil + } + + defer zero(params.New) + defer zero(params.Old) + + return "", w.ChangePrivatePassphrase(params.Old, params.New) +} + +// func (w *Wallet) ChangePublicPassphrase(old, new []byte) error +func (w *Wallet) changePublicPassphrase(raw json.RawMessage) (string, error) { + params := new(changePassphraseParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", nil + } + defer zero(params.New) + defer zero(params.Old) + return "", w.ChangePublicPassphrase(params.Old, params.New) +} + +// func (w *Wallet) ChangePassphrases(publicOld, publicNew, privateOld, privateNew []byte) error +type changePassphrasesParams struct { + Public changePassphraseParams `json:"public"` + Private changePassphraseParams `json:"private"` +} + +func (w *Wallet) changePassphrases(raw json.RawMessage) (string, error) { + params := new(changePassphrasesParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", nil + } + defer zero(params.Public.New) + defer zero(params.Public.Old) + defer zero(params.Private.New) + defer zero(params.Private.Old) + return "", w.ChangePassphrases(params.Public.Old, params.Public.New, params.Private.Old, params.Private.New) +} + +// func (w *Wallet) AccountAddresses(account uint32) (addrs []btcutil.Address, err error) +func (w *Wallet) accountAddresses(raw json.RawMessage) (string, error) { + var account uint32 + err := json.Unmarshal(raw, &account) + if err != nil { + return "", err + } + addrs, err := w.AccountAddresses(account) + if err != nil { + return "", err + } + addrStrs := make([]string, 0, len(addrs)) + for _, addr := range addrs { + addrStrs = append(addrStrs, addr.String()) + } + return encode(addrStrs) +} + +// func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) +func (w *Wallet) calculateBalance(raw json.RawMessage) (string, error) { + var confirms int32 + err := json.Unmarshal(raw, &confirms) + if err != nil { + return "", err + } + amt, err := w.CalculateBalance(confirms) + if err != nil { + return "", err + } + return encode(int64(amt)) +} + +// func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balances, error) +type calculateAccountBalances struct { + Account uint32 `json:"account"` + Confirms int32 `json:"confirms"` +} + +type balances struct { + Total int64 `json:"total"` + Spendable int64 `json:"spendable"` + ImmatureReward int64 `json:"immatureReward"` +} + +func (w *Wallet) calculateAccountBalances(raw json.RawMessage) (string, error) { + params := new(calculateAccountBalances) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + bals, err := w.CalculateAccountBalances(params.Account, params.Confirms) + if err != nil { + return "", err + } + return encode(&balances{ + Total: int64(bals.Total), + Spendable: int64(bals.Spendable), + ImmatureReward: int64(bals.ImmatureReward), + }) +} + +// func (w *Wallet) CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) +type currentAddressParams struct { + Account uint32 `json:"account"` + Scope keyScope `json:"scope"` +} + +type keyScope struct { + Purpose uint32 `json:"purpose"` + Coin uint32 `json:"coin"` +} + +func (w *Wallet) currentAddress(raw json.RawMessage) (string, error) { + params := new(currentAddressParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + addr, err := w.CurrentAddress(params.Account, waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }) + if err != nil { + return "", err + } + return encode(addr.String()) +} + +// func (w *Wallet) PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) +func (w *Wallet) pubKeyForAddress(raw json.RawMessage) (string, error) { + addr, err := w.decodeJSONAddr(raw) + if err != nil { + return "", err + } + pubKey, err := w.PubKeyForAddress(addr) + if err != nil { + return "", err + } + return encode(hex.EncodeToString(pubKey.SerializeCompressed())) +} + +// func (w *Wallet) LabelTransaction(hash chainhash.Hash, label string, overwrite bool) error +type labelTransactionParams struct { + Hash Bytes `json:"hash"` + Label string `json:"label"` + Overwrite bool `json:"overwrite"` +} + +func (w *Wallet) labelTransaction(raw json.RawMessage) (string, error) { + params := new(labelTransactionParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + h := chainhash.Hash{} + err = h.SetBytes(params.Hash) + if err != nil { + return "", err + } + return "", w.LabelTransaction(h, params.Label, params.Overwrite) +} + +// func (w *Wallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) +func (w *Wallet) privKeyForAddress(raw json.RawMessage) (string, error) { + addr, err := w.decodeJSONAddr(raw) + if err != nil { + return "", err + } + privKey, err := w.PrivKeyForAddress(addr) + if err != nil { + return "", err + } + return encode(hex.EncodeToString(privKey.Serialize())) +} + +// func (w *Wallet) HaveAddress(a btcutil.Address) (bool, error) +func (w *Wallet) haveAddress(raw json.RawMessage) (string, error) { + addr, err := w.decodeJSONAddr(raw) + if err != nil { + return "", err + } + has, err := w.HaveAddress(addr) + if err != nil { + return "", err + } + return encode(has) +} + +// func (w *Wallet) AccountOfAddress(a btcutil.Address) (uint32, error) +func (w *Wallet) accountOfAddress(raw json.RawMessage) (string, error) { + addr, err := w.decodeJSONAddr(raw) + if err != nil { + return "", err + } + acct, err := w.AccountOfAddress(addr) + if err != nil { + return "", err + } + return encode(acct) +} + +type managedAddress struct { + Account uint32 `json:"account"` + Address string `json:"address"` + AddrHash Bytes `json:"addrHash"` + Imported bool `json:"imported"` + Internal bool `json:"internal"` + Compressed bool `json:"compressed"` + // Used(ns walletdb.ReadBucket) bool + AddrType uint8 `json:"addrType"` +} + +// func (w *Wallet) AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress, error) +func (w *Wallet) addressInfo(raw json.RawMessage) (string, error) { + addr, err := w.decodeJSONAddr(raw) + if err != nil { + return "", err + } + wAddr, err := w.AddressInfo(addr) + if err != nil { + return "", err + } + return encode(&managedAddress{ + Account: wAddr.Account(), + Address: wAddr.Address().String(), + AddrHash: wAddr.AddrHash(), + Imported: wAddr.Imported(), + Internal: wAddr.Internal(), + Compressed: wAddr.Compressed(), + AddrType: uint8(wAddr.AddrType()), + }) +} + +// func (w *Wallet) AccountNumber(scope waddrmgr.KeyScope, accountName string) (uint32, error) +type accountNumberParams struct { + Scope keyScope `json:"scope"` + AccountName string `json:"accountName"` +} + +func (w *Wallet) accountNumber(raw json.RawMessage) (string, error) { + params := new(accountNumberParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + acct, err := w.AccountNumber(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.AccountName) + if err != nil { + return "", err + } + return encode(acct) +} + +// func (w *Wallet) AccountName(scope waddrmgr.KeyScope, accountNumber uint32) (string, error) +type accountNameParams struct { + Scope keyScope `json:"scope"` + AccountNumber uint32 `json:"accountNumber"` +} + +func (w *Wallet) accountName(raw json.RawMessage) (string, error) { + params := new(accountNameParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + acctName, err := w.AccountName(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.AccountNumber) + if err != nil { + return "", err + } + return encode(acctName) +} + +// func (w *Wallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) +type accountPropertiesParams accountNameParams + +type accountProperties struct { + AccountNumber uint32 `json:"accountNumber"` + AccountName string `json:"accountName"` + ExternalKeyCount uint32 `json:"externalKeyCount"` + InternalKeyCount uint32 `json:"internalKeyCount"` + ImportedKeyCount uint32 `json:"importedKeyCount"` +} + +func (w *Wallet) accountProperties(raw json.RawMessage) (string, error) { + params := new(accountPropertiesParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + acctProps, err := w.AccountProperties(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.AccountNumber) + if err != nil { + return "", err + } + return encode(&accountProperties{ + AccountNumber: acctProps.AccountNumber, + AccountName: acctProps.AccountName, + ExternalKeyCount: acctProps.ExternalKeyCount, + InternalKeyCount: acctProps.InternalKeyCount, + ImportedKeyCount: acctProps.ImportedKeyCount, + }) +} + +// func (w *Wallet) RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error +type renameAccountParams struct { + accountNameParams + NewName string `json:"newName"` +} + +func (w *Wallet) renameAccount(raw json.RawMessage) (string, error) { + params := new(renameAccountParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + err = w.RenameAccount(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.AccountNumber, params.NewName) + if err != nil { + return "", err + } + return "", nil +} + +// func (w *Wallet) NextAccount(scope waddrmgr.KeyScope, name string) (uint32, error) +type nextAccountParams accountNumberParams + +func (w *Wallet) nextAccount(raw json.RawMessage) (string, error) { + params := new(nextAccountParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + acct, err := w.NextAccount(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.AccountName) + if err != nil { + return "", err + } + return encode(acct) +} + +// func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) +type listSinceBlockParams struct { + Start int32 `json:"start"` + End int32 `json:"end"` + SyncHeight int32 `json:"syncHeight"` +} + +func (w *Wallet) listSinceBlock(raw json.RawMessage) (string, error) { + params := new(listSinceBlockParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + txs, err := w.ListSinceBlock(params.Start, params.End, params.SyncHeight) + if err != nil { + return "", err + } + return encode(txs) +} + +// func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) +type listTransactionsParams struct { + From int `json:"from"` + Count int `json:"count"` +} + +func (w *Wallet) listTransactions(raw json.RawMessage) (string, error) { + params := new(listTransactionsParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + txs, err := w.ListTransactions(params.From, params.Count) + if err != nil { + return "", err + } + return encode(txs) +} + +// func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjson.ListTransactionsResult, error) +func (w *Wallet) listAddressTransactions(raw json.RawMessage) (string, error) { + var addresses []string + err := json.Unmarshal(raw, &addresses) + if err != nil { + return "", err + } + hash160Map := make(map[string]struct{}) + for _, addrStr := range addresses { + addr, err := btcutil.DecodeAddress(addrStr, w.params) + if err != nil { + return "", err + } + hash160Map[string(addr.ScriptAddress())] = struct{}{} + } + txs, err := w.ListAddressTransactions(hash160Map) + if err != nil { + return "", err + } + return encode(txs) +} + +// func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) +func (w *Wallet) listAllTransactions(_ json.RawMessage) (string, error) { + txs, err := w.ListAllTransactions() + if err != nil { + return "", err + } + return encode(txs) +} + +// TODO +// func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel <-chan struct{}) (*GetTransactionsResult, error) + +// func (w *Wallet) Accounts(scope waddrmgr.KeyScope) (*AccountsResult, error) +type accountResult struct { + accountProperties + TotalBalance int64 `json:"totalBalance"` +} + +type accountsResult struct { + Accounts []accountResult `json:"accounts"` + CurrentBlockHash Bytes `json:"currentBlockHash"` + CurrentBlockHeight int32 `json:"currentBlockHeight"` +} + +func (w *Wallet) accounts(raw json.RawMessage) (string, error) { + scope := new(keyScope) + err := json.Unmarshal(raw, scope) + if err != nil { + return "", err + } + res, err := w.Accounts(waddrmgr.KeyScope{ + Purpose: scope.Purpose, + Coin: scope.Coin, + }) + if err != nil { + return "", err + } + acctsRes := &accountsResult{ + Accounts: make([]accountResult, 0, len(res.Accounts)), + CurrentBlockHash: res.CurrentBlockHash[:], + CurrentBlockHeight: res.CurrentBlockHeight, + } + for _, acctRes := range res.Accounts { + acctsRes.Accounts = append(acctsRes.Accounts, accountResult{ + accountProperties: accountProperties{ + AccountNumber: acctRes.AccountNumber, + AccountName: acctRes.AccountName, + ExternalKeyCount: acctRes.ExternalKeyCount, + InternalKeyCount: acctRes.InternalKeyCount, + ImportedKeyCount: acctRes.ImportedKeyCount, + }, + TotalBalance: int64(acctRes.TotalBalance), + }) + } + return encode(acctsRes) +} + +// func (w *Wallet) AccountBalances(scope waddrmgr.KeyScope, requiredConfs int32) ([]AccountBalanceResult, error) +type accountBalancesParams struct { + Scope keyScope `json:"scope"` + RequiredConfs int32 `json:"requiredConfs"` +} + +type accountBalanceResult struct { + AccountNumber uint32 `json:"accountNumber"` + AccountName string `json:"accountName"` + AccountBalance int64 `json:"accountBalance"` +} + +func (w *Wallet) accountBalances(raw json.RawMessage) (string, error) { + params := new(accountBalancesParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + balances, err := w.AccountBalances(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.RequiredConfs) + if err != nil { + return "", err + } + res := make([]accountBalanceResult, 0, len(balances)) + for _, bal := range balances { + res = append(res, accountBalanceResult{ + AccountNumber: bal.AccountNumber, + AccountName: bal.AccountName, + AccountBalance: int64(bal.AccountBalance), + }) + } + return encode(res) +} + +// func (w *Wallet) ListUnspent(minconf, maxconf int32, addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) +type listUnspentParams struct { + MinConf int32 `json:"minConf"` + MaxConf int32 `json:"maxConf"` + Addresses []string `json:"addresses"` +} + +func (w *Wallet) listUnspent(raw json.RawMessage) (string, error) { + params := new(listUnspentParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + var addresses map[string]struct{} + if len(params.Addresses) > 0 { + addresses = make(map[string]struct{}) + for _, as := range params.Addresses { + a, err := btcutil.DecodeAddress(as, w.params) + if err != nil { + return "", err + } + addresses[a.EncodeAddress()] = struct{}{} + } + } + unspents, err := w.ListUnspent(params.MinConf, params.MaxConf, addresses) + if err != nil { + return "", err + } + return encode(unspents) +} + +// func (w *Wallet) DumpPrivKeys() ([]string, error) +func (w *Wallet) dumpPrivKeys(_ json.RawMessage) (string, error) { + keys, err := w.DumpPrivKeys() + if err != nil { + return "", err + } + return encode(keys) +} + +// func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) +func (w *Wallet) dumpWIFPrivateKey(raw json.RawMessage) (string, error) { + addr, err := w.decodeJSONAddr(raw) + if err != nil { + return "", err + } + priv, err := w.DumpWIFPrivateKey(addr) + if err != nil { + return "", err + } + return encode(priv) +} + +// func (w *Wallet) ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF, bs *waddrmgr.BlockStamp, rescan bool) (string, error) + +// WIF is a private key. +type WIF struct { + PrivKey Bytes `json:"privKey"` + CompressPubKey bool `json:"compressPubKey"` +} + +type blockStamp struct { + Height int32 `json:"height"` + Hash Bytes `json:"hash"` + Timestamp int64 `json:"timestamp"` +} + +type importPrivateKeyParams struct { + Scope keyScope `json:"keyScope"` + WIF WIF `json:"wif"` + BlockStamp blockStamp `json:"blockStamp"` + Rescan bool `json:"rescan"` +} + +func (w *Wallet) importPrivateKey(raw json.RawMessage) (string, error) { + params := new(importPrivateKeyParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), params.WIF.PrivKey) + wif, err := btcutil.NewWIF(privKey, w.params, params.WIF.CompressPubKey) + if err != nil { + return "", err + } + + h := chainhash.Hash{} + err = h.SetBytes(params.BlockStamp.Hash) + if err != nil { + return "", err + } + + blockStamp := &waddrmgr.BlockStamp{ + Height: params.BlockStamp.Height, + Hash: h, + Timestamp: time.Unix(params.BlockStamp.Timestamp, 0), + } + + scope := waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + } + + res, err := w.ImportPrivateKey(scope, wif, blockStamp, params.Rescan) + if err != nil { + return "", err + } + + return encode(res) +} + +// func (w *Wallet) LockedOutpoint(op wire.OutPoint) bool +func (w *Wallet) lockedOutpoint(raw json.RawMessage) (string, error) { + outPt, err := outpointFromJSON(raw) + if err != nil { + return "", err + } + return encode(w.LockedOutpoint(*outPt)) +} + +// func (w *Wallet) LockOutpoint(op wire.OutPoint) +func (w *Wallet) lockOutpoint(raw json.RawMessage) (string, error) { + outPt, err := outpointFromJSON(raw) + if err != nil { + return "", err + } + w.LockOutpoint(*outPt) + return "", nil +} + +// func (w *Wallet) UnlockOutpoint(op wire.OutPoint) +func (w *Wallet) unlockOutpoint(raw json.RawMessage) (string, error) { + outPt, err := outpointFromJSON(raw) + if err != nil { + return "", err + } + w.UnlockOutpoint(*outPt) + return "", nil +} + +// func (w *Wallet) ResetLockedOutpoints() +func (w *Wallet) resetLockedOutpoints(_ json.RawMessage) (string, error) { + w.ResetLockedOutpoints() + return "", nil +} + +// func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput +func (w *Wallet) lockedOutpoints(_ json.RawMessage) (string, error) { + return encode(w.LockedOutpoints()) +} + +// func (w *Wallet) LeaseOutput(id wtxmgr.LockID, op wire.OutPoint) (time.Time, error) +type leaseOutputParams struct { + ID Bytes `json:"id"` + OutPoint hashIndex `json:"op"` +} + +func parseLeaseOutputParams(raw json.RawMessage) (wtxmgr.LockID, *wire.OutPoint, error) { + params := new(leaseOutputParams) + err := json.Unmarshal(raw, params) + if err != nil { + return wtxmgr.LockID{}, nil, err + } + + var lockID wtxmgr.LockID // [32]byte + copy(lockID[:], params.ID) + + h := chainhash.Hash{} + err = h.SetBytes(params.OutPoint.Hash) + if err != nil { + return wtxmgr.LockID{}, nil, err + } + + return lockID, wire.NewOutPoint(&h, uint32(params.OutPoint.Index)), nil +} + +func (w *Wallet) leaseOutput(raw json.RawMessage) (string, error) { + lockID, outPt, err := parseLeaseOutputParams(raw) + if err != nil { + return "", err + } + + t, err := w.LeaseOutput(lockID, *outPt) + if err != nil { + return "", err + } + return encode(t.Unix()) +} + +// func (w *Wallet) ReleaseOutput(id wtxmgr.LockID, op wire.OutPoint) error +func (w *Wallet) releaseOutput(raw json.RawMessage) (string, error) { + lockID, outPt, err := parseLeaseOutputParams(raw) + if err != nil { + return "", err + } + err = w.ReleaseOutput(lockID, *outPt) + if err != nil { + return "", err + } + return "", nil +} + +// func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) +func (w *Wallet) sortedActivePaymentAddresses(_ json.RawMessage) (string, error) { + addrs, err := w.SortedActivePaymentAddresses() + if err != nil { + return "", err + } + return encode(addrs) +} + +// func (w *Wallet) NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) +func (w *Wallet) newAddress(raw json.RawMessage) (string, error) { + params := new(currentAddressParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + addr, err := w.NewAddress(params.Account, waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }) + if err != nil { + return "", err + } + return encode(addr.String()) +} + +// func (w *Wallet) NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) +func (w *Wallet) newChangeAddress(raw json.RawMessage) (string, error) { + params := new(currentAddressParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + addr, err := w.NewChangeAddress(params.Account, waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }) + if err != nil { + return "", err + } + return encode(addr.String()) +} + +// func (w *Wallet) TotalReceivedForAccounts(scope waddrmgr.KeyScope, minConf int32) ([]AccountTotalReceivedResult, error) +type totalReceivedForAccountsParams struct { + Scope keyScope `json:"scope"` + MinConf int32 `json:"minConf"` +} + +type accountTotalReceivedResult struct { + AccountNumber uint32 `json:"accountNumber"` + AccountName string `json:"accountName"` + TotalReceived int64 `json:"totalReceived"` + LastConfirmation int32 `json:"lastConfirmation"` +} + +func (w *Wallet) totalReceivedForAccounts(raw json.RawMessage) (string, error) { + params := new(totalReceivedForAccountsParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + res, err := w.TotalReceivedForAccounts(waddrmgr.KeyScope{ + Purpose: params.Scope.Purpose, + Coin: params.Scope.Coin, + }, params.MinConf) + + outRows := make([]accountTotalReceivedResult, 0, len(res)) + for _, recv := range res { + outRows = append(outRows, accountTotalReceivedResult{ + AccountNumber: recv.AccountNumber, + AccountName: recv.AccountName, + TotalReceived: int64(recv.TotalReceived), + LastConfirmation: recv.LastConfirmation, + }) + } + return encode(outRows) +} + +// func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) +type totalReceivedForAddr struct { + Addr string `json:"addr"` + MinConf int32 `json:"minConf"` +} + +func (w *Wallet) totalReceivedForAddr(raw json.RawMessage) (string, error) { + params := new(totalReceivedForAddr) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + addr, err := btcutil.DecodeAddress(params.Addr, w.params) + if err != nil { + return "", err + } + amt, err := w.TotalReceivedForAddr(addr, params.MinConf) + if err != nil { + return "", err + } + return encode(int64(amt)) +} + +// func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) +type sendOutputsParams struct { + Outputs []scriptValue `json:"outputs"` + Account uint32 `json:"account"` + MinConf int32 `json:"minConf"` + SatsPerKB float64 `json:"satPerKb"` + Label string `json:"label"` +} + +func (w *Wallet) sendOutputs(raw json.RawMessage) (string, error) { + params := new(sendOutputsParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + outputs := make([]*wire.TxOut, 0, len(params.Outputs)) + for _, op := range params.Outputs { + outputs = append(outputs, wire.NewTxOut(op.Value, op.Script)) + } + + satsPerKB, err := btcutil.NewAmount(params.SatsPerKB) + if err != nil { + return "", err + } + + msgTx, err := w.SendOutputs(outputs, params.Account, params.MinConf, satsPerKB, params.Label) + if err != nil { + return "", err + } + + msgB, err := serializeMsgTx(msgTx) + if err != nil { + return "", err + } + + return encode(hex.EncodeToString(msgB)) +} + +// func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, +// additionalPrevScripts map[wire.OutPoint][]byte, +// additionalKeysByAddress map[string]*btcutil.WIF, +// p2shRedeemScriptsByAddress map[string][]byte) ([]SignatureError, error) +type signTransactionParams struct { + Tx Bytes `json:"tx"` + HashType uint32 `json:"hashType"` + AdditionalPrevScripts map[string]Bytes `json:"additionalPrevScripts"` + AdditionalKeysByAddress map[string]WIF `json:"additionalKeysByAddress"` + P2shRedeemScriptsByAddress map[string]Bytes `json:"p2shRedeemScriptsByAddress"` +} + +type signatureError struct { + InputIndex uint32 `json:"inputIndex"` + Error string `json:"error"` +} + +type signTxResponse struct { + SigErrors []signatureError `json:"sigErrs"` + SignedTx Bytes `json:"signedTx"` +} + +func (w *Wallet) signTransaction(raw json.RawMessage) (string, error) { + params := new(signTransactionParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + tx := new(wire.MsgTx) + err = tx.Deserialize(bytes.NewBuffer(params.Tx)) + if err != nil { + return "", err + } + + additionalPrevScripts := make(map[wire.OutPoint][]byte, len(params.AdditionalPrevScripts)) + for opStr, script := range params.AdditionalPrevScripts { + parts := strings.Split(opStr, ":") + if len(parts) != 2 { + return "", fmt.Errorf("error decoding outpoint %q: %v", opStr, err) + } + hashStr, voutStr := parts[0], parts[1] + + h, err := chainhash.NewHashFromStr(hashStr) + if err != nil { + return "", err + } + + vout, err := strconv.Atoi(voutStr) + if err != nil { + return "", err + } + + op := wire.NewOutPoint(h, uint32(vout)) + additionalPrevScripts[*op] = script + } + + additionalKeysByAddress := make(map[string]*btcutil.WIF, len(params.AdditionalKeysByAddress)) + for addr, wif := range params.AdditionalKeysByAddress { + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), wif.PrivKey) + walletWIF, err := btcutil.NewWIF(privKey, w.params, wif.CompressPubKey) + if err != nil { + return "", err + } + additionalKeysByAddress[addr] = walletWIF + } + + p2shRedeemScriptsByAddress := make(map[string][]byte, len(params.P2shRedeemScriptsByAddress)) + for addr, script := range params.P2shRedeemScriptsByAddress { + p2shRedeemScriptsByAddress[addr] = script + } + + wSigErrs, err := w.SignTransaction(tx, txscript.SigHashType(params.HashType), additionalPrevScripts, additionalKeysByAddress, p2shRedeemScriptsByAddress) + if err != nil { + return "", err + } + + sigErrs := make([]signatureError, 0, len(wSigErrs)) + for _, e := range wSigErrs { + eStr := "" + if e.Error != nil { + eStr = e.Error.Error() + } + + sigErrs = append(sigErrs, signatureError{ + InputIndex: e.InputIndex, + Error: eStr, + }) + } + + signedTxB, err := serializeMsgTx(tx) + if err != nil { + return "", err + } + + return encode(&signTxResponse{ + SigErrors: sigErrs, + SignedTx: signedTxB, + }) +} + +// func (w *Wallet) PublishTransaction(tx *wire.MsgTx, label string) error +type publishTransactionParams struct { + Tx Bytes `json:"tx"` + Label string `json:"label"` +} + +func (w *Wallet) publishTransaction(raw json.RawMessage) (string, error) { + params := new(publishTransactionParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + var tx wire.MsgTx + err = tx.Deserialize(bytes.NewBuffer(params.Tx)) + if err != nil { + return "", err + } + + return "", w.PublishTransaction(&tx, params.Label) +} + +// TODO: Just pass the name and grab the python versions Python-side. +// func (w *Wallet) ChainParams() *chaincfg.Params + +func (w *Wallet) prepHandlers() { + w.handlers = map[string]func(json.RawMessage) (string, error){ + "makeMultiSigScript": w.makeMultiSigScript, + "importP2SHRedeemScript": w.importP2SHRedeemScript, + "unspentOutputs": w.unspentOutputs, + "start": w.start, + "stop": w.stop, + "shuttingDown": w.shuttingDown, + "waitForShutdown": w.waitForShutdown, + "synchronizingToNetwork": w.synchronizingToNetwork, + "chainSynced": w.chainSynced, + "setChainSynced": w.setChainSynced, + "createSimpleTx": w.createSimpleTx, + "unlock": w.unlock, + "lock": w.lock, + "locked": w.locked, + "changePrivatePassphrase": w.changePrivatePassphrase, + "changePublicPassphrase": w.changePublicPassphrase, + "changePassphrases": w.changePassphrases, + "accountAddresses": w.accountAddresses, + "calculateBalance": w.calculateBalance, + "calculateAccountBalances": w.calculateAccountBalances, + "currentAddress": w.currentAddress, + "pubKeyForAddress": w.pubKeyForAddress, + "labelTransaction": w.labelTransaction, + "privKeyForAddress": w.privKeyForAddress, + "haveAddress": w.haveAddress, + "accountOfAddress": w.accountOfAddress, + "addressInfo": w.addressInfo, + "accountNumber": w.accountNumber, + "accountName": w.accountName, + "accountProperties": w.accountProperties, + "renameAccount": w.renameAccount, + "nextAccount": w.nextAccount, + "listSinceBlock": w.listSinceBlock, + "listTransactions": w.listTransactions, + "listAddressTransactions": w.listAddressTransactions, + "listAllTransactions": w.listAllTransactions, + "accounts": w.accounts, + "accountBalances": w.accountBalances, + "listUnspent": w.listUnspent, + "dumpPrivKeys": w.dumpPrivKeys, + "dumpWIFPrivateKey": w.dumpWIFPrivateKey, + "importPrivateKey": w.importPrivateKey, + "lockedOutpoint": w.lockedOutpoint, + "lockOutpoint": w.lockOutpoint, + "unlockOutpoint": w.unlockOutpoint, + "resetLockedOutpoints": w.resetLockedOutpoints, + "lockedOutpoints": w.lockedOutpoints, + "leaseOutput": w.leaseOutput, + "releaseOutput": w.releaseOutput, + "sortedActivePaymentAddresses": w.sortedActivePaymentAddresses, + "newAddress": w.newAddress, + "newChangeAddress": w.newChangeAddress, + "totalReceivedForAccounts": w.totalReceivedForAccounts, + "totalReceivedForAddr": w.totalReceivedForAddr, + "sendOutputs": w.sendOutputs, + "signTransaction": w.signTransaction, + "publishTransaction": w.publishTransaction, + "syncStatus": w.syncStatus, + } +} + +func (w *Wallet) router(name string) walletRouter { + return w.handlers[name] +} + +// syncHeight is the best known sync height among peers. +func (w *Wallet) syncHeight() int32 { + var maxHeight int32 + for _, p := range w.neutrino.Peers() { + tipHeight := p.StartingHeight() + lastBlockHeight := p.LastBlock() + if lastBlockHeight > tipHeight { + tipHeight = lastBlockHeight + } + if tipHeight > maxHeight { + maxHeight = tipHeight + } + } + return maxHeight +} + +type syncStatus struct { + Target int32 `json:"target"` + Height int32 `json:"height"` + Syncing bool `json:"syncing"` +} + +func (w *Wallet) syncStatus(_ json.RawMessage) (string, error) { + blk, err := w.neutrino.BestBlock() + if err != nil { + return "", err + } + + target := w.syncHeight() + height := blk.Height + + ss := &syncStatus{ + Target: target, + Height: height, + // Syncing is whether the wallet has finished syncing. The second filter + // is to prevent unexpected value in certain error situations. + Syncing: !w.ChainSynced() && height >= target-1, + } + + b, err := json.Marshal(ss) + + if err != nil { + return "", nil + } + + return string(b), nil +} + +func walletExistsUtility(raw json.RawMessage) (string, error) { + params := new(walletSpecs) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + netParams, err := parseNet(params.Net) + if err != nil { + return "", err + } + + exists, err := walletExists(params.Dir, netParams) + if err != nil { + return "", err + } + + b, err := json.Marshal(exists) + if err != nil { + return "", fmt.Errorf("walletExists error: %v", err) + } + return string(b), nil +} + +type createWalletParams struct { + walletSpecs + PW Bytes `json:"pw"` + Seed Bytes `json:"seed"` +} + +func createWalletUtility(raw json.RawMessage) (string, error) { + params := new(createWalletParams) + err := json.Unmarshal(raw, params) + if err != nil { + return "", err + } + + netParams, err := parseNet(params.Net) + if err != nil { + return "", err + } + + exists, err := walletExists(params.Dir, netParams) + if err != nil { + return "", err + } + if exists { + return "", fmt.Errorf("wallet already exists") + } + + return "", createWallet(params.PW, params.Seed, params.Dir, netParams) +} diff --git a/decred/decred/btc/btcwallet/libbtcwallet/log.go b/decred/decred/btc/btcwallet/libbtcwallet/log.go new file mode 100644 index 00000000..873c01a7 --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/log.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/neutrino" +) + +// LevelTrace Level = iota +// LevelDebug +// LevelInfo +// LevelWarn +// LevelError +// LevelCritical +// LevelOff + +var log btclog.Logger + +func useLogBackend(be *btclog.Backend, lvl btclog.Level) { + logger := func(name string) btclog.Logger { + lggr := be.Logger(name) + lggr.SetLevel(lvl) + return lggr + } + + log = logger("LIB") + wallet.UseLogger(logger("WLLT")) + chain.UseLogger(logger("CHAIN")) + wtxmgr.UseLogger(logger("TXMGR")) + neutrino.UseLogger(logger("NTRNO")) +} + +type logWriter struct{} + +func (logWriter) Write(p []byte) (n int, err error) { + feedChan <- &feedMessage{ + FeedID: logFeedID, + Payload: string(p), + } + + return len(p), nil +} + +func initializeLogging(lvl btclog.Level) { + useLogBackend(btclog.NewBackend(logWriter{}), lvl) +} diff --git a/decred/decred/btc/btcwallet/libbtcwallet/test-data.json b/decred/decred/btc/btcwallet/libbtcwallet/test-data.json new file mode 100644 index 00000000..8004739c --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/test-data.json @@ -0,0 +1,245 @@ +{ + "MakeMultiSigScript.in.addr.1": "bc1qkxv7gahcyyw9wqn6swcfr67k3fyuwh3r4pff5d", + "MakeMultiSigScript.in.addr.2": "1J6qmxrbixhjX9Js2FydPaJJDPpYQ6Q6GG", + "MakeMultiSigScript.in.nConfs": 5, + "MakeMultiSigScript.out": "d75c1b4b4c758f592fbf056855869ef7decd8b39bf3ec7f007c23e604c1befce", + + "ImportP2SHRedeemScript.in.script": "fe263769f677dc4977a60b5f06db4c5d3260b3bd68a3e4bcebbdf2c2f349a545", + "ImportP2SHRedeemScript.out.addr": "3BSmmPAjxPUUjN7J24UhAPmMT9g9XUss7n", + + "ImportP2SHRedeemScript.in.acct": 98, + "ImportP2SHRedeemScript.in.confs": 169, + "ImportP2SHRedeemScript.out.outPoint.hash": "fb66e86f2d9b937f861deba33a97536893c38fda6ce9c457733a4b0d9b569efc", + "ImportP2SHRedeemScript.out.outPoint.index": 49, + "ImportP2SHRedeemScript.out.output.script": "4605f83e6d6b8878726e1de79a43cc2992061517ec446abf71e3ce76a9b905c8", + "ImportP2SHRedeemScript.out.output.value": 132, + "ImportP2SHRedeemScript.out.outputKind": 255, + "ImportP2SHRedeemScript.out.containingBlock.hash": "187e1e5c824c22135584362c25776445f7e6a8ddccb14c8014eef7936ac75406", + "ImportP2SHRedeemScript.out.containingBlock.index": 107, + "ImportP2SHRedeemScript.out.receiveTime": 55540, + + "ShuttingDown.out": true, + + "SynchronizingToNetwork.out": true, + + "ChainSynced.out": true, + + "SetChainSynced.in": true, + + "CreateSimpleTx.in.acct": 16716, + "CreateSimpleTx.in.output.value": 557, + "CreateSimpleTx.in.output.script": "d3cb7e9a6a23123bc1b28c1360dd4e4ce0b66e86c15175c092e627f7c141674e", + "CreateSimpleTx.in.output.version": 9708, + "CreateSimpleTx.in.minConf": 4681, + "CreateSimpleTx.in.satPerKb": 0.00317, + "CreateSimpleTx.in.dryRun": true, + "CreateSimpleTx.out.tx": "010000000184b48ce46bdd04363f4ad1297ac0e8ad6991127bc10ae6c8d8e96cf96bef2f87be07000000ffffffff0000000000", + "CreateSimpleTx.out.prevScript": "82315428d9871ab488904277bbfffe7be45e8d8fdf3f2f57327e778bd6abf5a7", + "CreateSimpleTx.out.prevInputValue": 2476744933, + "CreateSimpleTx.out.totalInput": 60997, + "CreateSimpleTx.out.changeIndex": 43061, + + "Unlock.in.passphrase": "thispassword", + + "Locked.out": true, + + "ChangePrivatePassphrase.in.old": "oldpassword", + "ChangePrivatePassphrase.in.new": "newpassword", + + "ChangePublicPassphrase.in.old": "oldpassword_abc", + "ChangePublicPassphrase.in.new": "newpassword_abc", + + "ChangePassphrases.in.publicOld": "publicoldpass", + "ChangePassphrases.in.publicNew": "publicnewpass", + "ChangePassphrases.in.privateOld": "privateoldpass", + "ChangePassphrases.in.privateNew": "privatenewpass", + + "AccountAddresses.in.acct": 53995, + "AccountAddresses.out.addr.1": "bc1qg3x920t6fkw9mtzjxrskrpqyhl6l3u7p7wy0hh", + "AccountAddresses.out.addr.2": "37tqWaZu31u6k13M3tng8LTBNonuNmhofw", + + "CalculateBalance.in.confirms": 5848, + "CalculateBalance.out": 2446592793, + + "CalculateAccountBalances.in.acct": 42324, + "CalculateAccountBalances.in.confirms": 48432, + "CalculateAccountBalances.out.total": 1427393644, + "CalculateAccountBalances.out.spendable": 2499202751, + "CalculateAccountBalances.out.immatureReward": 3713607703, + + "CurrentAddress.in.acct": 38351, + "CurrentAddress.in.purpose": 62727, + "CurrentAddress.in.coin": 23588, + "CurrentAddress.out.addr": "bc1q0mfkzwagphjfw9shx9nrqmpru9ayqm3ztxjqul", + + "PubKeyForAddress.in.addr": "bc1qe888cmq20zt3r9myqptcr6dcu7tcvm79mshy9y", + "PubKeyForAddress.out.pubkey": "02290c67767ea097b6cb2b48af07769bf07273b294271e0d838c13cebdbb6be9f0", + + "PrivKeyForAddress.in.addr": "bc1q4n8xd4e3uffswqw48umxc7xf68wytqcuaa36pt", + "PrivKeyForAddress.out.privkey": "05e1c9177bbd52c174b702cfa9e091923e69836413c637a716648bba681ceff6", + + "LabelTransaction.in.h": "cefe95fbbedce4e463cb219da19c6b21db465772fa4437386bc518be556c372d", + "LabelTransaction.in.label": "label82471068.34835", + "LabelTransaction.in.overwrite": true, + + "HaveAddress.in": "17LnaMqaMHStPnpby9y9Vahf2c28RE7VF3", + "HaveAddress.out": true, + + "AccountOfAddress.in.addr": "bc1qznkukafxn2r8p00a4ymgjywjs0kncvny7lqzdh", + "AccountOfAddress.out.acct": 41067, + + "AddressInfo.in.addr": "bc1ql3mzyxygk8dxaxusq2jlthlhyedj7w99v4tey5knq0uhdhp34vxswm9vyd", + "AddressInfo.out.acct": 14992, + "AddressInfo.out.addr": "bc1qk5zsyws2p55nwcazv2xsk97fnmeqtxrzva4n98", + "AddressInfo.out.addrHash": "da07648ab8298b62d820cfd8e05cbe10a91e94aa663d6d5962a627651bddb228", + "AddressInfo.out.imported": true, + "AddressInfo.out.internal": true, + "AddressInfo.out.compressed": true, + "AddressInfo.out.addrType": 123, + + "AccountNumber.in.scope.purpose": 28126, + "AccountNumber.in.scope.coin": 34192, + "AccountNumber.in.accountName": "accountName4728525*", + "AccountNumber.out.acct": 41625, + + "AccountName.in.scope.purpose": 64615, + "AccountName.in.scope.coin": 1004, + "AccountName.in.acct": 57860, + "AccountName.out.accountName": "accountName!@#$%^12345", + + "AccountProperties.in.scope.purpose": 60413, + "AccountProperties.in.scope.coin": 17933, + "AccountProperties.in.acct": 51775, + "AccountProperties.out.accountNumber": 8219, + "AccountProperties.out.accountName": "accountName159753nkouk,", + "AccountProperties.out.externalKeyCount": 18302, + "AccountProperties.out.internalKeyCount": 42298, + "AccountProperties.out.importedKeyCount": 50439, + + "RenameAccount.in.scope.purpose": 10886, + "RenameAccount.in.scope.coin": 59541, + "RenameAccount.in.acct": 2196, + "RenameAccount.in.newName": "somedumbname", + + "NextAccount.in.scope.purpose": 39212, + "NextAccount.in.scope.coin": 32829, + "NextAccount.in.accountName": "ikmujnyhnbaccountName", + "NextAccount.out.acct": 51240, + + "ListSinceBlock.in.start": 56776, + "ListSinceBlock.in.end": 13496, + "ListSinceBlock.in.syncHeight": 26363, + "ListSinceBlock.out.blockTime": 47913, + + "ListTransactions.in.skip": 46408, + "ListTransactions.in.count": 13799, + "ListTransactions.out.confs": 27882, + + "ListAddressTransactions.in.pkHash": "d22d2c715aa0347dcac69dc6a8c4dcb11503db35", + "ListAddressTransactions.in.addr": "bc1q6gkjcu265q68mjkxnhr233xuky2s8ke4sflq5h", + "ListAddressTransactions.out.timeReceived": 2242780545, + + "ListAllTransactions.out.vout": 60598, + + "Accounts.in.purpose": 55300, + "Accounts.in.coin": 60609, + "Accounts.out.blockHash": "8a513b2c3ac427a9711fb3d6fa39060468641ac8e63217b34f7d68b2a9f6cad7", + "Accounts.out.blockHeight": 9848, + "Accounts.out.balance": 277561438602, + "Accounts.out.acct": 18149, + + "AccountBalances.in.confs": 44054, + "AccountBalances.in.purpose": 26316, + "AccountBalances.in.coin": 20837, + "AccountBalances.out.acctNumber": 54827, + "AccountBalances.out.acctName": "soemotheraccountname852963741", + "AccountBalances.out.balance": 60465, + + "ListUnspent.in.minConf": 9646, + "ListUnspent.in.maxConf": 14324, + "ListUnspent.in.addr": "1FAKWrQVG7p16YimUSqo453U9NWaH4ngv7", + "ListUnspent.out.scriptPubKey": "5daa9b2b233ab2c911398a7dc3b9ce8067fe7876dd75e4852c7501cd3c575d5e", + + "DumpPrivKeys.out": "ef3701bc6edff92169fa919f7afe771367f8ef6f3e36f98cd5549516f9b41183", + + "DumpWIFPrivateKey.in.addr": "1DaJ3pcHd7b41tEMbgzk4g8orj94zfZDaL", + "DumpWIFPrivateKey.out.priv": "c3d4659a7277355e95247ce08f382fe1b2a9cb5c2a1b544f8154c4cb1c45aa25", + "DumpWIFPrivateKey.out.wif": "L3nNtTABas33TrLXjwph9PW6b46QUoonCxiLzfumQpMGYAZsboq6", + + "ImportPrivateKey.in.purpose": 26247, + "ImportPrivateKey.in.coin": 14111, + "ImportPrivateKey.in.priv": "975a94cfc93a0947bf38eda631e2e80ce286f21c86e43056b83b462a2e05453d", + "ImportPrivateKey.in.wif": "L2HvV5EqeyvE3b2KVM2Ecidx99jB3LKYsk4n6fwC3tBrQafbHvff", + "ImportPrivateKey.in.blockHeight": 6557, + "ImportPrivateKey.in.blockHash": "17efcd20cd2e6c00c6978962de3440c0df20759bddf036ab2baa6110172b0d6c", + "ImportPrivateKey.in.blockStamp": 15108, + "ImportPrivateKey.in.rescan": true, + "ImportPrivateKey.out.addr": "bc1qfstgtjp3mj4ac29lcfzt9ffgkjqxpqv9aqk28l", + + "LockedOutpoint.in.hash": "cc9d168970f676477a74c4c328f60b51d0004cd9ec2f43077f28811d10e3a83c", + "LockedOutpoint.in.index": 21340, + "LockedOutpoint.out.locked": true, + + "LockOutpoint.in.hash": "ed34bbc3ce44fd6a8e033b7386c3e63706542c38978d71a3628a26e592da0651", + "LockOutpoint.in.index": 27663, + + "UnlockOutpoint.in.hash": "db2cb8e30eb063436fc75b5d919f64ea33fd40475a6536ff68e774fd6d76b1ad", + "UnlockOutpoint.in.index": 50388, + + "LockedOutpoints.out.hash": "af3f6a08e3757115eb9bfc03e7f384c2c5ad39e571cc94af62d8a23777882624", + + "LeaseOutput.in.hash": "7865fa0a41e2be0fe8028108a69f8477ab711d637cca46af9f80777e7cafa94b", + "LeaseOutput.in.index": 59960, + "LeaseOutput.in.lockID": "077f0c6b3c7a0331a74020c975f8c681a8cf45f449c3f52c693e250787b8cbf6", + "LeaseOutput.out": 4291200265, + + "ReleaseOutput.in.hash": "fe076200e3ff4275708369edba872ab9ba4042098669f19b6c232d1c788b3fba", + "ReleaseOutput.in.index": 28342, + "ReleaseOutput.in.lockID": "b9d4d06fefd77c88333c449feb8368ec54783f818ae1f34f393237153c124e06", + + "SortedActivePaymentAddresses.out": "bc1ql6vu656s8myx5vldjpned9vkpmxpwlkpdgam7v", + + "NewAddress.in.purpose": 14320, + "NewAddress.in.coin": 30305, + "NewAddress.in.acct": 12929, + "NewAddress.out.addr": "bc1qnx2zvx8j3p3hn55f6lvdx66pw87hzezf0xq7ha", + + "NewChangeAddress.in.purpose": 54895, + "NewChangeAddress.in.coin": 11446, + "NewChangeAddress.in.acct": 40472, + "NewChangeAddress.out.addr": "1JNeREtVvEZLarQph1fK3fBUmiiDDjxkWn", + + "TotalReceivedForAccounts.in.purpose": 54895, + "TotalReceivedForAccounts.in.coin": 11446, + "TotalReceivedForAccounts.in.minConf": 40472, + "TotalReceivedForAccounts.out.accountNumber": 22509, + "TotalReceivedForAccounts.out.accountName": "whatisanaccountnameanyway", + "TotalReceivedForAccounts.out.totalReceived": 220845781373530, + "TotalReceivedForAccounts.out.lastConfirmation": 29696, + + "TotalReceivedForAddr.in.addr": "1Ch31UgWbAV3aDRAfv3wQgAiZXPJGCHmva", + "TotalReceivedForAddr.in.minConf": 49742, + "TotalReceivedForAddr.out.amt": 162933232569807, + + "SendOutputs.in.value": 82934310751408, + "SendOutputs.in.pkScript": "f90ec5fdd495a6f70281312112c8f77c1bbe18d06a8d50a9be10f8390d113cb4", + "SendOutputs.in.acct": 7163706, + "SendOutputs.in.minconf": 8655001, + "SendOutputs.in.satPerKb": 0.0055, + "SendOutputs.in.label": "somelabelidontknowimtiredofcomingupwithstuff", + "SendOutputs.out.tx": "01000000019f753149069e38df6712393be3f12170be5323c5a895dfd182fea9763b46432231d4000000ffffffff0131d4000000000000096275636b353433323100000000", + + "SignTransaction.in.tx": "01000000019f753149069e38df6712393be3f12170be5323c5a895dfd182fea9763b46432231d4000000ffffffff0131d4000000000000096e657773637269707400000000", + "SignTransaction.in.hashType": 5, + "SignTransaction.in.prevout.hash": "18569474662c20be03d4a0e4bda9ba1c309ab221e45029a421aa0b35e5fdf8d8", + "SignTransaction.in.prevout.idx": 21221, + "SignTransaction.in.prevout.script": "684186e46dcce378f9daa14544972b169962fbc8a75bc85c2fe765f6be41bec4", + "SignTransaction.in.key.addr": "1NV6uXWDcoAUgWQMv3wULWzzkWJMHxsha", + "SignTransaction.in.key.wif": "KyPcw2aFAreeMEsu3NKukcyMhBJtX2b1fzcHDFw6MYu2R5LbT8fz", + "SignTransaction.in.script.addr": "bc1q0jtgqve963pwy4adz9mlz46j85xg0zs2g59r2u", + "SignTransaction.in.script.script": "145a410a0e1820dc157d2451eaeac8cc2d849cddd1b4b5fc2f5e1389195cb5c7", + "SignTransaction.out.sigScript": "58de6caedccfd587b3c81316673a094b626d9faccf9e4e759fbc64377878d3be", + + "PublishTransaction.in.tx": "01000000019f753149069e38df6712393be3f12170be5323c5a895dfd182fea9763b46432231d4000000ffffffff0131d40000000000002075c01f51c91ed7e6dee0f9e5f8c8e4bf1e6fee33cdc12a5ca24873577235914700000000", + "PublishTransaction.in.label": "finallythelastone" +} \ No newline at end of file diff --git a/decred/decred/btc/btcwallet/libbtcwallet/testwallet.go b/decred/decred/btc/btcwallet/libbtcwallet/testwallet.go new file mode 100644 index 00000000..3c614f2f --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/testwallet.go @@ -0,0 +1,1206 @@ +package main + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +type testWallet struct { + data map[string]json.RawMessage + params *chaincfg.Params +} + +func newTestWallet(dataPath string) (*testWallet, error) { + enc, err := ioutil.ReadFile(dataPath) + if err != nil { + return nil, fmt.Errorf("error reading test data file: %v", err) + } + + var testData map[string]json.RawMessage + err = json.Unmarshal(enc, &testData) + if err != nil { + return nil, fmt.Errorf("error decoding test data: %v", err) + } + return &testWallet{ + data: testData, + params: &chaincfg.MainNetParams, + }, nil +} + +func (w *testWallet) testData(k string, thing interface{}) { + stuff, found := w.data[k] + if !found { + panic("no test data at " + k) + } + err := json.Unmarshal(stuff, thing) + if err != nil { + panic(fmt.Sprintf("test data unmarshal error. key = %s, stuff = %s, err = %v", k, string(stuff), err)) + } +} + +func (w *testWallet) MakeMultiSigScript(addrs []btcutil.Address, nRequired int) ([]byte, error) { + var expAddr1, expAddr2 string + var expConfs int + w.testData("MakeMultiSigScript.in.addr.1", &expAddr1) + w.testData("MakeMultiSigScript.in.addr.2", &expAddr2) + w.testData("MakeMultiSigScript.in.nConfs", &expConfs) + if addrs[0].String() != expAddr1 { + return nil, fmt.Errorf("wrong address 1. wanted %s, got %s", expAddr1, addrs[0].String()) + } + if addrs[1].String() != expAddr2 { + return nil, fmt.Errorf("wrong address 1. wanted %s, got %s", expAddr1, addrs[1].String()) + } + if nRequired != expConfs { + return nil, fmt.Errorf("wrong number of confirmations. Expected %d, got %d", expConfs, nRequired) + } + + var script Bytes + w.testData("MakeMultiSigScript.out", &script) + return script, nil +} + +func (w *testWallet) ImportP2SHRedeemScript(script []byte) (*btcutil.AddressScriptHash, error) { + var addrStr string + var expScript Bytes + w.testData("ImportP2SHRedeemScript.in.script", &expScript) + w.testData("ImportP2SHRedeemScript.out.addr", &addrStr) + if !bytes.Equal(script, expScript) { + return nil, fmt.Errorf("wrong script. wanted %x, got %x", expScript[:], script) + } + addr, err := btcutil.DecodeAddress(addrStr, w.params) + if err != nil { + return nil, fmt.Errorf("DecodeAddress error: %v", err) + } + + return addr.(*btcutil.AddressScriptHash), nil +} + +func (w *testWallet) UnspentOutputs(policy wallet.OutputSelectionPolicy) ([]*wallet.TransactionOutput, error) { + var expAcct uint32 + var expConfs int32 + w.testData("ImportP2SHRedeemScript.in.acct", &expAcct) + w.testData("ImportP2SHRedeemScript.in.confs", &expConfs) + + if policy.Account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, policy.Account) + } + if policy.RequiredConfirmations != expConfs { + return nil, fmt.Errorf("wrong confs. wanted %d, got %d", expConfs, policy.RequiredConfirmations) + } + + var txHashB, scriptB, blockHashB Bytes + var outputKind byte + var receiveTime, opVal int64 + var txIdx uint32 + var blockHeight int32 + w.testData("ImportP2SHRedeemScript.out.outPoint.hash", &txHashB) + w.testData("ImportP2SHRedeemScript.out.outPoint.index", &txIdx) + w.testData("ImportP2SHRedeemScript.out.output.script", &scriptB) + w.testData("ImportP2SHRedeemScript.out.output.value", &opVal) + w.testData("ImportP2SHRedeemScript.out.outputKind", &outputKind) + w.testData("ImportP2SHRedeemScript.out.containingBlock.hash", &blockHashB) + w.testData("ImportP2SHRedeemScript.out.containingBlock.index", &blockHeight) + w.testData("ImportP2SHRedeemScript.out.receiveTime", &receiveTime) + + var txHash chainhash.Hash + copy(txHash[:], txHashB) + + var blockHash chainhash.Hash + copy(blockHash[:], blockHashB) + + return []*wallet.TransactionOutput{ + { + OutPoint: *wire.NewOutPoint(&txHash, txIdx), + Output: *wire.NewTxOut(opVal, scriptB), + OutputKind: wallet.OutputKind(outputKind), + ContainingBlock: wallet.BlockIdentity{ + Hash: blockHash, + Height: blockHeight, + }, + ReceiveTime: time.Unix(receiveTime, 0), + }, + }, nil +} + +func (w *testWallet) Start() {} + +func (w *testWallet) Stop() {} + +func (w *testWallet) ShuttingDown() bool { + var answer bool + w.testData("ShuttingDown.out", &answer) + return answer +} + +func (w *testWallet) WaitForShutdown() {} + +func (w *testWallet) SynchronizingToNetwork() bool { + var answer bool + w.testData("SynchronizingToNetwork.out", &answer) + return answer +} + +func (w *testWallet) ChainSynced() bool { + var answer bool + w.testData("ChainSynced.out", &answer) + return answer +} + +func (w *testWallet) SetChainSynced(synced bool) { + var expSynced bool + w.testData("SetChainSynced.in", &expSynced) + if expSynced != synced { + panic(fmt.Sprintf("wrong synced. wanted %t, got %t", expSynced, synced)) + } +} + +func (w *testWallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount, dryRun bool) (*txauthor.AuthoredTx, error) { + var expAcct uint32 + var expOutputVal int64 + var expSatsPerKB float64 + var expPkScript Bytes + var expMinConf int32 + var expDryRun bool + w.testData("CreateSimpleTx.in.acct", &expAcct) + w.testData("CreateSimpleTx.in.output.value", &expOutputVal) + w.testData("CreateSimpleTx.in.output.script", &expPkScript) + w.testData("CreateSimpleTx.in.minConf", &expMinConf) + w.testData("CreateSimpleTx.in.satPerKb", &expSatsPerKB) + w.testData("CreateSimpleTx.in.dryRun", &expDryRun) + + if account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + + if len(outputs) != 1 { + return nil, fmt.Errorf("expected 1 output. got %d", len(outputs)) + } + output := outputs[0] + if output.Value != expOutputVal { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expOutputVal, output.Value) + } + if !bytes.Equal(output.PkScript, expPkScript) { + return nil, fmt.Errorf("wrong pubkey script. wanted %x, got %x", expPkScript[:], output.PkScript) + } + + if minconf != expMinConf { + return nil, fmt.Errorf("wrong minconf. wanted %d, got %d", expMinConf, minconf) + } + + expFeeRate, err := btcutil.NewAmount(expSatsPerKB) + if err != nil { + return nil, fmt.Errorf("NewAmount error: %v", err) + } + + if satPerKb != expFeeRate { + return nil, fmt.Errorf("wrong satPerKb. wanted %d, got %d", expFeeRate, satPerKb) + } + if dryRun != expDryRun { + return nil, fmt.Errorf("wrong dryRun. wanted %t, got %t", expDryRun, dryRun) + } + + // tx.addTxIn(msgtx.TxIn(previousOutPoint=msgtx.OutPoint(txHash=newHash(), idx=1982))) + var txB, prevScript Bytes + var prevVal int64 + var totalInput uint64 + var changeIndex int + w.testData("CreateSimpleTx.out.tx", &txB) + w.testData("CreateSimpleTx.out.prevScript", &prevScript) + w.testData("CreateSimpleTx.out.prevInputValue", &prevVal) + w.testData("CreateSimpleTx.out.totalInput", &totalInput) + w.testData("CreateSimpleTx.out.changeIndex", &changeIndex) + + tx := &wire.MsgTx{} + err = tx.Deserialize(bytes.NewBuffer(txB)) + if err != nil { + return nil, fmt.Errorf("test tx deserialize error: %v", err) + } + + return &txauthor.AuthoredTx{ + Tx: tx, + PrevScripts: [][]byte{prevScript}, + PrevInputValues: []btcutil.Amount{btcutil.Amount(prevVal)}, + TotalInput: btcutil.Amount(int64(totalInput)), + ChangeIndex: changeIndex, + }, nil +} + +func (w *testWallet) Unlock(passphrase []byte, lock <-chan time.Time) error { + var expPassphrase string + w.testData("Unlock.in.passphrase", &expPassphrase) + if string(passphrase) != expPassphrase { + return fmt.Errorf("wrong passphrase. expected %s, got %s", expPassphrase, string(passphrase)) + } + return nil +} + +func (w *testWallet) Lock() {} + +func (w *testWallet) Locked() bool { + var answer bool + w.testData("Locked.out", &answer) + return answer +} + +func (w *testWallet) ChangePrivatePassphrase(old, new []byte) error { + var expOld, expNew string + w.testData("ChangePrivatePassphrase.in.old", &expOld) + w.testData("ChangePrivatePassphrase.in.new", &expNew) + if string(old) != expOld { + return fmt.Errorf("wrong old passphrase. expected %s, got %s", expOld, string(old)) + } + if string(new) != expNew { + return fmt.Errorf("wrong new passphrase. expected %s, got %s", expNew, string(new)) + } + return nil +} + +func (w *testWallet) ChangePublicPassphrase(old, new []byte) error { + var expOld, expNew string + w.testData("ChangePublicPassphrase.in.old", &expOld) + w.testData("ChangePublicPassphrase.in.new", &expNew) + if string(old) != expOld { + return fmt.Errorf("wrong old passphrase. expected %s, got %s", expOld, string(old)) + } + if string(new) != expNew { + return fmt.Errorf("wrong new passphrase. expected %s, got %s", expNew, string(new)) + } + return nil +} + +func (w *testWallet) ChangePassphrases(publicOld, publicNew, privateOld, privateNew []byte) error { + var expPublicOld, expPublicNew, expPrivateOld, expPrivateNew string + w.testData("ChangePassphrases.in.publicOld", &expPublicOld) + w.testData("ChangePassphrases.in.publicNew", &expPublicNew) + w.testData("ChangePassphrases.in.privateOld", &expPrivateOld) + w.testData("ChangePassphrases.in.privateNew", &expPrivateNew) + if string(publicOld) != expPublicOld { + return fmt.Errorf("wrong old public passphrase. expected %s, got %s", expPublicOld, string(publicOld)) + } + if string(publicNew) != expPublicNew { + return fmt.Errorf("wrong new public passphrase. expected %s, got %s", expPublicNew, string(publicNew)) + } + if string(privateOld) != expPrivateOld { + return fmt.Errorf("wrong old private passphrase. expected %s, got %s", expPrivateOld, string(privateOld)) + } + if string(privateNew) != expPrivateNew { + return fmt.Errorf("wrong new private passphrase. expected %s, got %s", expPrivateNew, string(privateNew)) + } + return nil +} + +func (w *testWallet) AccountAddresses(account uint32) (addrs []btcutil.Address, err error) { + var expAcct uint32 + w.testData("AccountAddresses.in.acct", &expAcct) + if account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + + var outAddr1, outAddr2 string + w.testData("AccountAddresses.out.addr.1", &outAddr1) + w.testData("AccountAddresses.out.addr.2", &outAddr2) + + addr1, _ := btcutil.DecodeAddress(outAddr1, w.params) + addr2, _ := btcutil.DecodeAddress(outAddr2, w.params) + + return []btcutil.Address{addr1, addr2}, nil +} + +func (w *testWallet) CalculateBalance(confirms int32) (btcutil.Amount, error) { + var expConfirms int32 + w.testData("CalculateBalance.in.confirms", &expConfirms) + + if expConfirms != confirms { + return -1, fmt.Errorf("wrong confirms. expected %d, got %d", expConfirms, confirms) + } + + var bal int64 + w.testData("CalculateBalance.out", &bal) + return btcutil.Amount(bal), nil +} + +func (w *testWallet) CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) { + var expAcct uint32 + var expConfirms int32 + w.testData("CalculateAccountBalances.in.acct", &expAcct) + w.testData("CalculateAccountBalances.in.confirms", &expConfirms) + + if expConfirms != confirms { + return wallet.Balances{}, fmt.Errorf("wrong confirms. expected %d, got %d", expConfirms, confirms) + } + + if account != expAcct { + return wallet.Balances{}, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + + var total, spendable, immature int64 + + w.testData("CalculateAccountBalances.out.total", &total) + w.testData("CalculateAccountBalances.out.spendable", &spendable) + w.testData("CalculateAccountBalances.out.immatureReward", &immature) + return wallet.Balances{ + Total: btcutil.Amount(total), + Spendable: btcutil.Amount(spendable), + ImmatureReward: btcutil.Amount(immature), + }, nil +} + +func (w *testWallet) CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) { + var expAcct, expPurpose, expCoin uint32 + w.testData("CurrentAddress.in.acct", &expAcct) + w.testData("CurrentAddress.in.purpose", &expPurpose) + w.testData("CurrentAddress.in.coin", &expCoin) + if account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + + var outAddrStr string + w.testData("CurrentAddress.out.addr", &outAddrStr) + addr, _ := btcutil.DecodeAddress(outAddrStr, w.params) + return addr, nil +} + +func (w *testWallet) PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) { + var expAddr string + w.testData("PubKeyForAddress.in.addr", &expAddr) + if a.String() != expAddr { + return nil, fmt.Errorf("wrong address. expected %s, got %s", expAddr, a.String()) + } + + var pkB Bytes + w.testData("PubKeyForAddress.out.pubkey", &pkB) + + pubKey, err := btcec.ParsePubKey(pkB, btcec.S256()) + if err != nil { + return nil, fmt.Errorf("pubkey decode error %s: %v", hex.EncodeToString(pkB), err) + } + return pubKey, nil +} + +func (w *testWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) { + var expAddr string + w.testData("PrivKeyForAddress.in.addr", &expAddr) + + var privB Bytes + w.testData("PrivKeyForAddress.out.privkey", &privB) + + priv, _ := btcec.PrivKeyFromBytes(btcec.S256(), privB) + + return priv, nil +} + +func (w *testWallet) LabelTransaction(hash chainhash.Hash, label string, overwrite bool) error { + var expHash Bytes + var expLabel string + var expOverwrite bool + w.testData("LabelTransaction.in.h", &expHash) + w.testData("LabelTransaction.in.label", &expLabel) + w.testData("LabelTransaction.in.overwrite", &expOverwrite) + if !bytes.Equal(hash[:], expHash) { + return fmt.Errorf("wrong tx hash. wanted %x, got %x", expHash[:], hash[:]) + } + if expLabel != label { + return fmt.Errorf("wrong tx hash. wanted %s, got %s", expLabel, label) + } + if expOverwrite != overwrite { + return fmt.Errorf("wrong overwrite. wanted %t, got %t", expOverwrite, overwrite) + } + + return nil +} + +func (w *testWallet) HaveAddress(a btcutil.Address) (bool, error) { + var expAddr string + w.testData("HaveAddress.in", &expAddr) + if expAddr != a.String() { + return false, fmt.Errorf("wrong address. expected %s, got %s", expAddr, a.String()) + } + + var have bool + w.testData("HaveAddress.out", &have) + return have, nil +} + +func (w *testWallet) AccountOfAddress(a btcutil.Address) (uint32, error) { + var expAddr string + w.testData("AccountOfAddress.in.addr", &expAddr) + if expAddr != a.String() { + return 0, fmt.Errorf("wrong address. expected %s, got %s", expAddr, a.String()) + } + + var acct uint32 + w.testData("AccountOfAddress.out.acct", &acct) + return acct, nil +} + +type tManagedAddress struct { + acct uint32 + addr btcutil.Address + addrHash []byte + imported bool + internal bool + compressed bool + addrType waddrmgr.AddressType +} + +func (a *tManagedAddress) Account() uint32 { + return a.acct +} + +func (a *tManagedAddress) Address() btcutil.Address { + return a.addr +} + +func (a *tManagedAddress) AddrHash() []byte { + return a.addrHash +} + +func (a *tManagedAddress) Imported() bool { + return a.imported +} + +func (a *tManagedAddress) Internal() bool { + return a.internal +} + +func (a *tManagedAddress) Compressed() bool { + return a.compressed +} + +func (a *tManagedAddress) Used(ns walletdb.ReadBucket) bool { + return false +} + +func (a *tManagedAddress) AddrType() waddrmgr.AddressType { + return a.addrType +} + +func (w *testWallet) AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress, error) { + var expAddr string + w.testData("AddressInfo.in.addr", &expAddr) + if expAddr != a.String() { + return nil, fmt.Errorf("wrong address. expected %s, got %s", expAddr, a.String()) + } + + var acct uint32 + var addrStr string + var addrHash Bytes + var imported, internal, compressed bool + var addrType uint8 + w.testData("AddressInfo.out.acct", &acct) + w.testData("AddressInfo.out.addr", &addrStr) + addr, _ := btcutil.DecodeAddress(addrStr, w.params) + w.testData("AddressInfo.out.addrHash", &addrHash) + w.testData("AddressInfo.out.imported", &imported) + w.testData("AddressInfo.out.internal", &internal) + w.testData("AddressInfo.out.compressed", &compressed) + w.testData("AddressInfo.out.addrType", &addrType) + + return &tManagedAddress{ + acct: acct, + addr: addr, + addrHash: addrHash, + imported: imported, + internal: internal, + compressed: compressed, + addrType: waddrmgr.AddressType(addrType), + }, nil +} + +func (w *testWallet) AccountNumber(scope waddrmgr.KeyScope, accountName string) (uint32, error) { + var expPurpose, expCoin uint32 + var expAcctName string + w.testData("AccountNumber.in.scope.purpose", &expPurpose) + w.testData("AccountNumber.in.scope.coin", &expCoin) + w.testData("AccountNumber.in.accountName", &expAcctName) + if scope.Purpose != expPurpose { + return 0, fmt.Errorf("wrong purpose. expected %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return 0, fmt.Errorf("wrong coin. expected %d, got %d", expCoin, scope.Coin) + } + if accountName != expAcctName { + return 0, fmt.Errorf("wrong account name. expected %s, got %s", expAcctName, accountName) + } + + var acct uint32 + w.testData("AccountNumber.out.acct", &acct) + return acct, nil +} + +func (w *testWallet) AccountName(scope waddrmgr.KeyScope, accountNumber uint32) (string, error) { + var expPurpose, expCoin, expAcct uint32 + w.testData("AccountName.in.scope.purpose", &expPurpose) + w.testData("AccountName.in.scope.coin", &expCoin) + w.testData("AccountName.in.acct", &expAcct) + if scope.Purpose != expPurpose { + return "", fmt.Errorf("wrong purpose. expected %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return "", fmt.Errorf("wrong coin. expected %d, got %d", expCoin, scope.Coin) + } + if accountNumber != expAcct { + return "", fmt.Errorf("wrong account. wanted %d, got %d", expAcct, accountNumber) + } + + var accountName string + w.testData("AccountName.out.accountName", &accountName) + return accountName, nil +} + +func (w *testWallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) { + var expPurpose, expCoin, expAcct uint32 + w.testData("AccountProperties.in.scope.purpose", &expPurpose) + w.testData("AccountProperties.in.scope.coin", &expCoin) + w.testData("AccountProperties.in.acct", &expAcct) + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. expected %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. expected %d, got %d", expCoin, scope.Coin) + } + if acct != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, acct) + } + + var accountNumber, externalKeyCount, internalKeyCount, importedKeyCount uint32 + var accountName string + w.testData("AccountProperties.out.accountNumber", &accountNumber) + w.testData("AccountProperties.out.accountName", &accountName) + w.testData("AccountProperties.out.externalKeyCount", &externalKeyCount) + w.testData("AccountProperties.out.internalKeyCount", &internalKeyCount) + w.testData("AccountProperties.out.importedKeyCount", &importedKeyCount) + return &waddrmgr.AccountProperties{ + AccountNumber: accountNumber, + AccountName: accountName, + ExternalKeyCount: externalKeyCount, + InternalKeyCount: internalKeyCount, + ImportedKeyCount: importedKeyCount, + }, nil +} + +func (w *testWallet) RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error { + var expPurpose, expCoin, expAcct uint32 + var expNewName string + w.testData("RenameAccount.in.scope.purpose", &expPurpose) + w.testData("RenameAccount.in.scope.coin", &expCoin) + w.testData("RenameAccount.in.acct", &expAcct) + w.testData("RenameAccount.in.newName", &expNewName) + if scope.Purpose != expPurpose { + return fmt.Errorf("wrong purpose. expected %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return fmt.Errorf("wrong coin. expected %d, got %d", expCoin, scope.Coin) + } + if account != expAcct { + return fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + if newName != expNewName { + return fmt.Errorf("wrong name. wanted %s, got %s", expNewName, newName) + } + return nil +} + +func (w *testWallet) NextAccount(scope waddrmgr.KeyScope, name string) (uint32, error) { + var expPurpose, expCoin uint32 + var expAcctName string + w.testData("NextAccount.in.scope.purpose", &expPurpose) + w.testData("NextAccount.in.scope.coin", &expCoin) + w.testData("NextAccount.in.accountName", &expAcctName) + if scope.Purpose != expPurpose { + return 0, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return 0, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + if name != expAcctName { + return 0, fmt.Errorf("wrong name. wanted %s, got %s", expAcctName, name) + } + + var acct uint32 + w.testData("NextAccount.out.acct", &acct) + return acct, nil +} + +func (w *testWallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) { + var expStart, expEnd, expSyncHeight int32 + w.testData("ListSinceBlock.in.start", &expStart) + w.testData("ListSinceBlock.in.end", &expEnd) + w.testData("ListSinceBlock.in.syncHeight", &expSyncHeight) + if start != expStart { + return nil, fmt.Errorf("wrong start. wanted %d, got %d", expStart, start) + } + if end != expEnd { + return nil, fmt.Errorf("wrong end. wanted %d, got %d", expEnd, end) + } + if syncHeight != expSyncHeight { + return nil, fmt.Errorf("wrong syncHeight. wanted %d, got %d", expSyncHeight, syncHeight) + } + + var blockTime int64 + w.testData("ListSinceBlock.out.blockTime", &blockTime) + + return []btcjson.ListTransactionsResult{{ + BlockTime: blockTime, + }}, nil +} + +func (w *testWallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) { + var expFrom, expCount int + w.testData("ListTransactions.in.skip", &expFrom) + w.testData("ListTransactions.in.count", &expCount) + if from != expFrom { + return nil, fmt.Errorf("wrong from. wanted %d, got %d", expFrom, from) + } + if count != expCount { + return nil, fmt.Errorf("wrong count. wanted %d, got %d", expCount, count) + } + + var confs int64 + w.testData("ListTransactions.out.confs", &confs) + return []btcjson.ListTransactionsResult{{ + Confirmations: confs, + }}, nil +} + +func (w *testWallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjson.ListTransactionsResult, error) { + var pkHash []byte + for hStr := range pkHashes { + pkHash = []byte(hStr) + break + } + var expPkHash Bytes + w.testData("ListAddressTransactions.in.pkHash", &expPkHash) + if !bytes.Equal(expPkHash, pkHash) { + return nil, fmt.Errorf("wrong pkHash. wanted %x, got %x", expPkHash[:], pkHash) + } + + var timeReceived int64 + w.testData("ListAddressTransactions.out.timeReceived", &timeReceived) + return []btcjson.ListTransactionsResult{{ + TimeReceived: timeReceived, + }}, nil +} + +func (w *testWallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) { + var vout uint32 + w.testData("ListAllTransactions.out.vout", &vout) + return []btcjson.ListTransactionsResult{{ + Vout: vout, + }}, nil +} + +func (w *testWallet) Accounts(scope waddrmgr.KeyScope) (*wallet.AccountsResult, error) { + var expPurpose, expCoin uint32 + w.testData("Accounts.in.purpose", &expPurpose) + w.testData("Accounts.in.coin", &expCoin) + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + + var blockHashB Bytes + var blockHeight int32 + var balance int64 + var acct uint32 + w.testData("Accounts.out.blockHash", &blockHashB) + w.testData("Accounts.out.blockHeight", &blockHeight) + w.testData("Accounts.out.balance", &balance) + w.testData("Accounts.out.acct", &acct) + var blockHash chainhash.Hash + copy(blockHash[:], blockHashB) + return &wallet.AccountsResult{ + Accounts: []wallet.AccountResult{{ + AccountProperties: waddrmgr.AccountProperties{ + AccountNumber: acct, + }, + TotalBalance: btcutil.Amount(balance), + }}, + CurrentBlockHash: &blockHash, + CurrentBlockHeight: blockHeight, + }, nil +} + +func (w *testWallet) AccountBalances(scope waddrmgr.KeyScope, requiredConfs int32) ([]wallet.AccountBalanceResult, error) { + var expConfs int32 + var expPurpose, expCoin uint32 + w.testData("AccountBalances.in.confs", &expConfs) + w.testData("AccountBalances.in.purpose", &expPurpose) + w.testData("AccountBalances.in.coin", &expCoin) + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + if requiredConfs != expConfs { + return nil, fmt.Errorf("wrong confs. wanted %d, got %d", expConfs, requiredConfs) + } + + var acctNumber uint32 + var bal int64 + var acctName string + w.testData("AccountBalances.out.acctNumber", &acctNumber) + w.testData("AccountBalances.out.acctName", &acctName) + w.testData("AccountBalances.out.balance", &bal) + return []wallet.AccountBalanceResult{{ + AccountNumber: acctNumber, + AccountName: acctName, + AccountBalance: btcutil.Amount(bal), + }}, nil +} + +func (w *testWallet) ListUnspent(minconf, maxconf int32, addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) { + var expMinConf, expMaxConf int32 + var expAddr string + w.testData("ListUnspent.in.minConf", &expMinConf) + w.testData("ListUnspent.in.maxConf", &expMaxConf) + w.testData("ListUnspent.in.addr", &expAddr) + if minconf != expMinConf { + return nil, fmt.Errorf("wrong minconf. wanted %d, got %d", expMinConf, minconf) + } + if maxconf != expMaxConf { + return nil, fmt.Errorf("wrong maxconf. wanted %d, got %d", expMaxConf, maxconf) + } + var addr string + for addr = range addresses { + break + } + if addr != expAddr { + return nil, fmt.Errorf("wrong addr. wanted %s, got %s", expAddr, addr) + } + + var scriptPubKey string + w.testData("ListUnspent.out.scriptPubKey", &scriptPubKey) + return []*btcjson.ListUnspentResult{{ + ScriptPubKey: scriptPubKey, + }}, nil +} + +func (w *testWallet) DumpPrivKeys() ([]string, error) { + var privKey string + w.testData("DumpPrivKeys.out", &privKey) + return []string{privKey}, nil +} + +func (w *testWallet) DumpWIFPrivateKey(a btcutil.Address) (string, error) { + var expAddr string + w.testData("DumpWIFPrivateKey.in.addr", &expAddr) + if expAddr != a.String() { + return "", fmt.Errorf("wrong address. expected %s, got %s", expAddr, a.String()) + } + + var encWIF string + w.testData("DumpWIFPrivateKey.out.wif", &encWIF) + return encWIF, nil +} + +func (w *testWallet) ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF, bs *waddrmgr.BlockStamp, rescan bool) (string, error) { + var expPurpose, expCoin uint32 + var expBlockHeight int32 + var expBlockHash Bytes + var expStamp int64 + var expEncWIF string + var expRescan bool + w.testData("ImportPrivateKey.in.purpose", &expPurpose) + w.testData("ImportPrivateKey.in.coin", &expCoin) + w.testData("ImportPrivateKey.in.wif", &expEncWIF) + w.testData("ImportPrivateKey.in.blockHeight", &expBlockHeight) + w.testData("ImportPrivateKey.in.blockHash", &expBlockHash) + w.testData("ImportPrivateKey.in.blockStamp", &expStamp) + w.testData("ImportPrivateKey.in.rescan", &expRescan) + if scope.Purpose != expPurpose { + return "", fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return "", fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + if wif.String() != expEncWIF { + return "", fmt.Errorf("wrong wif. wanted %s, got %s", expEncWIF, wif.String()) + } + if bs.Height != expBlockHeight { + return "", fmt.Errorf("wrong block height. wanted %d, got %d", expBlockHeight, bs.Height) + } + if !bytes.Equal(bs.Hash[:], expBlockHash[:]) { + return "", fmt.Errorf("wrong block hash. wanted %x, got %x", expBlockHash[:], bs.Hash[:]) + } + if bs.Timestamp.Unix() != expStamp { + return "", fmt.Errorf("wrong block time. wanted %d, got %d", expStamp, bs.Timestamp.Unix()) + } + if rescan != expRescan { + return "", fmt.Errorf("wrong rescan. wanted %t, got %t", expRescan, rescan) + } + + var addrStr string + w.testData("ImportPrivateKey.out.addr", &addrStr) + return addrStr, nil +} + +func (w *testWallet) LockedOutpoint(op wire.OutPoint) bool { + var expHash Bytes + var expIndex uint32 + w.testData("LockedOutpoint.in.hash", &expHash) + w.testData("LockedOutpoint.in.index", &expIndex) + if !bytes.Equal(op.Hash[:], expHash) { + panic(fmt.Sprintf("LockedOutpoint: wrong tx hash. wanted %x, got %x", expHash[:], op.Hash[:])) + } + if op.Index != expIndex { + panic(fmt.Sprintf("LockedOutpoint: wrong tx index. wanted %d, got %d", expIndex, op.Index)) + } + + var locked bool + w.testData("LockedOutpoint.out.locked", &locked) + return locked +} + +func (w *testWallet) LockOutpoint(op wire.OutPoint) { + var expHash Bytes + var expIndex uint32 + w.testData("LockOutpoint.in.hash", &expHash) + w.testData("LockOutpoint.in.index", &expIndex) + if !bytes.Equal(op.Hash[:], expHash) { + panic(fmt.Sprintf("LockOutpoint: wrong tx hash. wanted %x, got %x", expHash[:], op.Hash[:])) + } + if op.Index != expIndex { + panic(fmt.Sprintf("LockOutpoint: wrong tx index. wanted %d, got %d", expIndex, op.Index)) + } +} + +func (w *testWallet) UnlockOutpoint(op wire.OutPoint) { + var expHash Bytes + var expIndex uint32 + w.testData("UnlockOutpoint.in.hash", &expHash) + w.testData("UnlockOutpoint.in.index", &expIndex) + if !bytes.Equal(op.Hash[:], expHash) { + panic(fmt.Sprintf("UnlockOutpoint: wrong tx hash. wanted %x, got %x", expHash[:], op.Hash[:])) + } + if op.Index != expIndex { + panic(fmt.Sprintf("UnlockOutpoint: wrong tx index. wanted %d, got %d", expIndex, op.Index)) + } +} + +func (w *testWallet) ResetLockedOutpoints() {} + +func (w *testWallet) LockedOutpoints() []btcjson.TransactionInput { + var hexHash Bytes + w.testData("LockedOutpoints.out.hash", &hexHash) + + var h chainhash.Hash + copy(h[:], hexHash) + + return []btcjson.TransactionInput{{ + Txid: h.String(), + }} +} + +func (w *testWallet) LeaseOutput(id wtxmgr.LockID, op wire.OutPoint) (time.Time, error) { + var expLockID, expHash Bytes + var expIdx uint32 + w.testData("LeaseOutput.in.hash", &expHash) + w.testData("LeaseOutput.in.index", &expIdx) + w.testData("LeaseOutput.in.lockID", &expLockID) + if !bytes.Equal(op.Hash[:], expHash) { + return time.Time{}, fmt.Errorf("wrong tx hash. wanted %x, got %x", expHash[:], op.Hash[:]) + } + if op.Index != expIdx { + return time.Time{}, fmt.Errorf("wrong tx index. wanted %d, got %d", expIdx, op.Index) + } + if !bytes.Equal(id[:], expLockID) { + return time.Time{}, fmt.Errorf("wrong lock ID. wanted %x, got %x", id[:], expLockID[:]) + } + + var stamp int64 + w.testData("LeaseOutput.out", &stamp) + return time.Unix(stamp, 0), nil +} + +func (w *testWallet) ReleaseOutput(id wtxmgr.LockID, op wire.OutPoint) error { + var expLockID, expHash Bytes + var expIdx uint32 + w.testData("ReleaseOutput.in.hash", &expHash) + w.testData("ReleaseOutput.in.index", &expIdx) + w.testData("ReleaseOutput.in.lockID", &expLockID) + if !bytes.Equal(op.Hash[:], expHash) { + return fmt.Errorf("wrong tx hash. wanted %x, got %x", expHash[:], op.Hash[:]) + } + if op.Index != expIdx { + return fmt.Errorf("wrong tx index. wanted %d, got %d", expIdx, op.Index) + } + if !bytes.Equal(id[:], expLockID) { + return fmt.Errorf("wrong lock ID. wanted %x, got %x", id[:], expLockID[:]) + } + return nil +} + +func (w *testWallet) SortedActivePaymentAddresses() ([]string, error) { + var addr string + w.testData("SortedActivePaymentAddresses.out", &addr) + return []string{addr}, nil +} + +func (w *testWallet) NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) { + var expAcct, expPurpose, expCoin uint32 + w.testData("NewAddress.in.acct", &expAcct) + w.testData("NewAddress.in.purpose", &expPurpose) + w.testData("NewAddress.in.coin", &expCoin) + if account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + + var addrStr string + w.testData("NewAddress.out.addr", &addrStr) + addr, _ := btcutil.DecodeAddress(addrStr, w.params) + return addr, nil +} + +func (w *testWallet) NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) { + var expAcct, expPurpose, expCoin uint32 + w.testData("NewChangeAddress.in.acct", &expAcct) + w.testData("NewChangeAddress.in.purpose", &expPurpose) + w.testData("NewChangeAddress.in.coin", &expCoin) + if account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + + var addrStr string + w.testData("NewChangeAddress.out.addr", &addrStr) + addr, _ := btcutil.DecodeAddress(addrStr, w.params) + return addr, nil +} + +func (w *testWallet) TotalReceivedForAccounts(scope waddrmgr.KeyScope, minConf int32) ([]wallet.AccountTotalReceivedResult, error) { + var expPurpose, expCoin uint32 + var expConfs int32 + w.testData("TotalReceivedForAccounts.in.purpose", &expPurpose) + w.testData("TotalReceivedForAccounts.in.coin", &expCoin) + w.testData("TotalReceivedForAccounts.in.minConf", &expConfs) + if scope.Purpose != expPurpose { + return nil, fmt.Errorf("wrong purpose. wanted %d, got %d", expPurpose, scope.Purpose) + } + if scope.Coin != expCoin { + return nil, fmt.Errorf("wrong coin. wanted %d, got %d", expCoin, scope.Coin) + } + if minConf != expConfs { + return nil, fmt.Errorf("wrong minConf. wanted %d, got %d", expConfs, minConf) + } + + var accountNumber uint32 + var lastConfirmation int32 + var accountName string + var totalReceived int64 + w.testData("TotalReceivedForAccounts.out.accountNumber", &accountNumber) + w.testData("TotalReceivedForAccounts.out.accountName", &accountName) + w.testData("TotalReceivedForAccounts.out.totalReceived", &totalReceived) + w.testData("TotalReceivedForAccounts.out.lastConfirmation", &lastConfirmation) + return []wallet.AccountTotalReceivedResult{{ + AccountNumber: accountNumber, + AccountName: accountName, + TotalReceived: btcutil.Amount(totalReceived), + LastConfirmation: lastConfirmation, + }}, nil +} + +func (w *testWallet) TotalReceivedForAddr(a btcutil.Address, minConf int32) (btcutil.Amount, error) { + var expAddr string + var expConfs int32 + w.testData("TotalReceivedForAddr.in.addr", &expAddr) + w.testData("TotalReceivedForAddr.in.minConf", &expConfs) + if expAddr != a.String() { + return 0, fmt.Errorf("wrong address. expected %s, got %s", expAddr, a.String()) + } + if minConf != expConfs { + return 0, fmt.Errorf("wrong minConf. wanted %d, got %d", expConfs, minConf) + } + var total int64 + w.testData("TotalReceivedForAddr.out.amt", &total) + return btcutil.Amount(total), nil +} + +func (w *testWallet) SendOutputs(outputs []*wire.TxOut, account uint32, minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) { + if len(outputs) != 1 { + return nil, fmt.Errorf("expected 1 output, got %d", len(outputs)) + } + output := outputs[0] + + var expVal int64 + var expSatPerKb float64 + var expPkScript Bytes + var expAcct uint32 + var expConfs int32 + var expLabel string + w.testData("SendOutputs.in.value", &expVal) + w.testData("SendOutputs.in.pkScript", &expPkScript) + w.testData("SendOutputs.in.acct", &expAcct) + w.testData("SendOutputs.in.minconf", &expConfs) + w.testData("SendOutputs.in.satPerKb", &expSatPerKb) + w.testData("SendOutputs.in.label", &expLabel) + if output.Value != expVal { + return nil, fmt.Errorf("wrong output value. wanted %d, got %d", expVal, output.Value) + } + if !bytes.Equal(output.PkScript, expPkScript) { + return nil, fmt.Errorf("wrong pubkey script. wanted %x, got %x", expPkScript[:], output.PkScript) + } + if account != expAcct { + return nil, fmt.Errorf("wrong account. wanted %d, got %d", expAcct, account) + } + if minconf != expConfs { + return nil, fmt.Errorf("wrong minconf. wanted %d, got %d", expConfs, minconf) + } + expFeeRate, err := btcutil.NewAmount(expSatPerKb) + if err != nil { + return nil, fmt.Errorf("NewAmount error: %v", err) + } + if satPerKb != expFeeRate { + return nil, fmt.Errorf("wrong satPerKb. wanted %d, got %d", expFeeRate, satPerKb) + } + if expLabel != label { + return nil, fmt.Errorf("wrong label. wanted %s, got %s", expLabel, label) + } + + var txB Bytes + w.testData("SendOutputs.out.tx", &txB) + tx := &wire.MsgTx{} + err = tx.Deserialize(bytes.NewBuffer(txB)) + if err != nil { + return nil, fmt.Errorf("test tx deserialize error: %v", err) + } + + return tx, nil +} + +func (w *testWallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte, + additionalKeysByAddress map[string]*btcutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error) { + + var expTxB, expPrevHash, expPrevScript, expRedeemScript Bytes + var expHashType, expPrevIdx uint32 + var expKeyAddr, expWIF, expScriptAddr string + w.testData("SignTransaction.in.tx", &expTxB) + w.testData("SignTransaction.in.hashType", &expHashType) + w.testData("SignTransaction.in.prevout.hash", &expPrevHash) + w.testData("SignTransaction.in.prevout.idx", &expPrevIdx) + w.testData("SignTransaction.in.prevout.script", &expPrevScript) + w.testData("SignTransaction.in.key.addr", &expKeyAddr) + w.testData("SignTransaction.in.key.wif", &expWIF) + w.testData("SignTransaction.in.script.addr", &expScriptAddr) + w.testData("SignTransaction.in.script.script", &expRedeemScript) + + txB, err := serializeMsgTx(tx) + if err != nil { + return nil, fmt.Errorf("error serializing input transaction: %v", err) + } + if !bytes.Equal(expTxB, txB) { + return nil, fmt.Errorf("wrong tx. wanted %x, got %x", expTxB[:], txB) + } + if hashType != txscript.SigHashType(expHashType) { + return nil, fmt.Errorf("wrong hash type. wanted %d, got %d", expHashType, hashType) + } + if len(additionalPrevScripts) != 1 { + return nil, fmt.Errorf("expected 1 additionalPrevScripts, got %d", len(additionalPrevScripts)) + } + var op wire.OutPoint + var prevScript []byte + for op, prevScript = range additionalPrevScripts { + break + } + if !bytes.Equal(op.Hash[:], expPrevHash) { + return nil, fmt.Errorf("wrong prev hash. wanted %x, got %x", expPrevHash, op.Hash[:]) + } + if op.Index != expPrevIdx { + return nil, fmt.Errorf("wrong prev index. wanted %d, got %d", expPrevIdx, op.Index) + } + if !bytes.Equal(prevScript, expPrevScript) { + return nil, fmt.Errorf("wrong previous script. expected %x, got %x", expPrevScript[:], prevScript) + } + if len(additionalKeysByAddress) != 1 { + return nil, fmt.Errorf("expected 1 additionalKeysByAddress, got %d", len(additionalKeysByAddress)) + } + var addrStr string + var wif *btcutil.WIF + for addrStr, wif = range additionalKeysByAddress { + break + } + if addrStr != expKeyAddr { + return nil, fmt.Errorf("wrong key addr. wanted %s, got %s", expKeyAddr, addrStr) + } + if wif.String() != expWIF { + return nil, fmt.Errorf("wrong wif. wanted %s, got %s", expWIF, wif.String()) + } + if len(p2shRedeemScriptsByAddress) != 1 { + return nil, fmt.Errorf("expected 1 p2shRedeemScriptsByAddress, got %d", len(p2shRedeemScriptsByAddress)) + } + var scriptAddr string + var redeemScript []byte + for scriptAddr, redeemScript = range p2shRedeemScriptsByAddress { + break + } + if scriptAddr != expScriptAddr { + return nil, fmt.Errorf("wrong wif. wanted %s, got %s", expScriptAddr, scriptAddr) + } + if !bytes.Equal(redeemScript, expRedeemScript) { + return nil, fmt.Errorf("wrong redeem script. expected %x, got %x", expRedeemScript[:], redeemScript) + } + + var sigScript Bytes + w.testData("SignTransaction.out.sigScript", &sigScript) + if len(tx.TxIn) != 1 { + return nil, fmt.Errorf("expected 1 input, got %d", len(tx.TxIn)) + } + tx.TxIn[0].SignatureScript = sigScript + + return nil, nil +} + +func (w *testWallet) PublishTransaction(tx *wire.MsgTx, label string) error { + var expTxB Bytes + var expLabel string + w.testData("PublishTransaction.in.tx", &expTxB) + w.testData("PublishTransaction.in.label", &expLabel) + txB, err := serializeMsgTx(tx) + if err != nil { + return fmt.Errorf("error serializing input transaction: %v", err) + } + if !bytes.Equal(expTxB, txB) { + return fmt.Errorf("wrong tx. wanted %x, got %x", expTxB[:], txB) + } + if expLabel != label { + return fmt.Errorf("wrong tx hash. wanted %s, got %s", expLabel, label) + } + return nil +} diff --git a/decred/decred/btc/btcwallet/libbtcwallet/types.go b/decred/decred/btc/btcwallet/libbtcwallet/types.go new file mode 100644 index 00000000..03691f3e --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/types.go @@ -0,0 +1,109 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" +) + +type scriptValue struct { + Script Bytes `json:"script"` + Value int64 `json:"value"` +} + +type hashIndex struct { + Hash Bytes `json:"hash"` + Index int64 `json:"index"` +} + +// Bytes is a byte slice that marshals to and unmarshals from a hexadecimal +// string. The default go behavior is to marshal []byte to a base-64 string. +type Bytes []byte + +// String return the hex encoding of the Bytes. +func (b Bytes) String() string { + return hex.EncodeToString(b) +} + +// MarshalJSON satisfies the json.Marshaller interface, and will marshal the +// bytes to a hex string. +func (b Bytes) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(b)) +} + +// UnmarshalJSON satisfies the json.Unmarshaler interface, and expects a UTF-8 +// encoding of a hex string in double quotes. +func (b *Bytes) UnmarshalJSON(encHex []byte) (err error) { + if len(encHex) < 2 { + return fmt.Errorf("marshalled Bytes, %q, not valid", string(encHex)) + } + if encHex[0] != '"' || encHex[len(encHex)-1] != '"' { + return fmt.Errorf("marshalled Bytes, %q, not quoted", string(encHex)) + } + // DecodeString overallocates by at least double, and it makes a copy. + src := encHex[1 : len(encHex)-1] + dst := make([]byte, len(src)/2) + _, err = hex.Decode(dst, src) + if err == nil { + *b = dst + } + return err +} + +type jsonTransactionSummary struct { + Hash Bytes `json:"hash"` + Transaction Bytes `json:"transaction"` + MyInputs []jsonTransactionSummaryInput `json:"myInputs"` + MyOutputs []jsonTransactionSummaryOutput `json:"myOutputs"` + Fee int64 `json:"fee"` + Timestamp int64 `json:"stamp"` + Label string `json:"label"` + // Confs int32 `json:"confs"` + // BlockHash Bytes `json:"blockHash"` +} + +type jsonTransactionSummaryInput struct { + Index uint32 `json:"index"` + PreviousAccount uint32 `json:"previousAccount"` + PreviousAmount int64 `json:"previousAmount"` +} + +type jsonTransactionSummaryOutput struct { + Index uint32 `json:"index"` + Account uint32 `json:"acct"` + Internal bool `json:"internal"` +} + +type noteBlock struct { + Hash Bytes `json:"hash"` + Height int32 `json:"height"` + Timestamp int64 `json:"stamp"` + Transactions []jsonTransactionSummary `json:"transactions"` +} + +type noteBalance struct { + Acct uint32 `json:"acct"` + TotalBalance int64 `json:"totalBalance"` +} + +type jsonTransactionNotifications struct { + AttachedBlocks []noteBlock `json:"attachedBlocks"` + DetachedBlocks []Bytes `json:"detachedBlocks"` // headers + UnminedTransactions []jsonTransactionSummary `json:"unminedTransactions"` + NewBalances []noteBalance `json:"newBalances"` +} + +type accountNote struct { + AccountNumber uint32 `json:"accountNumber"` + AccountName string `json:"accountName"` + ExternalKeyCount uint32 `json:"externalKeyCount"` + InternalKeyCount uint32 `json:"internalKeyCount"` + ImportedKeyCount uint32 `json:"importedKeyCount"` +} + +type spentnessNote struct { + Hash Bytes `json:"hash"` + Vout uint32 `json:"vout"` + SpenderHash Bytes `json:"spenderHash"` + SpenderVin uint32 `json:"spenderVin"` +} diff --git a/decred/decred/btc/btcwallet/libbtcwallet/wallet.go b/decred/decred/btc/btcwallet/libbtcwallet/wallet.go new file mode 100644 index 00000000..a2968abc --- /dev/null +++ b/decred/decred/btc/btcwallet/libbtcwallet/wallet.go @@ -0,0 +1,268 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/lightninglabs/neutrino" +) + +type walletConfig struct { + DBDir string + Net *chaincfg.Params + ConnectPeers []string +} + +func loadWallet(cfg *walletConfig) (*wallet.Wallet, *neutrino.ChainService, error) { + netDir := filepath.Join(cfg.DBDir, cfg.Net.Name) + // timeout and recoverWindow arguments borrowed from btcwallet directly. + loader := wallet.NewLoader(cfg.Net, netDir, true, 60*time.Second, 250) + + exists, err := loader.WalletExists() + if err != nil { + return nil, nil, fmt.Errorf("error verifying wallet existence: %v", err) + } + if !exists { + return nil, nil, fmt.Errorf("wallet not found") + } + + nuetrinoDBPath := filepath.Join(netDir, "neutrino.db") + spvdb, err := walletdb.Create("bdb", nuetrinoDBPath, true, 60*time.Second) + if err != nil { + return nil, nil, fmt.Errorf("Unable to create wallet: %s", err) + } + // defer spvdb.Close() + chainService, err := neutrino.NewChainService(neutrino.Config{ + DataDir: netDir, + Database: spvdb, + ChainParams: *cfg.Net, + ConnectPeers: cfg.ConnectPeers, + // AddPeers: cfg.AddPeers, + }) + if err != nil { + return nil, nil, fmt.Errorf("Couldn't create Neutrino ChainService: %s", err) + } + + chainClient := chain.NewNeutrinoClient(cfg.Net, chainService) + err = chainClient.Start() + if err != nil { + return nil, nil, fmt.Errorf("Couldn't start Neutrino client: %s", err) + } + + loader.RunAfterLoad(func(w *wallet.Wallet) { + w.SynchronizeRPC(chainClient) + }) + + w, err := loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) + if err != nil { + return nil, nil, err + } + + return w, chainService, nil +} + +func convertTxSummary(summary *wallet.TransactionSummary) *jsonTransactionSummary { + + jsonSummary := &jsonTransactionSummary{ + Hash: summary.Hash[:], + Transaction: summary.Transaction, + MyInputs: make([]jsonTransactionSummaryInput, 0, len(summary.MyInputs)), + MyOutputs: make([]jsonTransactionSummaryOutput, 0, len(summary.MyOutputs)), + Fee: int64(summary.Fee), + Timestamp: summary.Timestamp, + Label: summary.Label, + } + + for i := range summary.MyInputs { + input := &summary.MyInputs[i] + jsonSummary.MyInputs = append(jsonSummary.MyInputs, jsonTransactionSummaryInput{ + Index: input.Index, + PreviousAccount: input.PreviousAccount, + PreviousAmount: int64(input.PreviousAmount), + }) + } + + for i := range summary.MyOutputs { + output := &summary.MyOutputs[i] + jsonSummary.MyOutputs = append(jsonSummary.MyOutputs, jsonTransactionSummaryOutput{ + Index: output.Index, + Account: output.Account, + Internal: output.Internal, + }) + } + + return jsonSummary +} + +func notesLoop(ctx context.Context, w *wallet.Wallet) { + txNotes := w.NtfnServer.TransactionNotifications() + defer txNotes.Done() + acctNotes := w.NtfnServer.AccountNotifications() + defer acctNotes.Done() + + // TODO: Get fancier about which accounts to register for. + spendNotes := w.NtfnServer.AccountSpentnessNotifications(0) + defer spendNotes.Done() + // confirmNotes := w.NtfnServer.ConfirmationNotifications(ctx) + + defer fmt.Println("--exiting notesLoop") + + for { + var msg *feedMessage + select { + case note := <-txNotes.C: + + fmt.Printf("--txNote: %+v \n", note) + + noteBlocks := make([]noteBlock, 0, len(note.AttachedBlocks)) + for i := range note.AttachedBlocks { + blk := ¬e.AttachedBlocks[i] + txs := make([]jsonTransactionSummary, 0, len(blk.Transactions)) + for i := range blk.Transactions { + txs = append(txs, *convertTxSummary(&blk.Transactions[i])) + } + noteBlocks = append(noteBlocks, noteBlock{ + Hash: blk.Hash[:], + Height: blk.Height, + Timestamp: blk.Timestamp, + Transactions: txs, + }) + } + + detachedBlocks := make([]Bytes, 0, len(note.DetachedBlocks)) + for i := range note.DetachedBlocks { + detachedBlocks = append(detachedBlocks, note.DetachedBlocks[i][:]) + } + + unminedTxs := make([]jsonTransactionSummary, 0, len(note.UnminedTransactions)) + for i := range note.UnminedTransactions { + unminedTxs = append(unminedTxs, *convertTxSummary(¬e.UnminedTransactions[i])) + } + + bals := make([]noteBalance, 0, len(note.NewBalances)) + for i := range note.NewBalances { + bal := ¬e.NewBalances[i] + bals = append(bals, noteBalance{ + Acct: bal.Account, + TotalBalance: int64(bal.TotalBalance), + }) + } + + msg = &feedMessage{ + FeedID: walletFeedID, + Subject: "tx", + Payload: &jsonTransactionNotifications{ + AttachedBlocks: noteBlocks, + DetachedBlocks: detachedBlocks, + UnminedTransactions: unminedTxs, + NewBalances: bals, + }, + } + + case note := <-acctNotes.C: + + fmt.Printf("--acctNotes: %+v \n", note) + + msg = &feedMessage{ + FeedID: walletFeedID, + Subject: "acct", + Payload: accountNote{ + AccountNumber: note.AccountNumber, + AccountName: note.AccountName, + ExternalKeyCount: note.ExternalKeyCount, + InternalKeyCount: note.InternalKeyCount, + ImportedKeyCount: note.ImportedKeyCount, + }, + } + + case note := <-spendNotes.C: + + fmt.Printf("--spendNotes: %+v \n", note) + + var spendHashB Bytes + spendHash, spendVin, spent := note.Spender() + if spent { + spendHashB = spendHash[:] + } + + h := note.Hash() + + msg = &feedMessage{ + FeedID: walletFeedID, + Subject: "spend", + Payload: spentnessNote{ + Hash: h[:], + Vout: note.Index(), + SpenderHash: spendHashB, + SpenderVin: spendVin, + }, + } + + case <-ctx.Done(): + return + } + + select { + case feedChan <- msg: + case <-time.After(time.Second * 5): + log.Errorf("Failed to send feed message") + } + } +} + +func walletExists(dbDir string, net *chaincfg.Params) (bool, error) { + netDir := filepath.Join(dbDir, net.Name) + return wallet.NewLoader(net, netDir, true, 60*time.Second, 250).WalletExists() +} + +func createWallet(privPass []byte, seed []byte, dbDir string, net *chaincfg.Params) error { + netDir := filepath.Join(dbDir, net.Name) + err := os.MkdirAll(netDir, 0777) + if err != nil { + return fmt.Errorf("error creating wallet directories: %v", err) + } + + loader := wallet.NewLoader(net, netDir, true, 60*time.Second, 250) + defer loader.UnloadWallet() + + // Ascertain the public passphrase. This will either be a value + // specified by the user or the default hard-coded public passphrase if + // the user does not want the additional public data encryption. + pubPass := []byte(wallet.InsecurePubPassphrase) + + log.Infof("Creating the wallet...") + w, err := loader.CreateNewWallet(pubPass, privPass, seed, time.Now()) + if err != nil { + return err + } + + _, err = w.NewAddress(0, waddrmgr.KeyScopeBIP0044) + if err != nil { + log.Errorf("Error creating first address: %v", err) + } + + // fmt.Println("Creating the base '" + spvAcctName + "' account") + // err = w.Unlock(privPass, time.After(time.Minute)) + // if err != nil { + // return nil, err + // } + // _, err = w.NextAccount(waddrmgr.KeyScopeBIP0044, spvAcctName) + // if err != nil { + // return nil, err + // } + // w.Lock() + + return nil +} diff --git a/decred/decred/btc/wire/msgtx.py b/decred/decred/btc/wire/msgtx.py new file mode 100644 index 00000000..6cd36623 --- /dev/null +++ b/decred/decred/btc/wire/msgtx.py @@ -0,0 +1,742 @@ +import hashlib +from typing import List, Union, Optional + +from decred import DecredError +from decred.util.encode import ByteArray +from decred.btc.wire import wire +from decred.dcr.wire import wire as dcrwire + +# chainhash.HashSize in Go +HASH_SIZE = 32 + +# TxVersion is the current latest supported transaction version. +TxVersion = 1 + +# MaxTxInSequenceNum is the maximum sequence number the sequence field +# of a transaction input can be. +MaxTxInSequenceNum = 0xffffffff + +# MaxPrevOutIndex is the maximum index the index field of a previous +# outpoint can be. +MaxPrevOutIndex = 0xffffffff + +# SequenceLockTimeDisabled is a flag that if set on a transaction +# input's sequence number, the sequence number will not be interpreted +# as a relative locktime. +SequenceLockTimeDisabled = 1 << 31 + +# SequenceLockTimeIsSeconds is a flag that if set on a transaction +# input's sequence number, the relative locktime has units of 512 +# seconds. +SequenceLockTimeIsSeconds = 1 << 22 + +# SequenceLockTimeMask is a mask that extracts the relative locktime +# when masked against the transaction input sequence number. +SequenceLockTimeMask = 0x0000ffff + +# SequenceLockTimeGranularity is the defined time based granularity +# for seconds-based relative time locks. When converting from seconds +# to a sequence number, the value is right shifted by this amount, +# therefore the granularity of relative time locks in 512 or 2^9 +# seconds. Enforced relative lock times are multiples of 512 seconds. +SequenceLockTimeGranularity = 9 + +# defaultTxInOutAlloc is the default size used for the backing array for +# transaction inputs and outputs. The array will dynamically grow as needed, +# but this figure is intended to provide enough space for the number of +# inputs and outputs in a typical transaction without needing to grow the +# backing array multiple times. +defaultTxInOutAlloc = 15 + +# minTxInPayload is the minimum payload size for a transaction input. +# PreviousOutPoint.Hash + PreviousOutPoint.Index 4 bytes + Varint for +# SignatureScript length 1 byte + Sequence 4 bytes. +minTxInPayload = 9 + HASH_SIZE + +# maxTxInPerMessage is the maximum number of transactions inputs that +# a transaction which fits into a message could possibly have. +maxTxInPerMessage = (wire.MaxMessagePayload / minTxInPayload) + 1 + +# MinTxOutPayload is the minimum payload size for a transaction output. +# Value 8 bytes + Varint for PkScript length 1 byte. +MinTxOutPayload = 9 + +# maxTxOutPerMessage is the maximum number of transactions outputs that +# a transaction which fits into a message could possibly have. +maxTxOutPerMessage = (wire.MaxMessagePayload / MinTxOutPayload) + 1 + +# minTxPayload is the minimum payload size for a transaction. Note +# that any realistically usable transaction must have at least one +# input or output, but that is a rule enforced at a higher layer, so +# it is intentionally not included here. +# Version 4 bytes + Varint number of transaction inputs 1 byte + Varint +# number of transaction outputs 1 byte + LockTime 4 bytes + min input +# payload + min output payload. +minTxPayload = 10 + +# freeListMaxScriptSize is the size of each buffer in the free list +# that is used for deserializing scripts from the wire before they are +# concatenated into a single contiguous buffers. This value was chosen +# because it is slightly more than twice the size of the vast majority +# of all "standard" scripts. Larger scripts are still deserialized +# properly as the free list will simply be bypassed for them. +freeListMaxScriptSize = 512 + +# freeListMaxItems is the number of buffers to keep in the free list +# to use for script deserialization. This value allows up to 100 +# scripts per transaction being simultaneously deserialized by 125 +# peers. Thus, the peak usage of the free list is 12,500 * 512 = +# 6,400,000 bytes. +freeListMaxItems = 12500 + +# maxWitnessItemsPerInput is the maximum number of witness items to +# be read for the witness data for a single TxIn. This number is +# derived using a possble lower bound for the encoding of a witness +# item: 1 byte for length + 1 byte for the witness item itself, or two +# bytes. This value is then divided by the currently allowed maximum +# "cost" for a transaction. +maxWitnessItemsPerInput = 500000 + +# maxWitnessItemSize is the maximum allowed size for an item within +# an input's witness data. This number is derived from the fact that +# for script validation, each pushed item onto the stack must be less +# than 10k bytes. +maxWitnessItemSize = 11000 + +# TxFlagMarker is the first byte of the FLAG field in a bitcoin tx +# message. It allows decoders to distinguish a regular serialized +# transaction from one that would require a different parsing logic. +# +# Position of FLAG in a bitcoin tx message: +# ┌─────────┬────────────────────┬─────────────┬─────┐ +# │ VERSION │ FLAG │ TX-IN-COUNT │ ... │ +# │ 4 bytes │ 2 bytes (optional) │ varint │ │ +# └─────────┴────────────────────┴─────────────┴─────┘ +# +# Zooming into the FLAG field: +# ┌── FLAG ─────────────┬────────┐ +# │ TxFlagMarker (0x00) │ TxFlag │ +# │ 1 byte │ 1 byte │ +# └─────────────────────┴────────┘ +TxFlagMarker = 0x00 + +# WitnessFlag is a flag specific to witness encoding. If the TxFlagMarker +# is encountered followed by the WitnessFlag, then it indicates a +# transaction has witness data. This allows decoders to distinguish a +# serialized transaction with witnesses from a legacy one. +WitnessFlag = 0x01 + +# BaseEncoding encodes all messages in the default format specified +# for the Bitcoin wire protocol. +BaseEncoding = 1 << 0 + +# WitnessEncoding encodes all messages other than transaction messages +# using the default Bitcoin wire protocol specification. For transaction +# messages, the new encoding format detailed in BIP0144 will be used. +WitnessEncoding = 1 << 1 + + +class OutPoint: + """ + OutPoint defines a Decred data type that is used to track previous + transaction outputs. + """ + + def __init__(self, txHash: ByteArray, idx: int): + self.hash = ( + txHash if txHash else ByteArray(0, length=HASH_SIZE) + ) + self.index = idx + + def __eq__(self, other: 'OutPoint') -> bool: + return ( + self.hash == other.hash + and self.index == other.index + ) + + def txid(self) -> ByteArray: + return reversed(self.hash).hex() + + def dict(self): + return dict( + hash=self.hash.hex(), + index=self.index, + ) + + +class TxIn: + """ + TxIn defines a Bitcoin transaction input. + """ + + def __init__( + self, + previousOutPoint: OutPoint, + sequence: Optional[int] = MaxTxInSequenceNum, + signatureScript: Optional[Union[ByteArray, None]] = None, + witness: Optional[Union[List[ByteArray], None]] = None, + ): + self.previousOutPoint = previousOutPoint # OutPoint + self.sequence = sequence # uint32 + self.signatureScript = signatureScript or ByteArray(b"") + self.witness = witness or [] + + def __eq__(self, ti: 'TxIn') -> bool: + """ + Check whether all fields are equal. + """ + return ( + self.previousOutPoint == ti.previousOutPoint + and self.sequence == ti.sequence + and self.signatureScript == ti.signatureScript + and self.witness == ti.witness # Or leave this off? + ) + + def serializeSize(self) -> int: + """ + serializeSize returns the number of bytes it would take to serialize the + the transaction input. + """ + # Outpoint Hash 32 bytes + Outpoint Index 4 bytes + Sequence 4 bytes + + # serialized varint size for the length of SignatureScript + + # SignatureScript bytes. + return 40 + dcrwire.varIntSerializeSize(len(self.signatureScript)) + len(self.signatureScript) + + def witnessSerializeSize(self) -> int: + """ + serializeSize returns the number of bytes it would take to serialize the + transaction input's witness. + """ + # A varint to signal the number of elements the witness has. + n = dcrwire.varIntSerializeSize(len(self.witness)) + + # For each element in the witness, we'll need a varint to signal the + # size of the element, then finally the number of bytes the element + # itself comprises. + for witItem in self.witness: + n += dcrwire.varIntSerializeSize(len(witItem)) + n += len(witItem) + + return n + + +class TxOut: + """ + TxOut defines a Bitcoin transaction output. + """ + + def __init__( + self, + value: Optional[int] = 0, + pkScript: Optional[Union[ByteArray, None]] = None, + version: Optional[int] = 0, + ): + self.value = value + self.version = version + self.pkScript = pkScript or ByteArray() + + def serializeSize(self): + """ + SerializeSize returns the number of bytes it would take to serialize the + the transaction output. + """ + return 8 + dcrwire.varIntSerializeSize(len(self.pkScript)) + len(self.pkScript) + + def __eq__(self, to: 'TxOut') -> bool: + """ + Check for all identical fields. + """ + return ( + self.value == to.value + and self.version == to.version + and self.pkScript == to.pkScript + ) + + +class MsgTx: + """ + MsgTx implements the Message interface and represents a Bitcoin tx message. + It is used to deliver transaction information in response to a getdata + message (MsgGetData) for a given transaction. + + Use the addTxIn and addTxOut functions to build up the list of transaction + inputs and outputs. + """ + def __init__( + self, + version: Optional[int] = 1, + txIn: Optional[Union[List[TxIn], None]] = None, + txOut: Optional[Union[List[TxOut], None]] = None, + lockTime: Optional[int] = 0, + ): + self.version = version + self.txIn = txIn or [] + self.txOut = txOut or [] + self.lockTime = lockTime + + def __eq__(self, tx): + """ + Check equality of all fields. Useful in testing. + """ + return ( + self.version == tx.version + and all((a == b for a, b in zip(self.txIn, tx.txIn))) + and all((a == b for a, b in zip(self.txOut, tx.txOut))) + and self.lockTime == tx.lockTime + ) + + def addTxIn(self, ti: TxIn): + """addTxIn adds a transaction input to the message.""" + self.txIn.append(ti) + + def addTxOut(self, to: TxOut): + """addTxOut adds a transaction output to the message.""" + self.txOut.append(to) + + def hash(self) -> ByteArray: + """txHash generates the hash for the transaction.""" + # Encode the transaction and calculate double sha256 on the result. + # Ignore the error returns since the only way the encode could fail + # is being out of memory or due to nil pointers, both of which would + # cause a run-time panic. + expSize = self.serializeSizeStripped() + toHash = self.serializeNoWitness() + if len(toHash) != expSize: + raise DecredError(f"txHash: expected {expSize}-byte serialization, got {len(toHash)} bytes") + return doubleHashH(toHash.bytes()) + + def witnessHash(self) -> ByteArray: + """ + witnessHash generates the hash of the transaction serialized according + to the new witness serialization defined in BIP0141 and BIP0144. The + final output is used within the Segregated Witness commitment of all the + witnesses within a block. If a transaction has no witness data, then the + witness hash, is the same as its txid. + """ + if self.hasWitness(): + expSize = self.serializeSize() + toHash = self.serialize() + if len(toHash) != expSize: + raise DecredError(f"witnessHash: expected {expSize}-byte serialization, got {len(toHash)} bytes") + return doubleHashH(toHash.bytes()) + + return self.hash() + + def copy(self) -> 'MsgTx': + """ + copy creates a deep copy of a transaction so that the original does not get + modified when the copy is manipulated. + """ + # Create new tx and start by copying primitive values and making space + # for the transaction inputs and outputs. + newTx = MsgTx( + version=self.version, + lockTime=self.lockTime, + ) + + # Deep copy the old TxIn data. + for oldTxIn in self.TxIn: + # Deep copy the old previous outpoint. + oldOutPoint = oldTxIn.previousOutPoint + newOP = OutPoint(txHash=oldOutPoint.hash.copy(), idx=oldOutPoint.index) + + # Create new txIn with the deep copied data. + # Deep copy the old signature script. + + newTxIn = TxIn( + previousOutPoint=newOP, + sequence=oldTxIn.sequence, + signatureScript=oldTxIn.signatureScript.copy(), + witness=[b.copy() for b in oldTxIn.witness], + ) + + # Finally, append this fully copied txin. + newTx.txIn.append(newTxIn) + + # Deep copy the old TxOut data. + for oldTxOut in self.txOut: + # Deep copy the old PkScript + newTx.txOut.append(TxOut(value=oldTxOut.value, pkScript=oldTxOut.pkScript.copy())) + + return newTx + + @staticmethod + def btcDecode(b: ByteArray, pver: int, enc: int) -> 'MsgTx': + """ + btcDecode decodes b using the Bitcoin protocol encoding. This is part of + the Message API. See deserialize for decoding transactions stored to + disk, such as in a database, as opposed to decoding transactions from + the wire. + """ + # The serialized encoding of the version includes the real transaction + # version in the lower 16 bits and the transaction serialization type + # in the upper 16 bits. + + version = b.pop(4).unLittle().int() + + tx = MsgTx(version=version) + + count = dcrwire.readVarInt(b, pver) + + # A count of zero (meaning no TxIn's to the uninitiated) means that the + # value is a TxFlagMarker, and hence indicates the presence of a flag. + flag = [0] + if count == TxFlagMarker and enc == WitnessEncoding: + # The count varint was in fact the flag marker byte. Next, we need to + # read the flag value, which is a single byte. + flag = b.pop(1) + + # At the moment, the flag MUST be WitnessFlag (0x01). In the future + # other flag types may be supported. + if flag[0] != WitnessFlag: + raise DecredError(f"MsgTx.BtcDecode: witness tx but flag byte is {flag}.") + + # With the Segregated Witness specific fields decoded, we can + # now read in the actual txin count. + count = dcrwire.readVarInt(b, pver) + + # Prevent more input transactions than could possibly fit into a + # message. It would be possible to cause memory exhaustion + # without a sane upper bound on this count. + if count > maxTxInPerMessage: + raise DecredError(f"MsgTx.BtcDecode: too many input transactions to fit into max message size [count {count}, max {maxTxInPerMessage}]") + + # Deserialize the inputs. + totalScriptSize = 0 + for i in range(count): + # The pointer is set now in case a script buffer is borrowed + # and needs to be returned to the pool on error. + ti = readTxIn(b, pver, tx.version) + tx.addTxIn(ti) + totalScriptSize += len(ti.signatureScript) + + count = dcrwire.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(f"MsgTx.btcDecode: too many output transactions to fit into max message size [count {count}, max {maxTxOutPerMessage}]") + + # Deserialize the outputs. + for _ in range(count): + # The pointer is set now in case a script buffer is borrowed + # and needs to be returned to the pool on error. + txOut = readTxOut(b, pver, tx.version) + tx.addTxOut(txOut) + totalScriptSize += len(txOut.pkScript) + + # If the transaction's flag byte isn't 0x00 at this point, then one or + # more of its inputs has accompanying witness data. + if flag[0] != 0 and enc == WitnessEncoding: + for txIn in tx.txIn: + # For each input, the witness is encoded as a stack + # with one or more items. Therefore, we first read a + # varint which encodes the number of stack items. + witCount = dcrwire.readVarInt(b, pver) + + # Prevent a possible memory exhaustion attack by + # limiting the witCount value to a sane upper bound. + if witCount > maxWitnessItemsPerInput: + raise DecredError(f"too many witness items to fit into max message size [count {witCount}, max {maxWitnessItemsPerInput}]") + + # Then for witCount number of stack items, each item + # has a varint length prefix, followed by the witness + # item itself. + for j in range(witCount): + script = readScript(b, pver, maxWitnessItemSize, "script witness item") + txIn.witness.append(script) + totalScriptSize += len(script) + + tx.lockTime = b.pop(4).unLittle().int() + + # btcwallet takes some cautions here to make sure all scripts in the + # transaction are collected into a single contiguous buffer in order to + # lower the garbage collector workload. It might be worth investigating + # whether such an approach, which should be possible on Python with + # memoryviews, would actually provide a worthwhile perf boost. + + return tx + + @staticmethod + def deserialize(b): + """Deserialize the MsgTx.""" + return MsgTx.btcDecode(b, 0, WitnessEncoding) + + @staticmethod + def deserializeNoWitness(b): + """Deserialize the MsgTx without witness data.""" + return MsgTx.btcDecode(b, 0, BaseEncoding) + + def btcEncode(self, pver: int, enc: int) -> ByteArray: + """ + btcEncode encodes the bytes using the Bitcoin protocol encoding. This is + part of the Message interface implementation. See serialize for encoding + transactions to be stored to disk, such as in a database, as opposed to + encoding transactions for the wire. + """ + b = ByteArray(self.version, length=4).littleEndian() + + # If the encoding version is set to WitnessEncoding, and the Flags + # field for the MsgTx aren't 0x00, then this indicates the transaction + # is to be encoded using the new witness inclusionary structure + # defined in BIP0144. + doWitness = enc == WitnessEncoding and self.hasWitness() + if doWitness: + # After the transaction's Version field, we include two additional + # bytes specific to the witness encoding. This byte sequence is known + # as a flag. The first byte is a marker byte (TxFlagMarker) and the + # second one is the flag value to indicate presence of witness data. + b += TxFlagMarker + b += WitnessFlag + + count = len(self.txIn) + b += dcrwire.writeVarInt(pver, count) + + for ti in self.txIn: + b += writeTxIn(pver, self.version, ti) + + count = len(self.txOut) + b += dcrwire.writeVarInt(pver, count) + for to in self.txOut: + b += writeTxOut(pver, self.version, to) + + # If this transaction is a witness transaction, and the witness + # encoded is desired, then encode the witness for each of the inputs + # within the transaction. + if doWitness: + for ti in self.txIn: + b += writeTxWitness(pver, self.version, ti.witness) + + b += ByteArray(self.lockTime, length=4).littleEndian() + + return b + + def hasWitness(self) -> bool: + """ + hasWitness returns False if none of the inputs within the transaction + contain witness data, True otherwise. + """ + for txIn in self.txIn: + if len(txIn.witness) != 0: + return True + return False + + def serialize(self) -> ByteArray: + """ + serialize encodes the transaction using a format that is suitable for + long-term storage such as a database while respecting the version field + in the transaction. This function differs from btcEncode in that + btcEncode encodes the transaction to the bitcoin wire protocol in order + to be sent across the network. The wire encoding can technically differ + depending on the protocol version and doesn't even really need to match + the format of a stored transaction at all. As of the time this comment + was written, the encoded transaction is the same in both instances, but + there is a distinct difference and separating the two allows the API to + be flexible enough to deal with changes. + """ + return self.btcEncode(0, WitnessEncoding) + + def serializeNoWitness(self) -> ByteArray: + """ + serializeNoWitness encodes the transaction in an identical manner to + Serialize, however even if the source transaction has inputs with + witness data, the old serialization format will still be used. + """ + return self.btcEncode(0, BaseEncoding) + + def baseSize(self) -> int: + """ + baseSize returns the serialized size of the transaction without + accounting for any witness data. + """ + # Version 4 bytes + LockTime 4 bytes + Serialized varint size for the + # number of transaction inputs and outputs. + n = 8 + dcrwire.varIntSerializeSize(len(self.txIn)) + dcrwire.varIntSerializeSize(len(self.txOut)) + + for txIn in self.txIn: + n += txIn.serializeSize() + + for txOut in self.txOut: + n += txOut.serializeSize() + + return n + + def serializeSize(self) -> int: + """ + serializeSize returns the number of bytes it would take to serialize the + the transaction. + """ + n = self.baseSize() + + if self.hasWitness(): + # The marker, and flag fields take up two additional bytes. + n += 2 + + # witnesses for each txin. + # Additionally, factor in the serialized size of each of the + for txIn in self.txIn: + n += txIn.witnessSerializeSize() + + return n + + def serializeSizeStripped(self) -> int: + """ + serializeSizeStripped returns the number of bytes it would take to serialize + the transaction, excluding any included witness data. + """ + return self.baseSize() + + def command(self) -> str: + """ + command returns the protocol command string for the message. This is + part of the Message interface implementation. + """ + return wire.CmdTx + + def maxPayloadLength(self, pver: int) -> int: + """ + maxPayloadLength returns the maximum length the payload can be. This is + part of the Message interface implementation. + """ + return wire.MaxBlockPayload + + def pkScriptLocs(self) -> List[int]: + """ + pkScriptLocs returns a slice containing the start of each public key + script within the raw serialized transaction. The caller can easily + obtain the length of each script by using len on the script available + via the appropriate transaction output entry. + """ + numTxOut = len(self.txOut) + if numTxOut == 0: + return [] + + # The starting offset in the serialized transaction of the first + # transaction output is: + # + # Version 4 bytes + serialized varint size for the number of + # transaction inputs and outputs + serialized size of each transaction + # input. + n = 4 + dcrwire.varIntSerializeSize(len(self.txIn)) + dcrwire.varIntSerializeSize(numTxOut) + + # If this transaction has a witness input, the an additional two bytes + # for the marker, and flag byte need to be taken into account. + if len(self.txIn) > 0 and self.txIn[0].witness: + n += 2 + + for txIn in self.txIn: + n += txIn.serializeSize() + + # Calculate and set the appropriate offset for each public key script. + pkScriptLocs = [] + for i, txOut in enumerate(self.txOut): + # The offset of the script in the transaction output is: + # + # Value 8 bytes + serialized varint size for the length of + # PkScript. + n += 8 + dcrwire.varIntSerializeSize(len(txOut.pkScript)) + pkScriptLocs.append(n) + n += len(txOut.pkScript) + + return pkScriptLocs + + +def readOutPoint(b: ByteArray, pver: int, version: int) -> OutPoint: + """ + readOutPoint reads the next sequence of bytes from b, removing the sequence + from b returning both. + """ + return OutPoint( + txHash=b.pop(HASH_SIZE), + idx=b.pop(4).unLittle().int(), + ) + + +def writeOutPoint(pver: int, version: int, op: OutPoint) -> ByteArray: + """ + writeOutPoint serializes the OutPoint. + """ + return op.hash + ByteArray(op.index, length=4).littleEndian() + + +def readScript(b: ByteArray, pver: int, maxAllowed: int, fieldName: str) -> ByteArray: + """ + readScript reads a variable length byte array that represents a transaction + script. It is encoded as a varInt containing the length of the array + followed by the bytes themselves. An error is returned if the length is + greater than the passed maxAllowed parameter which helps protect against + memory exhaustion attacks and forced panics through malformed messages. The + fieldName parameter is only used for the error message so it provides more + context in the error. + """ + count = dcrwire.readVarInt(b, pver) + + # Prevent byte array larger than the max message size. It would + # be possible to cause memory exhaustion and panics without a sane + # upper bound on this count. + if count > maxAllowed: + raise DecredError(f"readScript: {fieldName} is larger than the max allowed size [count {count}, max {maxAllowed}]", fieldName, count, maxAllowed) + + return b.pop(count) + + +def readTxIn(b: ByteArray, pver: int, version: int) -> TxIn: + """ + readTxIn reads and decodes the next sequence of bytes from b as a + transaction input (TxIn). + """ + return TxIn( + previousOutPoint=readOutPoint(b, pver, version), + signatureScript=readScript(b, pver, wire.MaxMessagePayload, "transaction input signature script"), + sequence=b.pop(4).unLittle().int(), + ) + + +def writeTxIn(pver: int, version: int, ti: TxIn) -> ByteArray: + """ + writeTxIn encodes ti to the bitcoin protocol encoding for a transaction + input (TxIn). + """ + b = writeOutPoint(pver, version, ti.previousOutPoint) + b += dcrwire.writeVarBytes(pver, ti.signatureScript) + return b + ByteArray(ti.sequence, length=4).littleEndian() + + +def readTxOut(b: ByteArray, pver: int, version: int) -> TxOut: + """ + readTxOut reads the next sequence of bytes from b as a transaction output + (TxOut). + """ + return TxOut( + value=b.pop(8).unLittle().int(), + pkScript=readScript(b, pver, wire.MaxMessagePayload, "transaction output public key script"), + ) + + +def writeTxOut(pver: int, version: int, to: TxOut) -> ByteArray: + """ + writeTxOut encodes to into the bitcoin protocol encoding for a transaction + output (TxOut). + """ + b = ByteArray(to.value, length=8).littleEndian() + return b + dcrwire.writeVarBytes(pver, to.pkScript) + + +def writeTxWitness(pver: int, version: int, wit: List[ByteArray]) -> ByteArray: + """ + writeTxWitness encodes the bitcoin protocol encoding for a transaction + input's witness. + """ + b = dcrwire.writeVarInt(pver, len(wit)) + for item in wit: + b += dcrwire.writeVarBytes(pver, item) + return b + + +def doubleHashH(b: bytes) -> bytes: + """ + Double-SHA256 hash. + """ + v = hashlib.sha256(b).digest() + return hashlib.sha256(v).digest() diff --git a/decred/decred/btc/wire/wire.py b/decred/decred/btc/wire/wire.py new file mode 100644 index 00000000..ee0db249 --- /dev/null +++ b/decred/decred/btc/wire/wire.py @@ -0,0 +1,79 @@ +CmdVersion = "version" +CmdVerAck = "verack" +CmdGetAddr = "getaddr" +CmdAddr = "addr" +CmdGetBlocks = "getblocks" +CmdInv = "inv" +CmdGetData = "getdata" +CmdNotFound = "notfound" +CmdBlock = "block" +CmdTx = "tx" +CmdGetHeaders = "getheaders" +CmdHeaders = "headers" +CmdPing = "ping" +CmdPong = "pong" +CmdAlert = "alert" +CmdMemPool = "mempool" +CmdFilterAdd = "filteradd" +CmdFilterClear = "filterclear" +CmdFilterLoad = "filterload" +CmdMerkleBlock = "merkleblock" +CmdReject = "reject" +CmdSendHeaders = "sendheaders" +CmdFeeFilter = "feefilter" +CmdGetCFilters = "getcfilters" +CmdGetCFHeaders = "getcfheaders" +CmdGetCFCheckpt = "getcfcheckpt" +CmdCFilter = "cfilter" +CmdCFHeaders = "cfheaders" +CmdCFCheckpt = "cfcheckpt" +CmdSendAddrV2 = "sendaddrv2" + +# MaxMessagePayload is the maximum bytes a message can be regardless of other +# individual limits imposed by messages themselves. +MaxMessagePayload = (1024 * 1024 * 32) # 32MB + +# MaxBlockPayload is the maximum bytes a block message can be in bytes. +# After Segregated Witness, the max block payload has been raised to 4MB. +MaxBlockPayload = 4000000 + + +# ProtocolVersion is the latest protocol version this package supports. +ProtocolVersion = 70013 + +# MultipleAddressVersion is the protocol version which added multiple +# addresses per message (pver >= MultipleAddressVersion). +MultipleAddressVersion = 209 + +# NetAddressTimeVersion is the protocol version which added the +# timestamp field (pver >= NetAddressTimeVersion). +NetAddressTimeVersion = 31402 + +# BIP0031Version is the protocol version AFTER which a pong message +# and nonce field in ping were added (pver > BIP0031Version). +BIP0031Version = 60000 + +# BIP0035Version is the protocol version which added the mempool +# message (pver >= BIP0035Version). +BIP0035Version = 60002 + +# BIP0037Version is the protocol version which added new connection +# bloom filtering related messages and extended the version message +# with a relay flag (pver >= BIP0037Version). +BIP0037Version = 70001 + +# RejectVersion is the protocol version which added a new reject +# message. +RejectVersion = 70002 + +# BIP0111Version is the protocol version which added the SFNodeBloom +# service flag. +BIP0111Version = 70011 + +# SendHeadersVersion is the protocol version which added a new +# sendheaders message. +SendHeadersVersion = 70012 + +# FeeFilterVersion is the protocol version which added a new +# feefilter message. +FeeFilterVersion = 70013 diff --git a/decred/decred/crypto/crypto.py b/decred/decred/crypto/crypto.py index 13bdbca8..93bcabf8 100644 --- a/decred/decred/crypto/crypto.py +++ b/decred/decred/crypto/crypto.py @@ -9,7 +9,7 @@ import hashlib import hmac -from base58 import b58decode, b58encode +from base58 import b58encode, b58decode from blake256.blake256 import blake_hash import nacl.secret @@ -18,7 +18,7 @@ from decred.util.encode import ByteArray, unblobCheck from . import rando -from .secp256k1.curve import PrivateKey, PublicKey, curve as Curve +from .secp256k1.curve import PublicKey, PrivateKey, curve as Curve BLAKE256_SIZE = 32 @@ -63,6 +63,10 @@ # compressed public key. PKFCompressed = 1 +# PKFHybrid indicates the pay-to-pubkey address format is a hybrid +# public key. +PKFHybrid = 2 + class CrazyKeyError(DecredError): """ @@ -203,6 +207,7 @@ def modInv(a, m): return x % m +# NOTE: hashH is for Decred. def hashH(b): """ The BLAKE256 hash as a ByteArray. @@ -216,31 +221,6 @@ def hashH(b): return ByteArray(blake_hash(b), length=BLAKE256_SIZE) -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 privKeyFromBytes(pk): """ privKeyFromBytes creates a PrivateKey for the secp256k1 curve based on @@ -252,8 +232,7 @@ def privKeyFromBytes(pk): Returns: secp256k1.Privatekey: The private key structure. """ - x, y = Curve.scalarBaseMult(pk.int()) - return PrivateKey(Curve, pk, x, y) + return PrivateKey.fromBytes(pk) class ExtendedKey: @@ -290,6 +269,9 @@ def __init__( if len(privVer) != 4 or len(pubVer) != 4: msg = "Network version bytes of incorrect lengths {} and {}" raise DecredError(msg.format(len(privVer), len(pubVer))) + # BTC uses a single 4-byte version, but I've convinced myself that + # because of the way we handle encoding, handling as separate priv + # and private versions like Decred does is compatible. self.privVer = ByteArray(privVer) self.pubVer = ByteArray(pubVer) self.key = ByteArray(key) @@ -352,6 +334,11 @@ def new(seed): isPrivate=True, ) + @staticmethod + def fromString(b58, netParams): + b = b58decode(b58) + return decodeExtendedKey(netParams, ByteArray(b)) + def setNetwork(self, netParams): """ Sets the privVer and pubVer fields. This should be used when deriving @@ -470,7 +457,7 @@ def child(self, i): # Il = intermediate key used to derive the child # Ir = child chain code il = ilr[: len(ilr) // 2] - childChainCode = ilr[len(ilr) // 2 :] + childChainCode = ilr[len(ilr) // 2:] # See CrazyKeyError docs for an explanation of this condition. if il.int() >= Curve.N or il.iszero(): @@ -506,6 +493,7 @@ def child(self, i): # Convert the serialized compressed parent public key into X # and Y coordinates so it can be added to the intermediate # public key. + pubKey = Curve.parsePubKey(self.key) # Add the intermediate public key to the parent public key to # derive the final child key. @@ -657,7 +645,7 @@ def publicKey(self): return Curve.parsePubKey(self.pubKey) -def decodeExtendedKey(netParams, cryptoKey, key): +def decodeEncryptedExtendedKey(netParams, cryptoKey, key): """ Decode an base58 ExtendedKey using the passphrase and network parameters. @@ -669,18 +657,24 @@ def decodeExtendedKey(netParams, cryptoKey, key): Returns: ExtendedKey: The decoded key. """ - decoded = decrypt(cryptoKey, key) - decoded_len = len(decoded) - if decoded_len != SERIALIZED_KEY_LENGTH + 4: - raise DecredError(f"decoded private key is wrong length: {decoded_len}") + decrypted = decrypt(cryptoKey, key) + decrypted_len = len(decrypted) + if decrypted_len != SERIALIZED_KEY_LENGTH + 4: + raise DecredError(f"decoded private key is wrong length: {decrypted_len}") + + return decodeExtendedKey(netParams, decrypted) + + +def decodeExtendedKey(netParams, encoded): # The serialized format is: # version (4) || depth (1) || parent fingerprint (4)) || # child num (4) || chain code (32) || key data (33) || checksum (4) # Split the payload and checksum up and ensure the checksum matches. - payload = decoded[: decoded_len - 4] - included_cksum = decoded[decoded_len - 4 :] + encoded_len = len(encoded) + payload = encoded[: encoded_len - 4] + included_cksum = encoded[encoded_len - 4:] computed_cksum = checksum(payload.b)[:4] if included_cksum != computed_cksum: raise DecredError("wrong checksum") @@ -898,6 +892,7 @@ def rekey(pw, kp): # with the included key parameters. authKey = b[32:].bytes() authMsg = kp.baseParams().bytes() + auth = hashlib.blake2b(authMsg, digest_size=32, key=authKey).digest() if auth != kp.auth: raise DecredError("rekey auth check failed") diff --git a/decred/decred/crypto/gcs.py b/decred/decred/crypto/gcs.py index 1fa34a07..a2938aa0 100644 --- a/decred/decred/crypto/gcs.py +++ b/decred/decred/crypto/gcs.py @@ -172,6 +172,9 @@ def deserialize(b): n = wire.readVarInt(filterData, 0) return FilterV2(n, filterData) + def serialize(self) -> ByteArray: + return wire.writeVarInt(0, self.n) + self.filterData + def readFullUint64(self, bitReader): """ Read a value represented by the sum of a unary multiple of the Golomb diff --git a/decred/decred/crypto/secp256k1/curve.py b/decred/decred/crypto/secp256k1/curve.py index cba88c7d..bc8ff009 100644 --- a/decred/decred/crypto/secp256k1/curve.py +++ b/decred/decred/crypto/secp256k1/curve.py @@ -35,9 +35,11 @@ COORDINATE_LEN = 32 PUBKEY_COMPRESSED_LEN = COORDINATE_LEN + 1 +PUBKEY_HYBRID_LEN = 65 PUBKEY_LEN = 65 PUBKEY_COMPRESSED = 0x02 # 0x02 y_bit + x coord PUBKEY_UNCOMPRESSED = 0x04 # 0x04 x coord + y coord +PUBKEY_HYBRID = 0x6 fieldOne = FieldVal.fromInt(1) @@ -157,6 +159,17 @@ def serializeUncompressed(self): b += ByteArray(self.y, length=32) return b + def serializeHybrid(self): + """SerializeHybrid serializes a public key in a 65-byte hybrid format.""" + fmt = PUBKEY_HYBRID + if not isEven(self.y): + fmt |= 0x1 + b = ByteArray(fmt) + b += ByteArray(self.x, length=COORDINATE_LEN) + ByteArray(self.y, length=COORDINATE_LEN) + if len(b) != PUBKEY_HYBRID_LEN: + raise DecredError("invalid hybrid pubkey length %d", len(b)) + return b + def __eq__(self, other): """ __eq__ compares this PublicKey instance to the one passed, returning @@ -175,6 +188,21 @@ def __init__(self, curve, k, x, y): self.key = k self.pub = PublicKey(curve, x, y) + @staticmethod + def fromBytes(pk): + """ + fromBytes creates a PrivateKey for the secp256k1 curve based on + the provided byte-encoding. + + Args: + pk (ByteArray): The private key bytes. + + Returns: + secp256k1.Privatekey: The private key structure. + """ + x, y = curve.scalarBaseMult(pk.int()) + return PrivateKey(Curve, pk, x, y) + def randFieldElement(): """ @@ -261,6 +289,7 @@ def scalarBaseMult(self, k): for i, bidx in enumerate(kb.b): p = BytePoints[diff + i][bidx] self.addJacobian(qx, qy, qz, p[0], p[1], p[2], qx, qy, qz) + return self.fieldJacobianToBigAffine(qx, qy, qz) def splitK(self, k): @@ -397,7 +426,7 @@ def publicKey(self, k): x, y = self.scalarBaseMult(k) return PublicKey(self, x, y) - def parsePubKey(self, pubKeyB): + def parsePubKey(self, pubKeyB, allowHybrid=True): """ parsePubKey parses a secp256k1 public key encoded according to the format specified by ANSI X9.62-1998, which means it is also compatible @@ -419,14 +448,21 @@ def parsePubKey(self, pubKeyB): ybit = (fmt & 0x1) == 0x1 fmt &= 0xFF ^ 0x01 - ifunc = lambda b: int.from_bytes(b, byteorder="big") + if fmt == PUBKEY_HYBRID and not allowHybrid: + raise DecredError("hybrid format not suported") + + def ifunc(b: bytes): + return int.from_bytes(b, byteorder="big") pkLen = len(pubKeyB) if pkLen == PUBKEY_LEN: - if PUBKEY_UNCOMPRESSED != fmt: + if fmt != PUBKEY_UNCOMPRESSED and fmt != PUBKEY_HYBRID: raise DecredError("invalid magic in pubkey: %d" % pubKeyB[0]) x = ifunc(pubKeyB[1:33]) y = ifunc(pubKeyB[33:]) + # hybrid keys have extra information, make use of it. + if fmt == PUBKEY_HYBRID and ybit == isEven(y): + raise DecredError("ybit doesn't match oddness") elif pkLen == PUBKEY_COMPRESSED_LEN: # format is 0x2 | solution, diff --git a/decred/decred/dcr/addrlib.py b/decred/decred/dcr/addrlib.py index e0fec3bc..ec06b7bb 100644 --- a/decred/decred/dcr/addrlib.py +++ b/decred/decred/dcr/addrlib.py @@ -17,7 +17,6 @@ STEcdsaSecp256k1, STEd25519, STSchnorrSecp256k1, - b58CheckDecode, checksum, hash160, ) @@ -465,3 +464,27 @@ def deriveChildAddress(branchXPub, i, netParams): return AddressPubKeyHash( hash160(child.publicKey().serializeCompressed().b), netParams, STEcdsaSecp256k1, ).string() + +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 diff --git a/decred/decred/dcr/wire/msgtx.py b/decred/decred/dcr/wire/msgtx.py index 11edb9d2..dde99784 100644 --- a/decred/decred/dcr/wire/msgtx.py +++ b/decred/decred/dcr/wire/msgtx.py @@ -25,7 +25,7 @@ # transaction's location in a block. TxTreeStake = 1 # go type int8 -# chainhash.HashSize in go +# chainhash.HashSize in Go HASH_SIZE = 32 # minTxInPayload is the minimum payload size for a transaction input. @@ -51,20 +51,10 @@ MaxTxInSequenceNum = 0xFFFFFFFF -def writeOutPoint(pver, ver, op): - """ - writeOutPoint encodes op to the Decred protocol encoding for an OutPoint - to w. - """ - b = op.hash.copy() - b += ByteArray(op.index, length=4).littleEndian() - b += ByteArray(op.tree, length=1) - return b - - def readOutPoint(b, pver, ver): """ - readOutPoint reads the next sequence of bytes from r as an OutPoint. + readOutPoint reads the next sequence of bytes from b, removing the sequence + from b returning both. """ op = OutPoint(None, None, None) op.hash = b.pop(HASH_SIZE) @@ -382,13 +372,23 @@ def txid(self): return reversed(self.hash).hex() +def writeOutPoint(pver: int, ver: int, op: OutPoint) -> ByteArray: + """ + writeOutPoint serializes the OutPoint. + """ + b = op.hash.copy() + b += ByteArray(op.index, length=4).littleEndian() + b += ByteArray(op.tree, length=1) + return b + + class MsgTx: """ MsgTx implements the Message API and represents a Decred tx message. It is used to deliver transaction information in response to a getdata message (MsgGetData) for a given transaction. - Use the AddTxIn and AddTxOut functions to build up the list of transaction + Use the addTxIn and addTxOut functions to build up the list of transaction inputs and outputs. The go types are diff --git a/decred/pyproject.toml b/decred/pyproject.toml index a917b289..fbdc9296 100644 --- a/decred/pyproject.toml +++ b/decred/pyproject.toml @@ -29,6 +29,7 @@ base58 = "^2.0.0" blake256 = "^0.1.1" pynacl = "^1.3.0" websocket_client = "^0.57.0" +bech32 = "^1.2.0" [tool.poetry.dev-dependencies] pytest = "^5.3" diff --git a/decred/tests/integration/btc/test_libbtcwallet.py b/decred/tests/integration/btc/test_libbtcwallet.py new file mode 100644 index 00000000..d532b608 --- /dev/null +++ b/decred/tests/integration/btc/test_libbtcwallet.py @@ -0,0 +1,420 @@ +import os +import pytest + +from decred.btc.btcwallet import btcwallet +from decred.btc.nets import mainnet +from decred.btc import addrlib +from decred.btc.wire import msgtx +from decred.crypto.secp256k1.curve import PrivateKey +from decred.util.encode import ByteArray + +import json + + +def decAddr(a): + return addrlib.decodeAddress(a, mainnet) + + +@pytest.fixture(scope='session', autouse=True) +def rig(request): + + class rig: + data = None + wallet = None + + libDir = os.path.dirname(os.path.realpath(btcwallet.__file__)) + dataPath = os.path.join(libDir, "libbtcwallet", "test-data.json") + with open(dataPath, "r") as f: + rig.data = json.loads(f.read()) + rig.wallet = btcwallet.BTCWallet( + walletDir=dataPath, + netParams=mainnet, + feeder=lambda obj: print(obj), + debugLevel=1, + test=True, + ) + return rig + + +def test_MakeMultiSigScript(rig): + addr1 = decAddr(rig.data["MakeMultiSigScript.in.addr.1"]) + addr2 = decAddr(rig.data["MakeMultiSigScript.in.addr.2"]) + nConfs = rig.data["MakeMultiSigScript.in.nConfs"] + script = rig.wallet.makeMultiSigScript([addr1, addr2], nConfs) + assert script == ByteArray(rig.data["MakeMultiSigScript.out"]) + + +def test_ImportP2SHRedeemScript(rig): + script = rig.data["ImportP2SHRedeemScript.in.script"] + addr = rig.wallet.importP2SHRedeemScript(ByteArray(script)) + assert addr == rig.data["ImportP2SHRedeemScript.out.addr"] + + +def test_UnspentOutputs(rig): + acct = rig.data["ImportP2SHRedeemScript.in.acct"] + confs = rig.data["ImportP2SHRedeemScript.in.confs"] + + outputs = rig.wallet.unspentOutputs(btcwallet.OutputSelectionPolicy(acct, confs)) + assert len(outputs) == 1 + + txo = outputs[0] + + assert txo.outPoint.hash == ByteArray(rig.data["ImportP2SHRedeemScript.out.outPoint.hash"]) + assert txo.outPoint.index == rig.data["ImportP2SHRedeemScript.out.outPoint.index"] + assert txo.output.pkScript == rig.data["ImportP2SHRedeemScript.out.output.script"] + assert txo.output.value == rig.data["ImportP2SHRedeemScript.out.output.value"] + assert txo.outputKind == rig.data["ImportP2SHRedeemScript.out.outputKind"] + assert txo.containingBlock.hash == rig.data["ImportP2SHRedeemScript.out.containingBlock.hash"] + assert txo.containingBlock.height == rig.data["ImportP2SHRedeemScript.out.containingBlock.index"] + assert txo.receiveTime == rig.data["ImportP2SHRedeemScript.out.receiveTime"] + + txHash = ByteArray(rig.data["LockedOutpoint.in.hash"]) + idx = rig.data["LockedOutpoint.in.index"] + locked = rig.wallet.lockedOutpoint(msgtx.OutPoint( + txHash=txHash, + idx=idx, + )) + assert locked == rig.data["LockedOutpoint.out.locked"] + + ops = rig.wallet.lockedOutpoints() + assert len(ops) == 1 + assert ops[0].hash == rig.data["LockedOutpoints.out.hash"] + + txHash = ByteArray(rig.data["LeaseOutput.in.hash"]) + idx = rig.data["LeaseOutput.in.index"] + lockID = ByteArray(rig.data["LeaseOutput.in.lockID"]) + expiration = rig.wallet.leaseOutput(lockID=lockID, op=msgtx.OutPoint( + txHash=txHash, + idx=idx, + )) + assert expiration == rig.data["LeaseOutput.out"] + + # looking for panics + txHash = ByteArray(rig.data["LockOutpoint.in.hash"]) + idx = rig.data["LockOutpoint.in.index"] + rig.wallet.lockOutpoint(msgtx.OutPoint( + txHash=txHash, + idx=idx, + )) + txHash = ByteArray(rig.data["UnlockOutpoint.in.hash"]) + idx = rig.data["UnlockOutpoint.in.index"] + rig.wallet.unlockOutpoint(msgtx.OutPoint( + txHash=txHash, + idx=idx, + )) + txHash = ByteArray(rig.data["ReleaseOutput.in.hash"]) + idx = rig.data["ReleaseOutput.in.index"] + lockID = ByteArray(rig.data["ReleaseOutput.in.lockID"]) + expiration = rig.wallet.releaseOutput(lockID=lockID, op=msgtx.OutPoint( + txHash=txHash, + idx=idx, + )) + + +def test_simpleMethods(rig): + # Single-valued returns + assert rig.wallet.shuttingDown() == rig.data["ShuttingDown.out"] + assert rig.wallet.synchronizingToNetwork() == rig.data["SynchronizingToNetwork.out"] + assert rig.wallet.chainSynced() == rig.data["ChainSynced.out"] + assert rig.wallet.locked() == rig.data["Locked.out"] + assert rig.wallet.haveAddress(decAddr(rig.data["HaveAddress.in"])) == rig.data["HaveAddress.out"] + assert rig.wallet.accountOfAddress(decAddr(rig.data["AccountOfAddress.in.addr"])) == rig.data["AccountOfAddress.out.acct"] + + # Look for exceptions/panics with these + rig.wallet.setChainSynced(rig.data["SetChainSynced.in"]) + rig.wallet.start() + rig.wallet.stop() + rig.wallet.waitForShutdown() + rig.wallet.unlock(rig.data["Unlock.in.passphrase"], 123) + rig.wallet.lock() + rig.wallet.labelTransaction( + h=ByteArray(rig.data["LabelTransaction.in.h"]), + label=rig.data["LabelTransaction.in.label"], + overwrite=rig.data["LabelTransaction.in.overwrite"], + ) + + +def test_passphraseChanges(rig): + rig.wallet.changePrivatePassphrase( + old=rig.data["ChangePrivatePassphrase.in.old"], + new=rig.data["ChangePrivatePassphrase.in.new"], + ) + rig.wallet.changePublicPassphrase( + old=rig.data["ChangePublicPassphrase.in.old"], + new=rig.data["ChangePublicPassphrase.in.new"], + ) + rig.wallet.changePassphrases( + publicOld=rig.data["ChangePassphrases.in.publicOld"], + publicNew=rig.data["ChangePassphrases.in.publicNew"], + privateOld=rig.data["ChangePassphrases.in.privateOld"], + privateNew=rig.data["ChangePassphrases.in.privateNew"], + ) + + +def test_MsgTx_methods(rig): + authoredTx = rig.wallet.createSimpleTx( + acct=rig.data["CreateSimpleTx.in.acct"], + outputs=[msgtx.TxOut( + value=rig.data["CreateSimpleTx.in.output.value"], + pkScript=ByteArray(rig.data["CreateSimpleTx.in.output.script"]), + version=rig.data["CreateSimpleTx.in.output.version"], + )], + minConf=rig.data["CreateSimpleTx.in.minConf"], + satPerKb=rig.data["CreateSimpleTx.in.satPerKb"], + dryRun=rig.data["CreateSimpleTx.in.dryRun"], + ) + + expTx = msgtx.MsgTx.deserialize(ByteArray(rig.data["CreateSimpleTx.out.tx"])) + assert expTx == authoredTx.tx + + assert len(authoredTx.prevScripts) == 1 + assert authoredTx.prevScripts[0] == ByteArray(rig.data["CreateSimpleTx.out.prevScript"]) + + assert len(authoredTx.prevInputValues) == 1 + assert authoredTx.prevInputValues[0] == rig.data["CreateSimpleTx.out.prevInputValue"] + + assert authoredTx.totalInput == rig.data["CreateSimpleTx.out.totalInput"] + assert authoredTx.changeIndex == rig.data["CreateSimpleTx.out.changeIndex"] + + value = rig.data["SendOutputs.in.value"] + pkScript = ByteArray(rig.data["SendOutputs.in.pkScript"]) + acct = rig.data["SendOutputs.in.acct"] + minConf = rig.data["SendOutputs.in.minconf"] + satPerKb = rig.data["SendOutputs.in.satPerKb"] + label = rig.data["SendOutputs.in.label"] + tx = rig.wallet.sendOutputs( + outputs=[msgtx.TxOut(value=value, pkScript=pkScript)], + acct=acct, + minConf=minConf, + satPerKb=satPerKb, + label=label, + ) + assert tx.serialize() == rig.data["SendOutputs.out.tx"] + + tx = msgtx.MsgTx.deserialize(ByteArray(rig.data["SignTransaction.in.tx"])) + hashType = rig.data["SignTransaction.in.hashType"] + prevoutHash = ByteArray(rig.data["SignTransaction.in.prevout.hash"]) + prevoutIdx = rig.data["SignTransaction.in.prevout.idx"] + prevoutScript = ByteArray(rig.data["SignTransaction.in.prevout.script"]) + keyAddr = rig.data["SignTransaction.in.key.addr"] + keyWIF = addrlib.WIF.decode(rig.data["SignTransaction.in.key.wif"]) + scriptAddr = rig.data["SignTransaction.in.script.addr"] + redeemScript = ByteArray(rig.data["SignTransaction.in.script.script"]) + rig.wallet.signTransaction( + tx=tx, + hashType=hashType, + additionalPrevScripts={f"{prevoutHash.rhex()}:{prevoutIdx}": prevoutScript}, # Dict[str, ByteArray], # key is outpoint ID txid:index, e.g. a27e1f:1 + additionalKeysByAddress={keyAddr: keyWIF}, + p2shRedeemScriptsByAddress={scriptAddr: redeemScript}, + ) + assert tx.txIn[0].signatureScript == rig.data["SignTransaction.out.sigScript"] + + tx = msgtx.MsgTx.deserialize(ByteArray(rig.data["PublishTransaction.in.tx"])) + label = rig.data["PublishTransaction.in.label"] + rig.wallet.publishTransaction(tx=tx, label=label) + + +def test_account_methods(rig): + addrs = rig.wallet.accountAddresses(rig.data["AccountAddresses.in.acct"]) + assert len(addrs) == 2 + assert addrs[0].string() == rig.data["AccountAddresses.out.addr.1"] + assert addrs[1].string() == rig.data["AccountAddresses.out.addr.2"] + + acct = rig.data["CurrentAddress.in.acct"] + purpose = rig.data["CurrentAddress.in.purpose"] + coin = rig.data["CurrentAddress.in.coin"] + addr = rig.wallet.currentAddress(acct=acct, scope=btcwallet.KeyScope(purpose=purpose, coin=coin)) + assert addr.string() == rig.data["CurrentAddress.out.addr"] + + purpose = rig.data["AccountNumber.in.scope.purpose"] + coin = rig.data["AccountNumber.in.scope.coin"] + acctName = rig.data["AccountNumber.in.accountName"] + acct = rig.wallet.accountNumber(accountName=acctName, scope=btcwallet.KeyScope(purpose=purpose, coin=coin)) + assert acct == rig.data["AccountNumber.out.acct"] + + purpose = rig.data["AccountName.in.scope.purpose"] + coin = rig.data["AccountName.in.scope.coin"] + acct = rig.data["AccountName.in.acct"] + acctName = rig.wallet.accountName(accountNumber=acct, scope=btcwallet.KeyScope(purpose=purpose, coin=coin)) + assert acctName == rig.data["AccountName.out.accountName"] + + purpose = rig.data["AccountProperties.in.scope.purpose"] + coin = rig.data["AccountProperties.in.scope.coin"] + acct = rig.data["AccountProperties.in.acct"] + acctProps = rig.wallet.accountProperties(accountNumber=acct, scope=btcwallet.KeyScope(purpose=purpose, coin=coin)) + assert acctProps.accountNumber == rig.data["AccountProperties.out.accountNumber"] + assert acctProps.accountName == rig.data["AccountProperties.out.accountName"] + assert acctProps.externalKeyCount == rig.data["AccountProperties.out.externalKeyCount"] + assert acctProps.internalKeyCount == rig.data["AccountProperties.out.internalKeyCount"] + assert acctProps.importedKeyCount == rig.data["AccountProperties.out.importedKeyCount"] + + purpose = rig.data["RenameAccount.in.scope.purpose"] + coin = rig.data["RenameAccount.in.scope.coin"] + acct = rig.data["RenameAccount.in.acct"] + newName = rig.data["RenameAccount.in.newName"] + rig.wallet.renameAccount( + accountNumber=acct, + newName=newName, + scope=btcwallet.KeyScope(purpose=purpose, coin=coin), + ) + + purpose = rig.data["Accounts.in.purpose"] + coin = rig.data["Accounts.in.coin"] + acctsResult = rig.wallet.accounts(scope=btcwallet.KeyScope(purpose=purpose, coin=coin)) + assert acctsResult.currentBlockHash == ByteArray(rig.data["Accounts.out.blockHash"]) + assert acctsResult.currentBlockHeight == rig.data["Accounts.out.blockHeight"] + assert len(acctsResult.accounts) == 1 + account = acctsResult.accounts[0] + assert account.totalBalance == rig.data["Accounts.out.balance"] + assert account.accountNumber == rig.data["Accounts.out.acct"] + + confs = rig.data["AccountBalances.in.confs"] + purpose = rig.data["AccountBalances.in.purpose"] + coin = rig.data["AccountBalances.in.coin"] + bals = rig.wallet.accountBalances( + requiredConfs=confs, + scope=btcwallet.KeyScope(purpose=purpose, coin=coin), + ) + assert len(bals) == 1 + bal = bals[0] + assert bal.accountNumber == rig.data["AccountBalances.out.acctNumber"] + assert bal.accountName == rig.data["AccountBalances.out.acctName"] + assert bal.accountBalance == rig.data["AccountBalances.out.balance"] + + minConf = rig.data["ListUnspent.in.minConf"] + maxConf = rig.data["ListUnspent.in.maxConf"] + addr = rig.data["ListUnspent.in.addr"] + unspents = rig.wallet.listUnspent(minConf=minConf, maxConf=maxConf, addresses=[addr]) + assert len(unspents) == 1 + assert unspents[0].scriptPubKey == rig.data["ListUnspent.out.scriptPubKey"] + + addrs = rig.wallet.sortedActivePaymentAddresses() + assert len(addrs) == 1 + assert addrs[0] == rig.data["SortedActivePaymentAddresses.out"] + + purpose = rig.data["NewAddress.in.purpose"] + coin = rig.data["NewAddress.in.coin"] + acct = rig.data["NewAddress.in.acct"] + addr = rig.wallet.newAddress( + acct=acct, + scope=btcwallet.KeyScope(purpose=purpose, coin=coin), + ) + assert addr.string() == rig.data["NewAddress.out.addr"] + + purpose = rig.data["NewChangeAddress.in.purpose"] + coin = rig.data["NewChangeAddress.in.coin"] + acct = rig.data["NewChangeAddress.in.acct"] + addr = rig.wallet.newChangeAddress( + acct=acct, + scope=btcwallet.KeyScope(purpose=purpose, coin=coin), + ) + assert addr.string() == rig.data["NewChangeAddress.out.addr"] + + purpose = rig.data["TotalReceivedForAccounts.in.purpose"] + coin = rig.data["TotalReceivedForAccounts.in.coin"] + minConf = rig.data["TotalReceivedForAccounts.in.minConf"] + accts = rig.wallet.totalReceivedForAccounts( + scope=btcwallet.KeyScope(purpose=purpose, coin=coin), + minConf=minConf, + ) + assert len(accts) == 1 + acct = accts[0] + assert acct.accountNumber == rig.data["TotalReceivedForAccounts.out.accountNumber"] + assert acct.accountName == rig.data["TotalReceivedForAccounts.out.accountName"] + assert acct.totalReceived == rig.data["TotalReceivedForAccounts.out.totalReceived"] + assert acct.lastConfirmation == rig.data["TotalReceivedForAccounts.out.lastConfirmation"] + + addr = decAddr(rig.data["TotalReceivedForAddr.in.addr"]) + minConf = rig.data["TotalReceivedForAddr.in.minConf"] + amt = rig.wallet.totalReceivedForAddr(addr=addr, minConf=minConf) + assert amt == rig.data["TotalReceivedForAddr.out.amt"] + + +def test_listTx_methods(rig): + start = rig.data["ListSinceBlock.in.start"] + end = rig.data["ListSinceBlock.in.end"] + syncHeight = rig.data["ListSinceBlock.in.syncHeight"] + txs = rig.wallet.listSinceBlock(start=start, end=end, syncHeight=syncHeight) + assert len(txs) == 1 + assert txs[0].blockTime == rig.data["ListSinceBlock.out.blockTime"] + + skip = rig.data["ListTransactions.in.skip"] + count = rig.data["ListTransactions.in.count"] + txs = rig.wallet.listTransactions(skip=skip, count=count) + assert len(txs) == 1 + assert txs[0].confirmations == rig.data["ListTransactions.out.confs"] + + addr = rig.data["ListAddressTransactions.in.addr"] + txs = rig.wallet.listAddressTransactions(addrs=[addr]) + assert len(txs) == 1 + assert txs[0].timeReceived == rig.data["ListAddressTransactions.out.timeReceived"] + + txs = rig.wallet.listAllTransactions() + assert len(txs) == 1 + assert txs[0].vout == rig.data["ListAllTransactions.out.vout"] + + +def test_Balances(rig): + bal = rig.wallet.calculateBalance(rig.data["CalculateBalance.in.confirms"]) + assert bal == rig.data["CalculateBalance.out"] + + bals = rig.wallet.calculateAccountBalances( + acct=rig.data["CalculateAccountBalances.in.acct"], + confirms=rig.data["CalculateAccountBalances.in.confirms"], + ) + + assert bals.total == rig.data["CalculateAccountBalances.out.total"] + assert bals.spendable == rig.data["CalculateAccountBalances.out.spendable"] + assert bals.immatureReward == rig.data["CalculateAccountBalances.out.immatureReward"] + + +def test_KeyMethods(rig): + addr = decAddr(rig.data["PubKeyForAddress.in.addr"]) + pubKey = rig.wallet.pubKeyForAddress(addr) + assert pubKey.serializeCompressed() == rig.data["PubKeyForAddress.out.pubkey"] + + addr = decAddr(rig.data["PrivKeyForAddress.in.addr"]) + privKey = rig.wallet.privKeyForAddress(addr) + assert privKey.key == rig.data["PrivKeyForAddress.out.privkey"] + + keys = rig.wallet.dumpPrivKeys() + assert len(keys) == 1 + assert keys[0].key == rig.data["DumpPrivKeys.out"] + + addr = decAddr(rig.data["DumpWIFPrivateKey.in.addr"]) + wif = rig.wallet.dumpWIFPrivateKey(addr) + assert wif.privKey.key == rig.data["DumpWIFPrivateKey.out.priv"] + + purpose = rig.data["ImportPrivateKey.in.purpose"] + coin = rig.data["ImportPrivateKey.in.coin"] + priv = PrivateKey.fromBytes(ByteArray(rig.data["ImportPrivateKey.in.priv"])) + blockHeight = rig.data["ImportPrivateKey.in.blockHeight"] + blockHash = ByteArray(rig.data["ImportPrivateKey.in.blockHash"]) + blockStamp = rig.data["ImportPrivateKey.in.blockStamp"] + rescan = rig.data["ImportPrivateKey.in.rescan"] + wif = addrlib.WIF(privKey=priv, compressPubKey=True, netID=mainnet) + addr = rig.wallet.importPrivateKey( + scope=btcwallet.KeyScope(purpose=purpose, coin=coin), + wif=wif, + bs=btcwallet.BlockStamp( + height=blockHeight, + blockHash=blockHash, + timeStamp=blockStamp, + ), + rescan=rescan, + ) + + assert addr == rig.data["ImportPrivateKey.out.addr"] + + +def test_addressInfo(rig): + addr = decAddr(rig.data["AddressInfo.in.addr"]) + mgAddr = rig.wallet.addressInfo(addr) + + assert mgAddr.acct == rig.data["AddressInfo.out.acct"] + assert mgAddr.addr == rig.data["AddressInfo.out.addr"] + assert mgAddr.addrHash == ByteArray(rig.data["AddressInfo.out.addrHash"]) + assert mgAddr.imported == rig.data["AddressInfo.out.imported"] + assert mgAddr.internal == rig.data["AddressInfo.out.internal"] + assert mgAddr.compressed == rig.data["AddressInfo.out.compressed"] + assert mgAddr.addrType == rig.data["AddressInfo.out.addrType"] diff --git a/decred/tests/unit/btc/test_addrlib.py b/decred/tests/unit/btc/test_addrlib.py new file mode 100644 index 00000000..bc79e43e --- /dev/null +++ b/decred/tests/unit/btc/test_addrlib.py @@ -0,0 +1,627 @@ +""" +Copyright (c) 2019-2020, the Decred developers +See LICENSE for details +""" + +from base58 import b58decode +import bech32 +import pytest + +from decred import DecredError +from decred.crypto import crypto +from decred.crypto.secp256k1.curve import PrivateKey +from decred.btc import addrlib +from decred.btc.nets import mainnet, testnet +from decred.dcr.nets import mainnet as foreignNet +from decred.util.encode import ByteArray + +foreignNet.Bech32HRPSegwit = "foreign" + + +def test_addresses(): + addrPKH = addrlib.AddressPubKeyHash + addrSH = addrlib.AddressScriptHash.fromScript + addrSHH = addrlib.AddressScriptHash + addrPK = addrlib.AddressPubKey + + addrPKH_w = addrlib.AddressWitnessPubKeyHash + addrSHH_w = addrlib.AddressWitnessScriptHash + # addrSHH_w = addrlib.AddressWitnessScriptHash + + """ + name (str): A name for the test. + addr (str): The expected Address.string(). + saddr (str): The expected Address.scriptAddress() after decoding. + encoded (str): The expected Address.address(). + valid (bool): False if the make func is expected to raise an error. + scriptAddress (ByteArray): The expected Address.scriptAddress(), but for + the address made with make. + make (func -> str): A function to create a new Address. + netParams (module): The network parameters. + skipComp (bool): Skip string comparison of decoded address. For + AddressSecpPubKey, the encoded pubkey is unrecoverable from the + AddressSecpPubKey.address() string. + """ + tests = [ + # Positive P2PKH tests. + dict( + name="mainnet p2pkh", + addr="1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", + encoded="1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", + valid=True, + scriptAddress=ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), + make=lambda: addrPKH(ByteArray("e34cce70c86373273efcc54ce7d2a491bb4a0e84"), mainnet), + netParams=mainnet, + ), + dict( + name="mainnet p2pkh 2", + addr="12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG", + encoded="12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG", + valid=True, + scriptAddress=ByteArray("0ef030107fd26e0b6bf40512bca2ceb1dd80adaa"), + make=lambda: addrPKH(ByteArray("0ef030107fd26e0b6bf40512bca2ceb1dd80adaa"), mainnet), + netParams=mainnet, + ), + # dict( + # name="decred mainnet p2pkh", + # addr="DsdvnzfMVZUPeD7HVy6rBbrZcJH6M2qfT8x", + # encoded="DsdvnzfMVZUPeD7HVy6rBbrZcJH6M2qfT8x", + # valid=True, + # scriptAddress=ByteArray("13c60d8e68d7349f5b4ca362c3954b15045061b1"), + # make=lambda: addrPKH(ByteArray("13c60d8e68d7349f5b4ca362c3954b15045061b1"), foreignNet), + # netParams=foreignNet, + # ), + dict( + name="testnet p2pkh", + addr="mrX9vMRYLfVy1BnZbc5gZjuyaqH3ZW2ZHz", + encoded="mrX9vMRYLfVy1BnZbc5gZjuyaqH3ZW2ZHz", + valid=True, + scriptAddress=ByteArray("78b316a08647d5b77283e512d3603f1f1c8de68f"), + make=lambda: addrPKH(ByteArray("78b316a08647d5b77283e512d3603f1f1c8de68f"), testnet), + netParams=testnet, + ), + + # Negative P2PKH tests. + dict( + name="p2pkh wrong hash length", + addr="", + valid=False, + make=lambda: addrPKH(ByteArray("000ef030107fd26e0b6bf40512bca2ceb1dd80adaa"), mainnet), + netParams=mainnet, + ), + dict( + name="p2pkh bad checksum", + addr="1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gY", + valid=False, + netParams=mainnet, + ), + + # Positive P2SH tests. + dict( + # Taken from transactions: + # output: 3c9018e8d5615c306d72397f8f5eef44308c98fb576a88e030c25456b4f3a7ac + # input: 837dea37ddc8b1e3ce646f1a656e79bbd8cc7f558ac56a169626d649ebe2a3ba. + name="mainnet p2sh", + addr="3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC", + encoded="3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC", + valid=True, + scriptAddress=ByteArray("f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), + make=lambda: addrSH( + ByteArray( + "52410491bba2510912a5bd37da1fb5b1673010e43d2c6d812c514e91bfa9f2eb129e1c183329db55bd868e209aac2fbc02cb33d98fe74bf23f0c235" + "d6126b1d8334f864104865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac09ef122b1a986818a7cb624532f062c1d1f87" + "22084861c5c3291ccffef4ec687441048d2455d2403e08708fc1f556002f1b6cd83f992d085097f9974ab08a28838f07896fbab08f39495e15fa6fa" + "d6edbfb1e754e35fa1c7844c41f322a1863d4621353ae" + ), + mainnet, + ), + netParams=mainnet, + ), + # script: 512102fcc6070080d2e44f7b9a280c744ca09a658ce05b87a4d81dd9dd2446b6953f1a21027b3226787328bb53659290a44bd33acc0d3a79c64b62722fb39f58bb211cb0d452ae + # dict( + # name="decred mainnet P2SH ", + # addr="DcaephHCqjdfb3gPz778DJZWvwmUUs3ssGk", + # encoded="DcaephHCqjdfb3gPz778DJZWvwmUUs3ssGk", + # valid=True, + # scriptAddress=ByteArray("e9c02720843a9b8e49dea2981f0f14d8247be48f"), + # make=lambda: addrSHH(ByteArray("e9c02720843a9b8e49dea2981f0f14d8247be48f"), foreignNet), + # netParams=foreignNet, + # ), + dict( + # Taken from transactions: + # output: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d + # input: (not yet redeemed at time test was written) + name="mainnet p2sh 2", + addr="3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8", + encoded="3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8", + valid=True, + # result=btcutil.TstAddressScriptHash( + # ByteArray( + # "e8c300c87986efa84c37c0519929019ef86eb5b4"}, + # mainnet.ScriptHashAddrID), + scriptAddress=ByteArray("e8c300c87986efa84c37c0519929019ef86eb5b4"), + make=lambda: addrSHH(ByteArray("e8c300c87986efa84c37c0519929019ef86eb5b4"), mainnet), + netParams=mainnet, + ), + dict( + # Taken from bitcoind base58_keys_valid. + name="testnet p2sh", + addr="2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n", + encoded="2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n", + valid=True, + # result=btcutil.TstAddressScriptHash( + # ByteArray( + # "c579342c2c4c9220205e2cdc285617040c924a0a"}, + # chaincfg.TestNet3Params.ScriptHashAddrID), + scriptAddress=ByteArray("c579342c2c4c9220205e2cdc285617040c924a0a"), + make=lambda: addrSHH(ByteArray("c579342c2c4c9220205e2cdc285617040c924a0a"), testnet), + netParams=testnet, + ), + + # # Negative P2SH tests. + dict( + name="p2sh wrong hash length", + addr="", + valid=False, + make=lambda: addrSHH(ByteArray("00f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), mainnet), + netParams=mainnet, + ), + + # Positive P2PK tests. + dict( + name="mainnet p2pk compressed (02)", + addr="02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4", + encoded="13CG6SJ3yHUXo4Cr2RY4THLLJrNFuG3gUg", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"}, + # btcutil.PKFCompressed, mainnet.PubKeyHashAddrID), + scriptAddress=ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), + make=lambda: addrPK(ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), mainnet), + netParams=mainnet, + ), + dict( + name="mainnet p2pk compressed (03)", + addr="03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65", + encoded="15sHANNUBSh6nDp8XkDPmQcW6n3EFwmvE6", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"}, + # btcutil.PKFCompressed, mainnet.PubKeyHashAddrID), + scriptAddress=ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), + make=lambda: addrPK(ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), mainnet), + netParams=mainnet, + ), + dict( + name="mainnet p2pk uncompressed (04)", + addr="0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2" + + "e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3", + encoded="12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"}, + # btcutil.PKFUncompressed, mainnet.PubKeyHashAddrID), + scriptAddress=ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + make=lambda: addrPK( + ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + mainnet, + ), + netParams=mainnet, + ), + dict( + name="mainnet p2pk hybrid (06)", + addr="06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4" + + "0d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e", + encoded="1Ja5rs7XBZnK88EuLVcFqYGMEbBitzchmX", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e"}, + # btcutil.PKFHybrid, mainnet.PubKeyHashAddrID), + scriptAddress=ByteArray("06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e"), + make=lambda: addrPK( + ByteArray("06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e"), + mainnet, + ), + netParams=mainnet, + ), + dict( + name="mainnet p2pk hybrid (07)", + addr="07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65" + + "37a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b", + encoded="1ExqMmf6yMxcBMzHjbj41wbqYuqoX6uBLG", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b"}, + # btcutil.PKFHybrid, mainnet.PubKeyHashAddrID), + scriptAddress=ByteArray("07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b"), + make=lambda: addrPK( + ByteArray("07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b"), + mainnet, + ), + netParams=mainnet, + ), + dict( + name="testnet p2pk compressed (02)", + addr="02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4", + encoded="mhiDPVP2nJunaAgTjzWSHCYfAqxxrxzjmo", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"}, + # btcutil.PKFCompressed, chaincfg.TestNet3Params.PubKeyHashAddrID), + scriptAddress=ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), + make=lambda: addrPK(ByteArray("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), testnet), + netParams=testnet, + ), + dict( + name="testnet p2pk compressed (03)", + addr="03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65", + encoded="mkPETRTSzU8MZLHkFKBmbKppxmdw9qT42t", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"}, + # btcutil.PKFCompressed, chaincfg.TestNet3Params.PubKeyHashAddrID), + scriptAddress=ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), + make=lambda: addrPK(ByteArray("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), testnet), + netParams=testnet, + ), + dict( + name="testnet p2pk uncompressed (04)", + addr="0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5" + + "cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3", + encoded="mh8YhPYEAYs3E7EVyKtB5xrcfMExkkdEMF", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"}, + # btcutil.PKFUncompressed, chaincfg.TestNet3Params.PubKeyHashAddrID), + scriptAddress=ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + make=lambda: addrPK( + ByteArray("0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3"), + testnet, + ), + netParams=testnet, + ), + dict( + name="testnet p2pk hybrid (06)", + addr="06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b" + + "40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e", + encoded="my639vCVzbDZuEiX44adfTUg6anRomZLEP", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e"}, + # btcutil.PKFHybrid, chaincfg.TestNet3Params.PubKeyHashAddrID), + scriptAddress=ByteArray("06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e"), + make=lambda: addrPK( + ByteArray("06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e"), + testnet, + ), + netParams=testnet, + ), + dict( + name="testnet p2pk hybrid (07)", + addr="07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6" + + "537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b", + encoded="muUnepk5nPPrxUTuTAhRqrpAQuSWS5fVii", + valid=True, + # result=btcutil.TstAddressPubKey( + # ByteArray( + # "07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b"}, + # btcutil.PKFHybrid, chaincfg.TestNet3Params.PubKeyHashAddrID), + scriptAddress=ByteArray("07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b"), + make=lambda: addrPK( + ByteArray("07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b"), + testnet, + ), + netParams=testnet, + ), + + # Segwit address tests. + dict( + name="segwit mainnet p2wpkh v0", + # addr was capitalized in the btcutil tests. I didn't feel that was important. + addr="bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + encoded="bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + valid=True, + # result=btcutil.TstAddressWitnessPubKeyHash( + # 0, + # [20]byte{ + # "751e76e8199196d454941c45d1b3a323f1433bd6"}, + # mainnet.Bech32HRPSegwit), + scriptAddress=ByteArray("751e76e8199196d454941c45d1b3a323f1433bd6"), + make=lambda: addrPKH_w(ByteArray("751e76e8199196d454941c45d1b3a323f1433bd6"), mainnet), + netParams=mainnet, + ), + dict( + name="segwit mainnet p2wsh v0", + addr="bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", + encoded="bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", + valid=True, + # result=btcutil.TstAddressWitnessScriptHash( + # 0, + # [32]byte{ + # "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"}, + # mainnet.Bech32HRPSegwit), + scriptAddress=ByteArray("1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), + make=lambda: addrSHH_w(ByteArray("1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), mainnet), + netParams=mainnet, + ), + dict( + name="segwit testnet p2wpkh v0", + addr="tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", + encoded="tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", + valid=True, + # result=btcutil.TstAddressWitnessPubKeyHash( + # 0, + # [20]byte{ + # "751e76e8199196d454941c45d1b3a323f1433bd6"}, + # chaincfg.TestNet3Params.Bech32HRPSegwit), + scriptAddress=ByteArray("751e76e8199196d454941c45d1b3a323f1433bd6"), + make=lambda:addrPKH_w(ByteArray("751e76e8199196d454941c45d1b3a323f1433bd6"), testnet), + netParams=testnet, + ), + dict( + name="segwit testnet p2wsh v0", + addr="tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + encoded="tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + valid=True, + # result=btcutil.TstAddressWitnessScriptHash( + # 0, + # [32]byte{ + # "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"}, + # chaincfg.TestNet3Params.Bech32HRPSegwit), + scriptAddress=ByteArray("1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), + make=lambda: addrSHH_w(ByteArray("1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), testnet), + netParams=testnet, + ), + dict( + name="segwit testnet p2wsh witness v0", + addr="tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + encoded="tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + valid=True, + # result=btcutil.TstAddressWitnessScriptHash( + # 0, + # [32]byte{ + # "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"}, + # chaincfg.TestNet3Params.Bech32HRPSegwit), + scriptAddress=ByteArray("000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"), + make=lambda: addrSHH_w(ByteArray("000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"), testnet), + netParams=testnet, + ), + # dict( + # name="segwit litecoin mainnet p2wpkh v0", + # addr="LTC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KGMN4N9", + # encoded="ltc1qw508d6qejxtdg4y5r3zarvary0c5xw7kgmn4n9", + # valid=True, + # # result=btcutil.TstAddressWitnessPubKeyHash( + # # 0, + # # [20]byte{ + # # "751e76e8199196d454941c45d1b3a323f1433bd6"}, + # # CustomParams.Bech32HRPSegwit, + # # ), + # scriptAddress=ByteArray("751e76e8199196d454941c45d1b3a323f1433bd6"), + # make=lambda: addrPKH_w(ByteArray("751e76e8199196d454941c45d1b3a323f1433bd6"), foreignNet), + # netParams=foreignNet, + # ), + # # Unsupported witness versions (version 0 only supported at this point) + dict( + name="segwit mainnet witness v1", + addr="bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit mainnet witness v16", + addr="BC1SW50QA3JX3S", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit mainnet witness v2", + addr="bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", + valid=False, + netParams=mainnet, + ), + # Invalid segwit addresses + dict( + name="segwit invalid hrp", + addr="tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + valid=False, + netParams=testnet, + ), + dict( + name="segwit invalid checksum", + addr="bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit invalid witness version", + addr="BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit invalid program length", + addr="bc1rw5uspcuh", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit invalid program length", + addr="bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit invalid program length for witness version 0 (per BIP141)", + addr="BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + valid=False, + netParams=mainnet, + ), + dict( + name="segwit mixed case", + addr="tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + valid=False, + netParams=testnet, + ), + dict( + name="segwit zero padding of more than 4 bits", + addr="tb1pw508d6qejxtdg4y5r3zarqfsj6c3", + valid=False, + netParams=testnet, + ), + dict( + name="segwit non-zero padding in 8-to-5 conversion", + addr="tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + valid=False, + netParams=testnet, + ), + ] + + for test in tests: + # Decode addr and compare error against valid. + err = None + name = test.get("name") + try: + decoded = addrlib.decodeAddress(test["addr"], test["netParams"]) + except (DecredError, ValueError) as e: + err = e + assert (err is None) == test["valid"], f"{name} error: {err}" + + if err is None: + # Ensure the stringer returns the same address as the original. + assert test["addr"] == decoded.string(), name + + # Encode again and compare against the original. + encoded = decoded.encodeAddress() + assert test["encoded"] == encoded + + # Perform type-specific calculations. + if isinstance(decoded, addrlib.AddressPubKeyHash): + d = ByteArray(b58decode(encoded)) + saddr = d[1: 1 + crypto.RIPEMD160_SIZE] + + elif isinstance(decoded, addrlib.AddressScriptHash): + d = ByteArray(b58decode(encoded)) + saddr = d[1: 1 + crypto.RIPEMD160_SIZE] + + elif isinstance(decoded, addrlib.AddressPubKey): + # Ignore the error here since the script + # address is checked below. + try: + saddr = ByteArray(decoded.string()) + except ValueError: + saddr = test["saddr"] + + elif isinstance(decoded, addrlib.AddressWitnessPubKeyHash): + _, addrb = bech32.decode(test["netParams"].Bech32HRPSegwit, encoded) + saddr = ByteArray(addrb) + + elif isinstance(decoded, addrlib.AddressWitnessScriptHash): + _, addrb = bech32.decode(test["netParams"].Bech32HRPSegwit, encoded) + saddr = ByteArray(addrb) + + else: + raise AssertionError( + f"Decoded address is of unknown type {type(decoded)}" + ) + + # Check script address, as well as the Hash160 method for P2PKH and + # P2SH addresses. + assert saddr == decoded.scriptAddress(), name + + if isinstance(decoded, addrlib.AddressPubKeyHash): + assert decoded.pkHash == saddr + + if isinstance(decoded, addrlib.AddressScriptHash): + assert decoded.hash160() == saddr + + make = test.get("make") + if not test["valid"]: + # If address is invalid, but a creation function exists, + # verify that it raises a DecredError. + if make: + try: + make() + raise AssertionError("invalid tests should raise exception") + except DecredError: + pass + continue + + # Valid test, compare address created with f against expected result. + addr = make() + assert addr != object() + if not test.get("skipComp"): + assert decoded == addr, name + assert decoded == addr.string(), name + assert addr.string() == decoded, name + assert addr.scriptAddress() == test["scriptAddress"], name + + # Test blobbing + b = addrlib.Address.blob(addr) + reAddr = addrlib.Address.unblob(b) + assert addr == reAddr + + +def test_EncodeDecodeWIF(): + validEncodeCases = [ + dict( + privateKey=ByteArray("0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d"), + net=mainnet, + compress=False, + wif="5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ", + publicKey=ByteArray("04d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645cd85228a6fb29940e858e7e55842ae2bd115d1ed7cc0e82d934e929c97648cb0a"), + name="encodeValidUncompressedMainNetWif", + ), + dict( + privateKey=ByteArray("dda35a1488fb97b6eb3fe6e9ef2a25814e396fb5dc295fe994b96789b21a0398"), + net=testnet, + compress=True, + wif="cV1Y7ARUr9Yx7BR55nTdnR7ZXNJphZtCCMBTEZBJe1hXt2kB684q", + publicKey=ByteArray("02eec2540661b0c39d271570742413bd02932dd0093493fd0beced0b7f93addec4"), + name="encodeValidCompressedTestNet3Wif", + ), + ] + + for validCase in validEncodeCases: + priv = PrivateKey.fromBytes(validCase["privateKey"]) + wif = addrlib.WIF(privKey=priv, compressPubKey=validCase["compress"], netID=validCase["net"]) + + assert wif.isForNet(validCase["net"]) + + assert wif.serializePubKey() == validCase["publicKey"] + + encWIF = wif.string() + assert encWIF == validCase["wif"] + + assert addrlib.WIF.decode(encWIF).string() == validCase["wif"] + + invalidDecodeCases = [ + # name string + # wif string + # err error + dict( + name="decodeInvalidLengthWif", + wif="deadbeef", + ), + dict( + name="decodeInvalidCompressMagicWif", + wif="KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sfZr2ym", + ), + dict( + name="decodeInvalidChecksumWif", + wif="5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTj", + ), + ] + + for invalidCase in invalidDecodeCases: + with pytest.raises(DecredError): + addrlib.WIF.decode(invalidCase["wif"]) diff --git a/decred/tests/unit/btc/wire/test_msgtx.py b/decred/tests/unit/btc/wire/test_msgtx.py new file mode 100644 index 00000000..48667501 --- /dev/null +++ b/decred/tests/unit/btc/wire/test_msgtx.py @@ -0,0 +1,569 @@ +""" +Copyright (c) 2019-2020, the Decred developers +See LICENSE for details +""" + +import pytest + +from decred import DecredError +from decred.btc.wire import msgtx, wire +from decred.util.encode import ByteArray + + +def test_txHash(): + """ + test_txHash tests the ability to generate the hash of a transaction accurately. + """ + # Hash of first transaction from block 113875. + txid = "f051e59b5e2503ac626d03aaeac8ab7be2d72ba4b7e97119c5852d70d52dcb86" + wantHash = reversed(ByteArray(txid)) + + # First transaction from block 113875. + msgTx = msgtx.MsgTx() + txIn = msgtx.TxIn( + previousOutPoint=msgtx.OutPoint( + txHash=ByteArray(length=32), + idx=0xffffffff, + ), + signatureScript=ByteArray("0431dc001b0162"), + sequence=0xffffffff, + ) + txOut = msgtx.TxOut( + value=5000000000, + pkScript=ByteArray([ + 0x41, # OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, # 65-byte signature + 0xac, # OP_CHECKSIG + ]), + ) + msgTx.addTxIn(txIn) + msgTx.addTxOut(txOut) + # msgTx.lockTime = 0 + + # Ensure the hash produced is expected. + assert msgTx.hash() == wantHash + + +def test_wTxSha(): + """ + test_wTxSha tests the ability to generate the wtxid, and txid of a transaction + with witness inputs accurately. + """ + txid = "0f167d1385a84d1518cfee208b653fc9163b605ccf1b75347e2850b3e2eb19f3" + wantHash = reversed(ByteArray(txid)) + + wTxid = "0858eab78e77b6b033da30f46699996396cf48fcf625a783c85a51403e175e74" + wantWitnessHash = reversed(ByteArray(wTxid)) + + # From block 23157 in a past version of segnet. + msgTx = msgtx.MsgTx() + txIn = msgtx.TxIn( + previousOutPoint=msgtx.OutPoint( + txHash=ByteArray("a53352d5135766f03076597418263da2d9c958315968fea823529467481ff9cd"), + idx=19, + ), + witness=[ + ByteArray( # 70-byte signature + "3043021f4d2381dc97f182abd8185f51753018523212f5ddc07cc4e63a8dc03658da190220608b5c4d92b86b6de7d78ef23a2fa735bcb59b914a48b0e187c5e7569a18197001", + ), + ByteArray( # 33-byte serialize pub key + "0307ead084807eb76346df6977000c89392f45c76425b26181f521d7f370066a8f", + ), + ], + sequence=0xffffffff, + ) + txOut = msgtx.TxOut( + value=395019, + pkScript=ByteArray([ + 0x00, # Version 0 witness program + 0x14, # OP_DATA_20 + 0x9d, 0xda, 0xc6, 0xf3, 0x9d, 0x51, 0xe0, 0x39, + 0x8e, 0x53, 0x2a, 0x22, 0xc4, 0x1b, 0xa1, 0x89, + 0x40, 0x6a, 0x85, 0x23, # 20-byte pub key hash + ]), + ) + msgTx.addTxIn(txIn) + msgTx.addTxOut(txOut) + # msgTx.LockTime = 0 + + # Ensure the correct txid, and wtxid is produced as expected. + assert msgTx.hash() == wantHash + assert msgTx.witnessHash() == wantWitnessHash + + +def test_TxWire(): + """ + test_TxWire tests the MsgTx wire encode and decode for various numbers + of transaction inputs and outputs and protocol versions. + """ + # Empty tx message. + noTx = msgtx.MsgTx(version=1) + noTxEncoded = ByteArray([ + 0x01, 0x00, 0x00, 0x00, # Version + 0x00, # Varint for number of input transactions + 0x00, # Varint for number of output transactions + 0x00, 0x00, 0x00, 0x00, # Lock time + ]) + + tests = [ + dict( + inTx=noTx, + out=noTx, + buf=noTxEncoded, + pver=wire.ProtocolVersion, + enc=msgtx.BaseEncoding, + ), + + # Latest protocol version with multiple transactions. + dict( + inTx=multiTx, + out=multiTx, + buf=multiTxEncoded, + pver=wire.ProtocolVersion, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version BIP0035Version with no transactions. + dict( + inTx=noTx, + out=noTx, + buf=noTxEncoded, + pver=wire.BIP0035Version, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version BIP0035Version with multiple transactions. + dict( + inTx=multiTx, + out=multiTx, + buf=multiTxEncoded, + pver=wire.BIP0035Version, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version BIP0031Version with no transactions. + dict( + inTx=noTx, + out=noTx, + buf=noTxEncoded, + pver=wire.BIP0031Version, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version BIP0031Version with multiple transactions. + dict( + inTx=multiTx, + out=multiTx, + buf=multiTxEncoded, + pver=wire.BIP0031Version, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version NetAddressTimeVersion with no transactions. + dict( + inTx=noTx, + out=noTx, + buf=noTxEncoded, + pver=wire.NetAddressTimeVersion, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version NetAddressTimeVersion with multiple transactions. + dict( + inTx=multiTx, + out=multiTx, + buf=multiTxEncoded, + pver=wire.NetAddressTimeVersion, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version MultipleAddressVersion with no transactions. + dict( + inTx=noTx, + out=noTx, + buf=noTxEncoded, + pver=wire.MultipleAddressVersion, + enc=msgtx.BaseEncoding, + ), + + # # Protocol version MultipleAddressVersion with multiple transactions. + dict( + inTx=multiTx, + out=multiTx, + buf=multiTxEncoded, + pver=wire.MultipleAddressVersion, + enc=msgtx.BaseEncoding, + ), + ] + + for test in tests: + # Encode the message to wire format. + b = test["inTx"].btcEncode(test["pver"], test["enc"]) + assert b == test["buf"] + + msgTx = msgtx.MsgTx.btcDecode(test["buf"].copy(), test["pver"], test["enc"]) + assert msgTx == test["out"] + + +def test_TxSerialize(): + """ + test_TxSerialize tests MsgTx serialize and deserialize. + """ + noTx = msgtx.MsgTx(version=1) + noTxEncoded = ByteArray([ + 0x01, 0x00, 0x00, 0x00, # Version + 0x00, # Varint for number of input transactions + 0x00, # Varint for number of output transactions + 0x00, 0x00, 0x00, 0x00, # Lock time + ]) + + tests = [ + # No transactions. + dict( + inTx=noTx, + out=noTx, + buf=noTxEncoded, + pkScriptLocs=[], + witness=False, + ), + + # Multiple transactions. + dict( + inTx=multiTx, + out=multiTx, + buf=multiTxEncoded, + pkScriptLocs=multiTxPkScriptLocs, + witness=True, + ), + # Multiple outputs witness transaction. + dict( + inTx=multiWitnessTx, + out=multiWitnessTx, + buf=multiWitnessTxEncoded, + pkScriptLocs=multiWitnessTxPkScriptLocs, + witness=True, + ), + ] + + for i, test in enumerate(tests): + # Serialize the transaction. + buf = test["inTx"].serialize() + assert buf == test["buf"] + + # Deserialize the transaction. + if test["witness"]: + tx = msgtx.MsgTx.deserialize(test["buf"].copy()) + else: + tx = msgtx.MsgTx.deserializeNoWitness(test["buf"]) + + assert tx == test["out"] + + # Ensure the public key script locations are accurate. + pkScriptLocs = test["inTx"].pkScriptLocs() + assert all(a == b for a, b in zip(pkScriptLocs, test["pkScriptLocs"])) + + for j, loc in enumerate(pkScriptLocs): + wantPkScript = test["inTx"].txOut[j].pkScript + gotPkScript = test["buf"][loc: loc+len(wantPkScript)] + assert gotPkScript == wantPkScript + + +def test_TxOverflowErrors(): + """ + test_TxOverflowErrors performs tests to ensure deserializing transactions + which are intentionally crafted to use large values for the variable number + of inputs and outputs are handled properly. This could otherwise potentially + be used as an attack vector. + """ + # Use protocol version 70001 and transaction version 1 specifically + # here instead of the latest values because the test data is using + # bytes encoded with those versions. + pver = 70001 + + tests = [ + ( + "too many inputs", + ByteArray([ + 0x00, 0x00, 0x00, 0x01, # Version + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, # Varint for number of input transactions + ]), + ), + + # Transaction that claims to have ~uint64(0) outputs. + ( + "too many outputs", + ByteArray([ + 0x00, 0x00, 0x00, 0x01, # Version + 0x00, # Varint for number of input transactions + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, # Varint for number of output transactions + ]), + ), + + # Transaction that has an input with a signature script that + # claims to have ~uint64(0) length. + ( + "sig script too long", + ByteArray([ + 0x00, 0x00, 0x00, 0x01, # Version + 0x01, # Varint for number of input transactions + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Previous output hash + 0xff, 0xff, 0xff, 0xff, # Prevous output index + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, # Varint for length of signature script + ]), + ), + + # Transaction that has an output with a public key script + # that claims to have ~uint64(0) length. + ( + "pubkey script too long", + ByteArray([ + 0x00, 0x00, 0x00, 0x01, # Version + 0x01, # Varint for number of input transactions + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Previous output hash + 0xff, 0xff, 0xff, 0xff, # Prevous output index + 0x00, # Varint for length of signature script + 0xff, 0xff, 0xff, 0xff, # Sequence + 0x01, # Varint for number of output transactions + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Transaction amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, # Varint for length of public key script + ]), + ), + ] + + for name, b in tests: + with pytest.raises(DecredError): + msgtx.MsgTx.btcDecode(b.copy(), pver, msgtx.BaseEncoding) + + with pytest.raises(DecredError): + msgtx.MsgTx.deserialize(b) + + +def test_TxSerializeSizeStripped(): + """ + test_TxSerializeSizeStripped performs tests to ensure the serialize size for + various transactions is accurate. + """ + # Empty tx message. + assert msgtx.MsgTx(version=1).serializeSizeStripped() == 10 + + # Transcaction with an input and an output. + assert multiTx.serializeSizeStripped() == 210 + + # Transaction with an input which includes witness data, and + # one output. Note that this uses SerializeSizeStripped which + # excludes the additional bytes due to witness data encoding. + assert multiWitnessTx.serializeSizeStripped() == 82 + + +def test_TxWitnessSize(): + """ + test_TxWitnessSize performs tests to ensure that the serialized size for + various types of transactions that include witness data is accurate. + """ + assert multiWitnessTx.serializeSize() == 190 + + +multiTx = msgtx.MsgTx( + version=1, + txIn=[ + msgtx.TxIn( + previousOutPoint=msgtx.OutPoint( + txHash=ByteArray(length=32), + idx=0xffffffff, + ), + signatureScript=ByteArray("0431dc001b0162"), + sequence=0xffffffff, + ), + ], + txOut=[ + msgtx.TxOut( + value=0x12a05f200, + pkScript=ByteArray([ + 0x41, # OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, # 65-byte signature + 0xac, # OP_CHECKSIG + ]), + ), + msgtx.TxOut( + value=0x5f5e100, + pkScript=ByteArray([ + 0x41, # OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, # 65-byte signature + 0xac, # OP_CHECKSIG + ]), + ), + ], + lockTime=0, +) + +# multiTxEncoded is the wire encoded bytes for multiTx using protocol version +# 60002 and is used in the various tests. +multiTxEncoded = ByteArray([ + 0x01, 0x00, 0x00, 0x00, # Version + 0x01, # Varint for number of input transactions + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Previous output hash + 0xff, 0xff, 0xff, 0xff, # Prevous output index + 0x07, # Varint for length of signature script + 0x04, 0x31, 0xdc, 0x00, 0x1b, 0x01, 0x62, # Signature script + 0xff, 0xff, 0xff, 0xff, # Sequence + 0x02, # Varint for number of output transactions + 0x00, 0xf2, 0x05, 0x2a, 0x01, 0x00, 0x00, 0x00, # Transaction amount + 0x43, # Varint for length of pk script + 0x41, # OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, # 65-byte signature + 0xac, # OP_CHECKSIG + 0x00, 0xe1, 0xf5, 0x05, 0x00, 0x00, 0x00, 0x00, # Transaction amount + 0x43, # Varint for length of pk script + 0x41, # OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, # 65-byte signature + 0xac, # OP_CHECKSIG + 0x00, 0x00, 0x00, 0x00, # Lock time +]) + +multiTxPkScriptLocs = (63, 139) + +multiWitnessTx = msgtx.MsgTx( + version=1, + txIn=[ + msgtx.TxIn( + previousOutPoint=msgtx.OutPoint( + txHash=ByteArray([ + 0xa5, 0x33, 0x52, 0xd5, 0x13, 0x57, 0x66, 0xf0, + 0x30, 0x76, 0x59, 0x74, 0x18, 0x26, 0x3d, 0xa2, + 0xd9, 0xc9, 0x58, 0x31, 0x59, 0x68, 0xfe, 0xa8, + 0x23, 0x52, 0x94, 0x67, 0x48, 0x1f, 0xf9, 0xcd, + ]), + idx=19, + ), + signatureScript=ByteArray(), + witness=[ + ByteArray([ # 70-byte signature + 0x30, 0x43, 0x02, 0x1f, 0x4d, 0x23, 0x81, 0xdc, + 0x97, 0xf1, 0x82, 0xab, 0xd8, 0x18, 0x5f, 0x51, + 0x75, 0x30, 0x18, 0x52, 0x32, 0x12, 0xf5, 0xdd, + 0xc0, 0x7c, 0xc4, 0xe6, 0x3a, 0x8d, 0xc0, 0x36, + 0x58, 0xda, 0x19, 0x02, 0x20, 0x60, 0x8b, 0x5c, + 0x4d, 0x92, 0xb8, 0x6b, 0x6d, 0xe7, 0xd7, 0x8e, + 0xf2, 0x3a, 0x2f, 0xa7, 0x35, 0xbc, 0xb5, 0x9b, + 0x91, 0x4a, 0x48, 0xb0, 0xe1, 0x87, 0xc5, 0xe7, + 0x56, 0x9a, 0x18, 0x19, 0x70, 0x01, + ]), + ByteArray([ # 33-byte serialize pub key + 0x03, 0x07, 0xea, 0xd0, 0x84, 0x80, 0x7e, 0xb7, + 0x63, 0x46, 0xdf, 0x69, 0x77, 0x00, 0x0c, 0x89, + 0x39, 0x2f, 0x45, 0xc7, 0x64, 0x25, 0xb2, 0x61, + 0x81, 0xf5, 0x21, 0xd7, 0xf3, 0x70, 0x06, 0x6a, + 0x8f, + ]), + ], + sequence=0xffffffff, + ), + ], + txOut=[ + msgtx.TxOut( + value=395019, + pkScript=ByteArray([ # p2wkh output + 0x00, # Version 0 witness program + 0x14, # OP_DATA_20 + 0x9d, 0xda, 0xc6, 0xf3, 0x9d, 0x51, 0xe0, 0x39, + 0x8e, 0x53, 0x2a, 0x22, 0xc4, 0x1b, 0xa1, 0x89, + 0x40, 0x6a, 0x85, 0x23, # 20-byte pub key hash + ]), + ), + ], +) + +multiWitnessTxEncoded = ByteArray([ + 0x1, 0x0, 0x0, 0x0, # Version + msgtx.TxFlagMarker, # Marker byte indicating 0 inputs, or a segwit encoded tx + msgtx.WitnessFlag, # Flag byte + 0x1, # Varint for number of inputs + 0xa5, 0x33, 0x52, 0xd5, 0x13, 0x57, 0x66, 0xf0, + 0x30, 0x76, 0x59, 0x74, 0x18, 0x26, 0x3d, 0xa2, + 0xd9, 0xc9, 0x58, 0x31, 0x59, 0x68, 0xfe, 0xa8, + 0x23, 0x52, 0x94, 0x67, 0x48, 0x1f, 0xf9, 0xcd, # Previous output hash + 0x13, 0x0, 0x0, 0x0, # Little endian previous output index + 0x0, # No sig script (this is a witness input) + 0xff, 0xff, 0xff, 0xff, # Sequence + 0x1, # Varint for number of outputs + 0xb, 0x7, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, # Output amount + 0x16, # Varint for length of pk script + 0x0, # Version 0 witness program + 0x14, # OP_DATA_20 + 0x9d, 0xda, 0xc6, 0xf3, 0x9d, 0x51, 0xe0, 0x39, + 0x8e, 0x53, 0x2a, 0x22, 0xc4, 0x1b, 0xa1, 0x89, + 0x40, 0x6a, 0x85, 0x23, # 20-byte pub key hash + 0x2, # Two items on the witness stack + 0x46, # 70 byte stack item + 0x30, 0x43, 0x2, 0x1f, 0x4d, 0x23, 0x81, 0xdc, + 0x97, 0xf1, 0x82, 0xab, 0xd8, 0x18, 0x5f, 0x51, + 0x75, 0x30, 0x18, 0x52, 0x32, 0x12, 0xf5, 0xdd, + 0xc0, 0x7c, 0xc4, 0xe6, 0x3a, 0x8d, 0xc0, 0x36, + 0x58, 0xda, 0x19, 0x2, 0x20, 0x60, 0x8b, 0x5c, + 0x4d, 0x92, 0xb8, 0x6b, 0x6d, 0xe7, 0xd7, 0x8e, + 0xf2, 0x3a, 0x2f, 0xa7, 0x35, 0xbc, 0xb5, 0x9b, + 0x91, 0x4a, 0x48, 0xb0, 0xe1, 0x87, 0xc5, 0xe7, + 0x56, 0x9a, 0x18, 0x19, 0x70, 0x1, + 0x21, # 33 byte stack item + 0x3, 0x7, 0xea, 0xd0, 0x84, 0x80, 0x7e, 0xb7, + 0x63, 0x46, 0xdf, 0x69, 0x77, 0x0, 0xc, 0x89, + 0x39, 0x2f, 0x45, 0xc7, 0x64, 0x25, 0xb2, 0x61, + 0x81, 0xf5, 0x21, 0xd7, 0xf3, 0x70, 0x6, 0x6a, + 0x8f, + 0x0, 0x0, 0x0, 0x0, # Lock time +]) + +multiWitnessTxPkScriptLocs = [58]