This commit is contained in:
Peter D. Gray 2024-03-20 13:29:01 -04:00
parent 98f2341a4b
commit 3e7a3a879b
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
11 changed files with 204 additions and 35 deletions

View File

@ -94,3 +94,9 @@
- Enhancement: Move dice rolls (for generating master seed) to `Advanced` submenu.
- Cleanup reproducible building / start process of backporting to Mk4.
## 1.0.2Q - 2024-04-xx
- Enhancement: Scan any QR and report if it is part of a wallet this Coldcard knows
the key for. Includes Multisig and single sig wallets.

View File

@ -297,20 +297,14 @@ Press (3) if you really understand and accept these risks.
else:
# single-signer wallets
from wallet import MasterSingleSigWallet
main = MasterSingleSigWallet(addr_fmt, path, self.account_num)
with stash.SensitiveValues() as sv:
for idx in range(start, start + n):
deriv = path.format(account=self.account_num, change=change, idx=idx)
node = sv.derive_path(deriv, register=False)
addr = chain.address(node, addr_fmt)
addrs.append(addr)
msg += "%s =>\n%s\n\n" % (deriv, addr)
dis.progress_bar_show(idx/n)
stash.blank_object(node)
for (idx, addr, deriv) in main.yield_addresses(start, n,
change_idx=(change if allow_change else None)):
addrs.append(addr)
msg += "%s =>\n%s\n\n" % (deriv, addr)
dis.progress_bar_show(idx/n)
# export options
k0 = 'to show change addresses' if allow_change and change == 0 else None
@ -389,7 +383,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
'Redeem Script (%d of %d)' % (ms_wallet.M, ms_wallet.N)]
+ (['Derivation'] * ms_wallet.N)) + '"\n'
for (idx, derivs, addr, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
ln += '"\n'
@ -398,17 +392,13 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
return
# build the "master" wallet based on indicated preferences
from wallet import MasterSingleSigWallet
main = MasterSingleSigWallet(addr_fmt, path, account_num)
yield '"Index","Payment Address","Derivation"\n'
ch = chains.current_chain()
with stash.SensitiveValues() as sv:
for idx in range(start, start+n):
deriv = path.format(account=account_num, change=change, idx=idx)
node = sv.derive_path(deriv, register=False)
yield '%d,"%s","%s"\n' % (idx, ch.address(node, addr_fmt), deriv)
stash.blank_object(node)
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
yield '%d,"%s","%s"\n' % (idx, addr, deriv)
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
count=250, change=0, **save_opts):

View File

@ -200,7 +200,7 @@ class Descriptor:
def _serialize(self, internal=False, int_ext=False) -> str:
"""Serialize without checksum"""
assert len(self.keys) == 1, "Multiple keys for single signature script"
assert len(self.keys) == 1 # "Multiple keys for single signature script"
desc_base = SINGLE_FMT_TO_SCRIPT[self.addr_fmt]
inner = self.serialize_keys(internal=internal, int_ext=int_ext)[0]
return desc_base % (inner)

View File

@ -10,8 +10,8 @@ from ubinascii import b2a_base64, a2b_base64
from serializations import COutPoint, uint256_from_str
from glob import settings
# Very limited space in serial flash, so we compress as much as possible:
# - would be bad for privacy to store these **UTXO amounts** in plaintext
# Very limited space in flash, so we compress as much as possible:
# - would be very bad for privacy to store these **UTXO amounts** in plaintext
# - result is stored in a JSON serialization, so needs to be text encoded
# - using base64, in two parts, concatenated
# - 15 bytes are hash over txnhash:out_num => base64 => 20 chars text

View File

@ -49,6 +49,7 @@ freeze_as_mpy('', [
'version.py',
'xor_seed.py',
'tapsigner.py',
'wallet.py',
], opt=0)
# Optimize data-like files, since no need to debug them.

View File

