예제 #1
0
def WatchWalletTest(rpc: RPC) -> None:
    #Keys to send funds to later.
    keys: List[bytes] = [
        Ristretto.SigningKey(i.to_bytes(1, "little") * 32).get_verifying_key()
        for i in range(5)
    ]

    #Backup the Mnemonic so we can independently derive this data and verify it.
    mnemonic: str = rpc.call("personal", "getMnemonic")

    #Node's Wallet's keys.
    nodeKeys: List[bytes] = [getPublicKey(mnemonic, "", i) for i in range(4)]
    nodeAddresses: List[str] = [getAddress(mnemonic, "", i) for i in range(4)]

    #Convert this to a WatchWallet node.
    account: Dict[str, Any] = rpc.call("personal", "getAccount")
    rpc.call("personal", "setAccount", account)
    if rpc.call("personal", "getAccount") != account:
        raise TestError("Meros set a different account.")

    #Verify it has the correct initial address.
    if rpc.call("personal", "getAddress") != nodeAddresses[0]:
        raise TestError("WatchWallet has an incorrect initial address.")

    #Load the vectors.
    #This test requires 3 Claims be available.
    vectors: Dict[str, Any]
    with open("e2e/Vectors/RPC/Personal/WatchWallet.json", "r") as file:
        vectors = json.loads(file.read())
    transactions: Transactions = Transactions.fromJSON(vectors["transactions"])

    #The order of the Claims isn't relevant to this test.
    claims: List[Claim] = []
    for tx in transactions.txs.values():
        claims.append(Claim.fromTransaction(tx))

    def test() -> None:
        #Send to it.
        sends: List[bytes] = [createSend(rpc, claims[0], nodeKeys[0])]
        verify(rpc, sends[-1])

        #Test the most basic template possible.
        checkTemplate(
            rpc, mnemonic, {
                "outputs": [{
                    "address":
                    bech32_encode("mr", convertbits(
                        bytes([0]) + keys[0], 8, 5)),
                    "amount":
                    "1"
                }]
            }, [{
                "hash": sends[-1].hex().upper(),
                "nonce": 0,
                "change": False,
                "index": 0
            }], [{
                "key": keys[0].hex().upper(),
                "amount": "1"
            }, {
                "key": getChangePublicKey(mnemonic, "", 0).hex().upper(),
                "amount": str(claims[0].amount - 1)
            }])

        #Verify it has the correct next address.
        if rpc.call("personal", "getAddress") != nodeAddresses[1]:
            raise TestError("WatchWallet has an incorrect next address.")

        #Send to it.
        sends.append(createSend(rpc, claims[1], nodeKeys[1]))
        verify(rpc, sends[-1])

        #Get and send to one more, yet don't verify it yet.
        if rpc.call("personal", "getAddress") != nodeAddresses[2]:
            raise TestError("WatchWallet has an incorrect next next address.")
        sends.append(createSend(rpc, claims[2], nodeKeys[2]))

        #Verify it can get UTXOs properly.
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(
            [{
                "address": getAddress(mnemonic, "", i),
                "hash": sends[i].hex().upper(),
                "nonce": 0
            } for i in range(2)]):
            raise TestError("WatchWallet Meros couldn't get its UTXOs.")

        #Also test balance getting.
        if rpc.call("personal", "getBalance") != str(
                sum([
                    int(
                        rpc.call("transactions", "getTransaction",
                                 {"hash": send.hex()})["outputs"][0]["amount"])
                    for send in sends[:2]
                ])):
            raise TestError("WatchWallet Meros couldn't get its balance.")

        #Verify the third Send.
        verify(rpc, sends[-1])

        #Close the sockets for now.
        rpc.meros.live.connection.close()
        rpc.meros.sync.connection.close()

        #Verify getUTXOs again. Redundant thanks to the extensive getUTXO testing elsewhere, yet valuable.
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(
            [{
                "address": getAddress(mnemonic, "", i),
                "hash": sends[i].hex().upper(),
                "nonce": 0
            } for i in range(3)]):
            raise TestError("WatchWallet Meros couldn't get its UTXOs.")

        #Again test the balance.
        if rpc.call("personal", "getBalance") != str(
                sum([
                    int(
                        rpc.call("transactions", "getTransaction",
                                 {"hash": send.hex()})["outputs"][0]["amount"])
                    for send in sends[:3]
                ])):
            raise TestError("WatchWallet Meros couldn't get its balance.")

        #Verify it can craft a Transaction Template properly.
        claimsAmount: int = sum(claim.amount for claim in claims)
        amounts: List[int] = [
            claimsAmount // 4, claimsAmount // 4, claimsAmount // 5
        ]
        amounts.append(claimsAmount - sum(amounts))
        req: Dict[str, Any] = {
            "outputs": [{
                "address":
                bech32_encode("mr", convertbits(bytes([0]) + keys[0], 8, 5)),
                "amount":
                str(amounts[0])
            }, {
                "address":
                bech32_encode("mr", convertbits(bytes([0]) + keys[1], 8, 5)),
                "amount":
                str(amounts[1])
            }, {
                "address":
                bech32_encode("mr", convertbits(bytes([0]) + keys[2], 8, 5)),
                "amount":
                str(amounts[2])
            }]
        }
        inputs: List[Dict[str, Any]] = [{
            "hash": sends[i].hex().upper(),
            "nonce": 0,
            "change": False,
            "index": i
        } for i in range(3)]
        outputs: List[Dict[str, Any]] = [{
            "key": keys[i].hex().upper(),
            "amount": str(amounts[i])
        } for i in range(3)] + [{
            "key":
            getChangePublicKey(mnemonic, "", 0).hex().upper(),
            "amount":
            str(amounts[-1])
        }]
        checkTemplate(rpc, mnemonic, req, inputs, outputs)

        #Specify only to use specific addresses and verify Meros does so.
        req["from"] = [nodeAddresses[1], nodeAddresses[2]]

        #Correct the amounts so this is feasible.
        del req["outputs"][-1]
        del outputs[-2]
        #Remove the change output amount and actual output amount.
        for _ in range(2):
            del amounts[-1]
        claimsAmount -= claims[-1].amount
        #Correct the change output.
        outputs[-1]["amount"] = str(claimsAmount - sum(amounts))

        del inputs[0]
        checkTemplate(rpc, mnemonic, req, inputs, outputs)
        del req["from"]

        #Use the change address in question and verify the next template uses the next change address.
        #This is done via creating a Send which doesn't spend all of inputs value.
        #Also tests Meros handles Sends, and therefore templates, which don't use all funds.
        change: bytes = getChangePublicKey(mnemonic, "", 0)

        #Convert to a Wallet in order to do so.
        rpc.call("personal", "setWallet", {"mnemonic": mnemonic})
        send: Dict[str, Any] = rpc.call(
            "transactions", "getTransaction", {
                "hash":
                rpc.call(
                    "personal", "send", {
                        "outputs": [{
                            "address":
                            bech32_encode(
                                "mr", convertbits(bytes([0]) + change, 8, 5)),
                            "amount":
                            "1"
                        }]
                    })
            })
        #Convert back.
        rpc.call("personal", "setAccount", account)

        #Reconnect.
        sleep(65)
        rpc.meros.liveConnect(Blockchain().blocks[0].header.hash)
        rpc.meros.syncConnect(Blockchain().blocks[0].header.hash)

        #Verify the Send so the Wallet doesn't lose Meros from consideration.
        verify(rpc, bytes.fromhex(send["hash"]))

        #Verify the Send's accuracy.
        if len(send["inputs"]) != 1:
            raise TestError("Meros used more inputs than neccessary.")
        if send["outputs"] != [
            {
                "key": change.hex().upper(),
                "amount": "1"
            },
                #Uses the existing, unused, change address as change.
                #While this Transaction will make it used, that isn't detected.
                #This isn't worth programming around due to the lack of implications except potentially minor metadata.
            {
                "key": change.hex().upper(),
                "amount": str(claims[0].amount - 1)
            }
        ]:
            raise TestError("Send outputs weren't as expected.")

        if rpc.call("personal", "getTransactionTemplate",
                    req)["outputs"][-1]["key"] != getChangePublicKey(
                        mnemonic, "", 1).hex().upper():
            raise TestError("Meros didn't move to the next change address.")

        #Specify an explicit change address.
        req["change"] = nodeAddresses[3]
        if rpc.call("personal", "getTransactionTemplate",
                    req)["outputs"][-1]["key"] != nodeKeys[3].hex().upper():
            raise TestError(
                "Meros didn't handle an explicitly set change address.")

        #Verify RPC methods which require the private key error properly.
        #Tests via getMnemonic and data.
        try:
            rpc.call("personal", "getMnemonic")
            raise TestError()
        except Exception as e:
            if str(e) != "-3 This is a WatchWallet node; no Mnemonic is set.":
                raise TestError(
                    "getMnemonic didn't error as expected when Meros didn't have a Wallet."
                )

        try:
            rpc.call("personal", "data", {"data": "abc"})
            raise TestError()
        except Exception as e:
            if str(e) != "-3 This is a WatchWallet node; no Mnemonic is set.":
                raise TestError(
                    "data didn't error as expected when Meros didn't have a Wallet."
                )

        #Also test getMeritHolderKey, as no Merit Holder key should exist.
        try:
            rpc.call("personal", "getMeritHolderKey")
            raise TestError()
        except Exception as e:
            if str(
                    e
            ) != "-3 Node is running as a WatchWallet and has no Merit Holder.":
                raise TestError(
                    "data didn't error as expected when Meros didn't have a Wallet."
                )

        #Try calling getTransactionTemplate spending way too much Meros.
        try:
            rpc.call(
                "personal", "getTransactionTemplate", {
                    "outputs": [{
                        "address":
                        bech32_encode("mr",
                                      convertbits(bytes([0]) + keys[0], 8, 5)),
                        "amount":
                        str(claimsAmount * 100)
                    }]
                })
            raise TestError()
        except Exception as e:
            if str(e) != "1 Wallet doesn't have enough Meros.":
                raise TestError(
                    "Meros didn't error as expected when told to spend more Meros than it has."
                )

        #Try calling getTransactionTemplate with no outputs.
        try:
            rpc.call("personal", "getTransactionTemplate", {"outputs": []})
            raise TestError()
        except Exception as e:
            if str(e) != "-3 No outputs were provided.":
                raise TestError(
                    "Meros didn't error as expected when told to create a template with no outputs."
                )

        #Try calling getTransactionTemplate with a 0 value output.
        try:
            rpc.call(
                "personal", "getTransactionTemplate", {
                    "outputs": [{
                        "address":
                        bech32_encode("mr",
                                      convertbits(bytes([0]) + keys[0], 8, 5)),
                        "amount":
                        "0"
                    }]
                })
            raise TestError()
        except Exception as e:
            if str(e) != "-3 0 value output was provided.":
                raise TestError(
                    "Meros didn't error as expected when told to create a template with a 0 value output."
                )

    #Use a late enough block we can instantly verify transactions.
    Liver(rpc, vectors["blockchain"], transactions, {50: test}).live()
