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()
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()
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()