@ -17,6 +17,7 @@ freeze_as_mpy('', [
'ux_q1.py',
'battery.py',
'notes.py',
'ownership.py',
], opt=0)
# Optimize data-like files, since no need to debug them.

View File

@ -14,6 +14,7 @@ from opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue
from glob import settings
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from wallet import WalletABC
# PSBT Xpub trust policies
TRUST_VERIFY = const(0)
@ -104,7 +105,7 @@ def make_redeem_script(M, nodes, subkey_idx):
return b''.join(pubkeys)
class MultisigWallet:
class MultisigWallet(WalletABC):
# Capture the info we need to store long-term in order to participate in a
# multisig wallet as a co-signer.
# - can be saved to nvram
@ -433,7 +434,7 @@ class MultisigWallet:
return set(xp_idx for xp_idx, (wxfp, _, _) in enumerate(self.xpubs)
if wxfp == xfp)
def yield_addresses(self, start_idx, count, change_idx=0):
def yield_addresses(self, start_idx, count, change_idx=0, censored=True):
# Assuming a suffix of /0/0 on the defined prefix's, yield
# possible deposit addresses for this wallet. Never show
# user the resulting addresses because we cannot be certain
@ -460,9 +461,12 @@ class MultisigWallet:
# make the redeem script, convert into address
script = make_redeem_script(self.M, nodes, idx)
addr = ch.p2sh_address(self.addr_fmt, script)
addr = addr[0:12] + '___' + addr[12+3:]
yield idx, [p.format(idx=idx) for p in paths], addr, script
if censored:
addr = addr[0:12] + '___' + addr[12+3:]
yield idx, addr, [p.format(idx=idx) for p in paths], addr
else:
# internal use
yield idx, addr, 'ms'
idx += 1
count -= 1

View File

@ -55,7 +55,9 @@ from utils import call_later_ms
# seedvault = (bool) opt-in enable seed vault feature
# seeds = list of stored secrets for seedvault feature
# bright = (int:0-255) LCD brightness when on battery
# notes = (complex) Secure notes held for user
# notes = (complex) Secure notes held for user, see notes.py
# MAYBE: own = (complex) Records about wallets we know about, see ownership.py
# paths = (list of tuples: (addr_fmt, deriv_path)) Single-sig wallets we've seen them use
# Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug)

62
shared/ownership.py Normal file
View File

@ -0,0 +1,62 @@
# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# ownership.py - store a cache of hashes related to addresses we might control.
#
import gc, chains, stash, ngu
from uhashlib import sha256
from ustruct import pack, unpack
from ubinascii import b2a_base64, a2b_base64
from glob import settings
from ucollections import namedtuple
from wallet import WalletABC
from ubinascii import hexlify as b2a_hex
# Track many addresses, but in compressed form
# - map from random Bech32/Base58 payment address to (wallet)/keypath
# - does change and normal (internal, external) addresses, but won't consider
# any keypath that does not end in 0/* or 1/*
# - store just hints, since we can re-construct any address and want to fully verify
# - try to keep private between different duress wallets, and seed vaults
# - storing bulk data into LFS, not settings
# - okay to wipe, can restore anytime; with CPU cost
# - MAYBE: tracks "high water level" of wallets (highest used addr)
# - MAYBE: enforces a gap limit concept, but would be better if it didn't
# - cannot be used to accelerate address explorer because we don't store full addresses
# - data stored in binary, fixed-length header, then fixed-length records
# - multisig and single sig, and someday taproot, miniscript too
# - searching is interruptable; and leaves behind a cache for next time
# - data building/saves happens when are searching, but might grab some during addr expl export?
#
REL_GAP_LIMIT = const(1000)
OwnershipFileHdr = namedtuple('OwnershipFileHdr', 'file_magic offset')
OWNERSHIP_FILE_HDR = 'II'
FILE_HDR_LEN = const(8)
OWNERSHIP_MAGIC = 0xA010 # "Address Ownership" v1.0
# length of hashed & truncated address record
HASH_ENC_LEN = const(8)
class OwnershipCache:
def wallet_to_fname(self, wallet: WalletABC):
# hash up something about the wallet to form a filename
desc = wallet.to_descriptor().serialize()
h = ngu.hash.sha256d(desc)
return b2a_hex(h)[0:32] + '.own'
def register(self, wallet:WalletABC):
# notes the details of a new wallet
# - won't build anything, so still fast
fn = self.wallet_to_fname(wallet)
def note_subkey(self, xfp, path, pubkey):
# whenever we see an in or out that is ours, note it here
pass
# singleton
OWNERSHIP = OwnershipCache()
# EOF

