search also the change addresses

This commit is contained in:
Peter D. Gray 2024-03-25 14:21:56 -04:00
parent 7380a8b163
commit 2aecce6161
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
8 changed files with 80 additions and 62 deletions

View File

@ -382,6 +382,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
# Produce CSV file contents as a generator
# - maybe cache internally
from ownership import OWNERSHIP
from utils import censor_address
if ms_wallet:
# For multisig, include redeem script and derivation for each signer
@ -389,8 +390,8 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
) + '"\n'
if n > 100 and change == 0:
saver = OWNERSHIP.saver(ms_wallet, start)
if n > 100 and change in (0, 1):
saver = OWNERSHIP.saver(ms_wallet, change, start)
else:
saver = None
@ -398,7 +399,8 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
if saver:
saver(addr)
# XXX censor_address
# policy choice: never provide a complete multisig address to user.
addr = censor_address(addr)
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
@ -407,7 +409,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
yield ln
if saver:
saver(None) # close
saver(None) # close file
return
@ -415,8 +417,8 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
from wallet import MasterSingleSigWallet
main = MasterSingleSigWallet(addr_fmt, path, account_num)
if n > 100 and change == 0:
saver = OWNERSHIP.saver(main, start)
if n > 100 and change in (0, 1):
saver = OWNERSHIP.saver(main, change, start)
else:
saver = None

View File

@ -261,9 +261,10 @@ class ChainsBase:
def possible_address_fmt(cls, addr):
# Given a text (serialized) address, return what
# address format applies to the address, but
# for AF_P2SH case, could be AF_P2WPKH_P2SH, AF_P2WSH_P2SH.
# for AF_P2SH case, could be: AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH. .. we don't know
if addr.startswith(cls.bech32_hrp):
if addr.startswith(cls.bech32_hrp+'1p'):
# really any ver=1 script or address, but for now...
return AF_P2TR
else:
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH

View File

@ -149,6 +149,7 @@ class Display:
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".
# - 'line2' not supported on smaller screen sizes, ignore
self.clear()
y = 14
self.text(None, y, msg, font=FontLarge)

View File

@ -6,7 +6,7 @@ import machine, uzlib, utime, array
from uasyncio import sleep_ms
from graphics_q1 import Graphics
from st7788 import ST7788
from utils import xfp2str
from utils import xfp2str, word_wrap
from ucollections import namedtuple
# the one font: fixed-width (except for a few double-width chars)
@ -483,11 +483,12 @@ class Display:
# show a simple message "fullscreen".
self.clear()
y = CHARS_H // 3
self.text(None, y, msg)
if line2:
x = self.text(None, y, msg)
self.text(x-self.width(msg), y+2, line2, dark=True)
else:
self.text(None, y, msg)
y += 2
for ln in word_wrap(line2, CHARS_W):
self.text(None, y, ln, dark=True)
y += 1
if percent is not None:
self.progress_bar(percent)
self.show()
@ -586,7 +587,6 @@ class Display:
def show_yikes(self, lines):
# dump a stack trace
# - intended for photos, sent to support!
from utils import word_wrap
self.clear()
self.text(None, 0, '>>>> Yikes!! <<<<')
@ -634,7 +634,6 @@ class Display:
# Show a QR code on screen w/ some text under it
# - invert not supported on Q1
# - sidebar not supported here (see users.py)
from utils import word_wrap
# maybe show something other than QR contents under it
msg = sidebar or msg

View File

@ -36,13 +36,6 @@ def disassemble_multisig_mn(redeem_script):
return M, N
def censor_address(addr):
# We don't like to show the
# user multisig addresses because we cannot be certain
# they are valid and could be signed. And yet, dont blank too many
# spots or else an attacker could grind out a suitable replacement.
return addr[0:12] + '___' + addr[12+3:]
def disassemble_multisig(redeem_script):
# Take apart a standard multisig's redeem/witness script, and return M/N and public keys
# - only for multisig scripts, not general purpose