예제 #2
0
    def test() -> None:
        #Send to the new address and get the next address.
        dest: str = rpc.call("personal", "getAddress")
        last: Send = createSend(rpc, Claim.fromJSON(vectors["newerMintClaim"]),
                                dest)

        utxos: List[Dict[str, Any]] = rpc.call("personal", "getUTXOs")
        if utxos != [{
                "address": dest,
                "hash": last.hash.hex().upper(),
                "nonce": 0
        }]:
            raise TestError(
                "personal_getUTXOs didn't return the correct UTXOs.")

        #Set a different mnemonic to verify the tracked addresses is cleared.
        rpc.call("personal", "setWallet")
        if rpc.call("personal", "getUTXOs") != []:
            raise TestError(
                "Setting a new Mnemonic didn't clear the tracked addresses.")

        #Reload the Mnemonic and verify the UTXOs are correct.
        rpc.call("personal", "setWallet", {"mnemonic": mnemonic})
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            #This error message points out how no addresses are really being discovered yet; this is account zero's address.
            #That said, if the test started at the next address, there'd be a gap.
            #As that's an extra factor, this is tested before gaps are.
            raise TestError("Meros didn't recover the very first address.")

        #Now send to the next address and check accuracy.
        dest = rpc.call("personal", "getAddress")
        last = createSend(rpc, last, dest)
        utxos.append({
            "address": dest,
            "hash": last.hash.hex().upper(),
            "nonce": 0
        })
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            raise TestError("Meros didn't track an implicitly gotten address.")
        rpc.call("personal", "setWallet", {"mnemonic": mnemonic})
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            raise TestError(
                "Meros didn't recover the first address after the initial address."
            )

        #Send funds to the address after the next address; tests a gap when discovering addresses.
        last = createSend(rpc, last, getAddress(mnemonic, "", 3))
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            raise TestError(
                "Meros magically recognized UTXOs as belonging to this Wallet or someone implemented an address cache."
            )
        utxos.append({
            "address": getAddress(mnemonic, "", 3),
            "hash": last.hash.hex().upper(),
            "nonce": 0
        })
        rpc.call("personal", "setWallet", {"mnemonic": mnemonic})
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            raise TestError(
                "Meros didn't discover a used address in the Wallet when there was a gap."
            )

        #Finally, anything 10+ unused addresses out shouldn't be recovered.
        last = createSend(rpc, last, getAddress(mnemonic, "", 14))
        rpc.call("personal", "setWallet", {"mnemonic": mnemonic})
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            raise TestError(
                "Meros recovered an address's UTXOs despite it being 10 unused addresses out."
            )

        #Explicitly generating this address should start tracking it though.
        rpc.call("personal", "getAddress",
                 {"index": getIndex(mnemonic, "", 14)})
        utxos.append({
            "address": getAddress(mnemonic, "", 14),
            "hash": last.hash.hex().upper(),
            "nonce": 0
        })
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(utxos):
            raise TestError(
                "personal_getUTXOs didn't track an address explicitly indexed."
            )

        #If asked for an address, Meros should return the 5th (skip 4).
        #It's the first unused address AFTER used addresss EXCEPT ones explicitly requested.
        #This can, in the future, be juwst the first unused address/include ones explicitly requested (see DerivationTest for commentary on that).
        #This is really meant to ensure consistent behavior until we properly decide otherwise.
        if rpc.call("personal", "getAddress") != getAddress(mnemonic, "", 4):
            raise TestError(
                "Meros didn't return the next unused address (with conditions; see comment)."
            )

        #Mine a Block to flush the Transactions and Verifications to disk.
        sleep(65)
        rpc.meros.liveConnect(Blockchain().blocks[0].header.hash)
        mineBlock(rpc)

        #Existing values used to test getAddress/getUTXOs consistency.
        #The former is thoroughly tested elsewhere, making it quite redundant.
        existing: Dict[str, Any] = {
            "getAddress": rpc.call("personal", "getAddress"),
            "getUTXOs": rpc.call("personal", "getUTXOs")
        }

        #Reboot the node and verify consistency.
        rpc.quit()
        sleep(3)
        rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc)
        if sortUTXOs(rpc.call("personal", "getUTXOs")) != sortUTXOs(
                existing["getUTXOs"]):
            raise TestError(
                "Rebooting the node caused the WalletDB to improperly reload UTXOs."
            )
        if rpc.call("personal", "getAddress") != existing["getAddress"]:
            raise TestError(
                "Rebooting the node caused the WalletDB to improperly reload the next address."
            )

        #Used so Liver doesn't run its own post-test checks.
        #Since we added our own Blocks, those will fail.
        raise SuccessError()
