start
This commit is contained in:
parent
98f2341a4b
commit
3e7a3a879b
@ -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.
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
62
shared/ownership.py
Normal 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
104
shared/wallet.py
Normal 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
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user