View File

@ -31,7 +31,7 @@ from exceptions import UnknownAddressExplained
HASH_ENC_LEN = const(2)
# File header
OwnershipFileHdr = namedtuple('OwnershipFileHdr', 'file_magic future flags')
OwnershipFileHdr = namedtuple('OwnershipFileHdr', 'file_magic change_idx flags')
OWNERSHIP_FILE_HDR = 'HHI'
OWNERSHIP_FILE_HDR_LEN = 8
@ -48,17 +48,24 @@ def encode_addr(addr, salt):
class AddressCacheFile:
def __init__(self, wallet):
def __init__(self, wallet, change_idx):
self.wallet = wallet
self.desc = wallet.to_descriptor().serialize()
h = b2a_hex(ngu.hash.sha256d(wallet.chain.ctype + self.desc))
self.fname = h[0:32] + '.own'
self.change_idx = change_idx
desc = wallet.to_descriptor().serialize()
h = b2a_hex(ngu.hash.sha256d(wallet.chain.ctype + desc))
self.fname = h[0:32] + '-%d.own' % change_idx
self.salt = h[32:]
self.count = 0
self.hdr = None
self.peek()
def nice_name(self):
rv = self.wallet.name
if self.change_idx:
rv += ' (change)'
return rv
def exists(self):
return bool(self.count)
@ -71,6 +78,7 @@ class AddressCacheFile:
flen = fd.seek(0, 2)
self.hdr = OwnershipFileHdr(*struct.unpack(OWNERSHIP_FILE_HDR, hdr))
assert self.hdr.file_magic == OWNERSHIP_MAGIC
assert self.hdr.change_idx == self.change_idx
except OSError:
return
except Exception as exc:
@ -81,7 +89,9 @@ class AddressCacheFile:
self.count = (flen - OWNERSHIP_FILE_HDR_LEN) // HASH_ENC_LEN
def setup(self, start_idx):
def setup(self, change_idx, start_idx):
assert self.change_idx == change_idx
if self.count or self.hdr:
assert start_idx == self.count, 'not an append'
@ -90,7 +100,7 @@ class AddressCacheFile:
else:
# Start new file
self.fd = open(self.fname, 'wb')
self.hdr = OwnershipFileHdr(OWNERSHIP_MAGIC, 0x0, 0x0)
self.hdr = OwnershipFileHdr(OWNERSHIP_MAGIC, self.change_idx, 0x0)
hdr = struct.pack(OWNERSHIP_FILE_HDR, *self.hdr)
self.fd.write(hdr)
@ -122,19 +132,19 @@ class AddressCacheFile:
chk = encode_addr(addr, self.salt)
for idx in range(self.count):
if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk:
yield (0, idx)
yield (self.change_idx, idx)
dis.progress_sofar(idx, self.count)
def check_match(self, want_addr, subpath):
# need to double-check matches, to get rid of false positives.
chg, idx = subpath
got = self.wallet.render_address(*subpath)
chg, idx = subpath
#print('(%d, %d) => %s ?= %s' % (chg, idx, got, want_addr))
return want_addr == got
def rebuild(self, addr):
# build more addresses
# - maybe wipe incomplete stuff from csv export hack
def build_and_search(self, addr):
# build many more addresses
# - return subpath for a hit or None
from glob import dis
@ -147,14 +157,14 @@ class AddressCacheFile:
if count <= 0:
return None
self.setup(start_idx)
self.setup(self.change_idx, start_idx)
for idx,here,*_ in self.wallet.yield_addresses(
start_idx, count, change_idx=0, censored=False):
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
change_idx=self.change_idx):
if here == addr:
# Found it! But keep going a little for next time.
match = (0, idx)
match = (self.change_idx, idx)
self.append(here)
self.count += 1
@ -174,11 +184,11 @@ class AddressCacheFile:
class OwnershipCache:
@classmethod
def saver(cls, wallet, start_idx):
def saver(cls, wallet, change_idx, start_idx):
# when we are generating many addresses for export, capture them
# as we go with this function
# - not change -- only main addrs
file = AddressCacheFile(wallet)
file = AddressCacheFile(wallet, change_idx)
if file.exists():
# don't save to existing file, has some already
@ -199,7 +209,7 @@ class OwnershipCache:
# - if you start w/ testnet, we'll follow that
from chains import current_chain
from multisig import MultisigWallet
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
from glob import dis
ch = current_chain()
@ -220,8 +230,11 @@ class OwnershipCache:
if addr_fmt & AFC_SCRIPT:
# multisig or script at least.. must exist already
for w in MultisigWallet.iter_wallets(addr_fmt=addr_fmt):
possibles.append(w)
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
if addr_fmt == AF_P2SH:
# might look like P2SH but actually be AF_P2WSH_P2SH
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH))
# TODO: add tapscript and such fancy stuff here
@ -247,32 +260,34 @@ class OwnershipCache:
count = 0
phase2 = []
files = [AddressCacheFile(w) for w in possibles]
for f in files:
dis.fullscreen('Searching wallet(s)...', line2=f.wallet.name)
for change_idx in (0, 1):
files = [AddressCacheFile(w, change_idx) for w in possibles]
for f in files:
dis.fullscreen('Searching wallet(s)...', line2=f.nice_name())
for maybe in f.fast_search(addr):
ok = f.check_match(addr, maybe)
if not ok: continue
for maybe in f.fast_search(addr):
ok = f.check_match(addr, maybe)
if not ok: continue
# found winner.
return f.wallet, maybe
# found winner.
return f.wallet, maybe
if f.count < MAX_ADDRS_STORED:
phase2.append(f)
count += f.count
if f.count < MAX_ADDRS_STORED:
phase2.append(f)
count += f.count
# maybe we haven't rendered all the addresses yet, so do that
# - very slow, but only needed once
# - might stop when match found, or maybe go a bit beyond that?
# - MAYBE NOT: search all in parallel, rather than serially because
# more likely to find a match with low index
# - we could search all in parallel, rather than serially because
# more likely to find a match with low index... but seen as too much memory
for f in phase2:
b4 = f.count
dis.fullscreen("Generating addresses...", line2=f.wallet.name)
dis.fullscreen("Generating addresses...", line2=f.nice_name())
result = f.rebuild(addr)
result = f.build_and_search(addr)
if result:
# found it, so report it and stop
return f.wallet, result

View File

@ -683,4 +683,10 @@ def decode_bip21_text(got):
raise ValueError('not bip-21')
def censor_address(addr):
# We don't like to show the user multisig addresses because we cannot be certain
# they are valid and could actually be signed. And yet, dont blank too many
# spots or else an attacker could grind out a suitable replacement.
return addr[0:12] + '___' + addr[12+3:]
# EOF

View File

@ -16,11 +16,12 @@ class WalletABC:
# chain
def yield_addresses(self, start_idx, count, change_idx=0):
# TODO: returns various expected tuples?
# TODO: returns various tuples, with at least (idx, address, ...)
pass
def render_address(self, change_idx, idx):
# make one single address
# make one single address as text.
tmp = list(self.yield_addresses(idx, 1, change_idx=change_idx))
assert len(tmp) == 1
@ -56,12 +57,12 @@ class MasterSingleSigWallet(WalletABC):
self.chain = chains.current_chain()
if account_idx != 0:
n += ' Acct#%d' % account_idx
n += ' Account#%d' % account_idx
if self.chain.ctype == 'XTN':
n += ' (TestNet)'
n += ' (Testnet)'
if self.chain.ctype == 'XRT':
n += ' (RegTest)'
n += ' (Regtest)'
self.name = n