예제 #3
0
    def restOfTest() -> None:
        #Move expected into scope.
        expected: str = getAddress(mnemonic, password, 0)

        #Send to the new address, then call getAddress again. Verify a new address appears.
        last: Send = createSend(
            rpc,
            Claim.fromTransaction(iter(transactions.txs.values()).__next__()),
            expected)
        hashes: List[bytes] = [last.hash]

        expected = getAddress(mnemonic, password, 1)
        if rpc.call("personal", "getAddress") != expected:
            raise TestError(
                "Meros didn't move to the next address once the existing one was used."
            )

        #Send to the new unused address, spending the funds before calling getAddress again.
        #Checks address usage isn't defined as having an UTXO, yet rather any TXO.
        #Also confirm the spending TX with full finalization before checking.
        #Ensures the TXO isn't unspent by any definition.
        last = createSend(rpc, last, expected)
        hashes.append(last.hash)

        #Spending TX.
        send: Send = Send([(hashes[-1], 0)], [(bytes(32), 1)])
        send.signature = ed.sign(b"MEROS" + send.hash,
                                 getPrivateKey(mnemonic, password, 1))
        send.beat(SpamFilter(3))
        if rpc.meros.liveTransaction(send) != rpc.meros.live.recv():
            raise TestError("Meros didn't broadcast back the spending Send.")
        hashes.append(send.hash)

        #In order to finalize, we need to mine 6 Blocks once this Transaction and its parent have Verifications.
        for txHash in hashes:
            sv: SignedVerification = SignedVerification(txHash)
            sv.sign(0, PrivateKey(0))
            if rpc.meros.signedElement(sv) != rpc.meros.live.recv():
                raise TestError("Meros didn't broadcast back a Verification.")

        #Close the sockets while we mine.
        rpc.meros.live.connection.close()
        rpc.meros.sync.connection.close()

        #Mine these to the Wallet on the node so we can test getMeritHolderNick.
        privKey: PrivateKey = PrivateKey(
            bytes.fromhex(rpc.call("personal", "getMeritHolderKey")))
        blockchain: Blockchain = Blockchain.fromJSON(vectors["blockchain"])
        for _ in range(6):
            template: Dict[str, Any] = rpc.call(
                "merit", "getBlockTemplate",
                {"miner": privKey.toPublicKey().serialize().hex()})
            proof: int = -1
            tempHash: bytes = bytes()
            tempSignature: bytes = bytes()
            while ((proof == -1)
                   or ((int.from_bytes(tempHash, "little") *
                        (blockchain.difficulty() * 11 // 10)) > int.from_bytes(
                            bytes.fromhex("FF" * 32), "little"))):
                proof += 1
                tempHash = RandomX(
                    bytes.fromhex(template["header"]) +
                    proof.to_bytes(4, "little"))
                tempSignature = privKey.sign(tempHash).serialize()
                tempHash = RandomX(tempHash + tempSignature)

            rpc.call(
                "merit", "publishBlock", {
                    "id":
                    template["id"],
                    "header":
                    template["header"] + proof.to_bytes(4, "little").hex() +
                    tempSignature.hex()
                })
            blockchain.add(
                Block.fromJSON(
                    rpc.call("merit", "getBlock",
                             {"block": len(blockchain.blocks)})))

        #Verify a new address is returned.
        expected = getAddress(mnemonic, password, 2)
        if rpc.call("personal", "getAddress") != expected:
            raise TestError(
                "Meros didn't move to the next address once the existing one was used."
            )

        #Get a new address after sending to the address after it.
        #Use both, and then call getAddress.
        #getAddress should detect X is used, move to Y, detect Y is used, and move to Z.
        #It shouldn't assume the next address after an used address is unused.
        #Actually has two Ys as one iteration of the code only ran for the next address; not all future addresses.

        #Send to the next next addresses.
        for i in range(2):
            addy: str = getAddress(mnemonic, password, 3 + i)

            #Reopen the sockets. This isn't done outside of the loop due to the time deriving the final address can take.
            #Due to how slow the reference Python code is, it is necessary to redo the socket connections.
            sleep(65)
            rpc.meros.liveConnect(Blockchain().blocks[0].header.hash)
            rpc.meros.syncConnect(Blockchain().blocks[0].header.hash)

            last = createSend(rpc, last, addy)
            if MessageType(rpc.meros.live.recv()
                           [0]) != MessageType.SignedVerification:
                raise TestError(
                    "Meros didn't create and broadcast a SignedVerification for this Send."
                )

            if i == 0:
                #Close them again.
                rpc.meros.live.connection.close()
                rpc.meros.sync.connection.close()

        #Verify getAddress returns the existing next address.
        if rpc.call("personal", "getAddress") != expected:
            raise TestError(
                "Sending to the address after this address caused Meros to consider this address used."
            )

        #Send to the next address.
        last = createSend(rpc, last, expected)
        if MessageType(
                rpc.meros.live.recv()[0]) != MessageType.SignedVerification:
            raise TestError(
                "Meros didn't create and broadcast a SignedVerification for this Send."
            )

        #Verify getAddress returns the address after the next next addresses.
        expected = getAddress(mnemonic, password, 5)
        if rpc.call("personal", "getAddress") != expected:
            raise TestError(
                "Meros didn't return the correct next address after using multiple addresses in a row."
            )

        #Now that we have mined a Block as part of this test, ensure the Merit Holder nick is set.
        if rpc.call("personal", "getMeritHolderNick") != 1:
            raise TestError(
                "Merit Holder nick wasn't made available despite having one.")

        #Sanity check off Mnemonic.
        if rpc.call("personal", "getMnemonic") != mnemonic:
            raise TestError("getMnemonic didn't return the correct Mnemonic.")

        #Existing values used to test getMnemonic/getMeritHolderKey/getMeritHolderNick/getAccount/getAddress consistency.
        existing: Dict[str, Any] = {
            #Should be equal to the mnemonic variable, which is verified in a check above.
            "getMnemonic": rpc.call("personal", "getMnemonic"),
            "getMeritHolderKey": rpc.call("personal", "getMeritHolderKey"),
            "getMeritHolderNick": rpc.call("personal", "getMeritHolderNick"),
            "getAccount": rpc.call("personal", "getAccount"),
            #Should be equal to expected, which is also verified in a check above.
            "getAddress": rpc.call("personal", "getAddress")
        }

        #Set a new seed and verify the Merit Holder nick is cleared.
        rpc.call("personal", "setWallet")
        try:
            rpc.call("personal", "getMeritHolderNick")
            raise TestError("")
        except TestError as e:
            if str(
                    e
            ) != "-2 Wallet doesn't have a Merit Holder nickname assigned.":
                raise TestError(
                    "getMeritHolderNick returned something or an unexpected error when a new Mnemonic was set."
                )

        #Set back the old seed and verify consistency.
        rpc.call("personal", "setWallet", {
            "mnemonic": mnemonic,
            "password": password
        })
        for method in existing:
            if rpc.call("personal", method) != existing[method]:
                raise TestError(
                    "Setting an old seed caused the WalletDB to improperly reload."
                )

        #Verify calling getAddress returns the expected address.
        if rpc.call("personal", "getAddress") != expected:
            raise TestError(
                "Meros returned an address that wasn't next after reloading the seed."
            )

        #Reboot the node and verify consistency.
        rpc.quit()
        sleep(3)
        rpc.meros = Meros(rpc.meros.db, rpc.meros.tcp, rpc.meros.rpc)
        for method in existing:
            if rpc.call("personal", method) != existing[method]:
                raise TestError(
                    "Rebooting the node caused the WalletDB to improperly reload."
                )

        #Used so Liver doesn't run its own post-test checks.
        #Since we added our own Blocks, those will fail.
        raise SuccessError()