From f51bbf4617368c69b9648f94d6184abc9aef7ac4 Mon Sep 17 00:00:00 2001 From: jokpine Date: Thu, 26 May 2022 20:33:26 +0000 Subject: [PATCH] Add find-wallet-depths command --- webcash/walletclient.py | 92 +++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/webcash/walletclient.py b/webcash/walletclient.py index 6505bfe..2d73f9a 100755 --- a/webcash/walletclient.py +++ b/webcash/walletclient.py @@ -21,6 +21,7 @@ import secrets import hashlib import datetime +import string import struct import os import sys @@ -45,6 +46,9 @@ # unused? FEE_AMOUNT = 0 +BATCH_SIZE = 25 +STOP_AFTER_N_TESTED_UNKNOWN_TOKENS = 5 * BATCH_SIZE + WALLET_NAME = "default_wallet.webcash" CHAIN_CODES = { @@ -54,6 +58,22 @@ "MINING": 3, } + +def is_master_secret(s): + return len(s) == 64 and all(c in string.hexdigits for c in s) + + +def get_deterministic_secret(master_secret_hex, chain_code, wallet_depth): + assert is_master_secret(master_secret_hex) + assert chain_code == chain_code.upper() + assert wallet_depth >= 0 + tag = hashlib.sha256(b"webcashwalletv1").digest() + secret = hashlib.sha256(tag + tag) + secret.update(convert_secret_hex_to_bytes(master_secret_hex)) + secret.update(struct.pack(">Q", CHAIN_CODES[chain_code])) # big-endian + secret.update(struct.pack(">Q", wallet_depth)) + return secret.hexdigest() + def convert_secret_hex_to_bytes(secret): """ Convert a string secret to bytes. @@ -71,15 +91,7 @@ def generate_new_secret(webcash_wallet=None, chain_code="RECEIVE", walletdepth=N else: walletdepth = walletdepth - master_secret = webcash_wallet["master_secret"] - master_secret_bytes = convert_secret_hex_to_bytes(master_secret) - - tag = hashlib.sha256(b"webcashwalletv1").digest() - new_secret = hashlib.sha256(tag + tag) - new_secret.update(master_secret_bytes) - new_secret.update(struct.pack(">Q", CHAIN_CODES[chain_code.upper()])) # big-endian - new_secret.update(struct.pack(">Q", walletdepth)) - new_secret = new_secret.hexdigest() + new_secret = get_deterministic_secret(webcash_wallet["master_secret"], chain_code, walletdepth) # Record the change in walletdepth, but don't record the new secret # because (1) it can be re-constructed even if it is lost, and (2) the @@ -239,7 +251,7 @@ def webcash_server_request(url, json_data): raise Exception(f"Something went wrong on the server: {response.content}") json_response = response.json() if json_response.get("status", "") != "success": - raise Exception(f"Something went wrong on the server: {response}") + raise Exception(f"Something went wrong on the server: {response.content}") return json_response def check_wallet(): @@ -255,9 +267,9 @@ def check_wallet(): outputs[str(sk.to_public().hashed_value)] = webcash while outputs: - # Batch into no more than 25 at a time + # Batch into no more than BATCH_SIZE at a time batch = {} - while outputs and len(batch) < 25: + while outputs and len(batch) < BATCH_SIZE: item = outputs.popitem() batch[item[0]] = item[1] @@ -297,6 +309,62 @@ def check_wallet(): def check(): return check_wallet() +@cli.command( + "find-wallet-depths", short_help="[advanced] Find the 'walletdepths' parameters of a given master secret key by scanning." +) +@click.argument("master_secret") +def find_wallet_depths(master_secret): + if not is_master_secret(master_secret): + raise click.ClickException( + "Invalid master secret. Hex string of length 64 expected." + ) + + resulting_wallet_depths = {} + for chain_code in CHAIN_CODES.keys(): + max_used_wallet_depth = -1 + for step_size in [100000, 10000, 1000, 100, 10, 1]: + wallet_depth = max(0, max_used_wallet_depth) + unknown_count = 0 + while unknown_count < STOP_AFTER_N_TESTED_UNKNOWN_TOKENS: + hash_to_webcash_and_depth = {} + while len(hash_to_webcash_and_depth) < BATCH_SIZE: + hex_secret = get_deterministic_secret( + master_secret, chain_code, wallet_depth + ) + secret_webcash = SecretWebcash.deserialize(f"e1:secret:{hex_secret}") + public_webhash = secret_webcash.to_public() + hash_to_webcash_and_depth[ + public_webhash.hashed_value + ] = (secret_webcash, wallet_depth) + wallet_depth += step_size + + health_check_request = [ + str(o[0].to_public()) for o in hash_to_webcash_and_depth.values() + ] + response = webcash_server_request( + WEBCASH_ENDPOINT_HEALTH_CHECK, health_check_request + ) + + assert len(hash_to_webcash_and_depth) == BATCH_SIZE + assert len(response["results"]) == BATCH_SIZE + + found_new = False + for public_webcash, result in response["results"].items(): + secret_webcash, found_at_wallet_depth = hash_to_webcash_and_depth[PublicWebcash.deserialize(public_webcash).hashed_value] + spent = result["spent"] + amount = result.get("amount") + token_is_unknown = spent is None and amount is None + if token_is_unknown: + unknown_count += 1 + continue + if found_at_wallet_depth > max_used_wallet_depth: + max_used_wallet_depth = found_at_wallet_depth + found_new = True + if found_new: + print(f"Scanning... Found used walled depth {chain_code}:{max_used_wallet_depth}") + resulting_wallet_depths[chain_code] = max_used_wallet_depth + 1 + print(f"\"walletdepths\": {resulting_wallet_depths}") + @cli.command("recover", short_help="Recover webcash using the wallet's master secret.") @click.option("--gaplimit", default=20) @lock_wallet