diff --git a/shared/address_explorer.py b/shared/address_explorer.py index 761a439b..a29cde53 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -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 diff --git a/shared/chains.py b/shared/chains.py index 7f98d7e5..86a9fbf7 100644 --- a/shared/chains.py +++ b/shared/chains.py @@ -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 diff --git a/shared/display.py b/shared/display.py index 6870f0d3..1e4c3cd9 100644 --- a/shared/display.py +++ b/shared/display.py @@ -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) diff --git a/shared/lcd_display.py b/shared/lcd_display.py index 75faa341..6514bfec 100644 --- a/shared/lcd_display.py +++ b/shared/lcd_display.py @@ -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 diff --git a/shared/multisig.py b/shared/multisig.py index ae54c6d7..ee019956 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -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 diff --git a/shared/ownership.py b/shared/ownership.py index 3fb68b48..8eab80de 100644 --- a/shared/ownership.py +++ b/shared/ownership.py @@ -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 diff --git a/shared/utils.py b/shared/utils.py index 10641eff..bb419059 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -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 diff --git a/shared/wallet.py b/shared/wallet.py index 449dc361..18261b35 100644 --- a/shared/wallet.py +++ b/shared/wallet.py @@ -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