search also the change addresses
This commit is contained in:
parent
7380a8b163
commit
2aecce6161
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user