Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add find-wallet-depths command: quickly find walletdepths from master secret #34

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 80 additions & 12 deletions webcash/walletclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import secrets
import hashlib
import datetime
import string
import struct
import os
import sys
Expand All @@ -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 = {
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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]

Expand Down Expand Up @@ -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
Expand Down