104
shared/wallet.py Normal file
View File

@ -0,0 +1,104 @@
# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# wallet.py - A place you find UTXO, addresses and descriptors.
#
import chains
from descriptor import Descriptor
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from stash import blank_object, SensitiveValues
class WalletABC:
# How to make this ABC useful without consuming memory/code space??
# - be more of an "interface" than a base class
# name
# addr_fmt
# chain
def yield_addresses(self, start_idx, count, change_idx=0, censored=True):
# TODO: expected tuples?
pass
def to_descriptor(self):
pass
class MasterSingleSigWallet(WalletABC):
# Refers to current master seed phrase, whichever is loaded
def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None):
# Construct a wallet based on current master secret, and chain.
# - path is optional, and then we use standard path for addr_fmt
# - path can be overriden when we come here via address explorer
if addr_fmt == AF_P2WPKH:
n = 'Segwit'
prefix = path or 'm/84h/{coin_type}h/{account}h'
elif addr_fmt == AF_CLASSIC:
n = 'Classic'
prefix = path or 'm/44h/{coin_type}h/{account}h'
elif addr_fmt == AF_P2WPKH_P2SH:
n = 'P2WPKH-in-P2SH'
prefix = path or 'm/49h/{coin_type}h/{account}h'
else:
raise ValueError(addr_fmt)
if chain_name:
self.chain = chains.get_chain(chain_name)
else:
self.chain = chains.current_chain()
if self.chain.ctype != 'BTC':
n += ' ' + self.chain.menu_name
self.name = n
self.addr_fmt = addr_fmt
# Figure out the derivation path
# - we want to store path w/o change and index part
p = prefix.format(account=account_idx, coin_type=self.chain.b44_cointype,
change='C', idx='I')
if p.endswith('/C/I'):
p = p[:-4]
if p.endswith('/I'):
# custom path in addr explorer can get this
p = p[:-2]
self._path = p
def yield_addresses(self, start_idx, count, change_idx=None, censored=True):
# Render a range of addresses. Slow to start, since accesses SE in general
# - if count==1, don't derive any subkey, just do path.
path = self._path
if change_idx is not None:
assert 0 <= change_idx <= 1
path += '/%d' % change_idx
with SensitiveValues() as sv:
node = sv.derive_path(path)
if count == 1:
address = self.chain.address(node, self.addr_fmt)
yield 0, address, path
return
path += '/'
for idx in range(start_idx, start_idx+count):
try:
here = node.copy()
here.derive(idx, False) # works in-place
address = self.chain.address(here, self.addr_fmt)
finally:
here.blank()
del here
yield idx, address, path+str(idx)
def to_descriptor(self):
from glob import settings
xfp = settings.get('xfp')
xpub = settings.get('xpub')
keys = (xfp, self._path, xpub)
return Descriptor([keys], self.addr_fmt)
# EOF

View File

@ -16,8 +16,7 @@ BOOTLOADER_DIR = q1-bootloader
LATEST_RELEASE = $(shell ls -t1 ../releases/*-q1-*.dfu | head -1)
# Our version for this release.
# - caution, some bootroms (Q < 1.0.3) will not accept version < 3.0.0 (but that never shipped)
VERSION_STRING = 1.0.1Q
VERSION_STRING = 1.0.2Q
# Remove this closer to shipping.
#$(warning "Forcing debug build")