Multisig PSBT support
This commit is contained in:
parent
6fd2ef619e
commit
39adb2ac41
@ -44,8 +44,7 @@ The first byte encodes what the package contents (under all the encryption).
|
||||
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
|
||||
- `n` - one or many notes export (JSON array)
|
||||
- `v` - seed vault export (JSON: one secret key but includes name, source of key)
|
||||
- `p` - binary PSBT to be signed
|
||||
- `P` - a more-signed binary PSBT being returned back to sender
|
||||
- `p` - binary PSBT to be signed, perhaps multisig but not required.
|
||||
|
||||
## QR details
|
||||
|
||||
@ -58,30 +57,38 @@ New type codes for BBQr are defined for the purposes of this application:
|
||||
|
||||
- `R` contains `(pubkey)` ... begins the process from receiver; compressed pubkey is 33 bytes
|
||||
- `S` contains `(pubkey)(data)` ... data from sender; first 33 bytes are sender's pubkey
|
||||
- `E` for PSBT: `(randint)(data)` ... randint (4 bytes) indicates which randomly
|
||||
selected derived subkey from pre-shared xpub associated with receiver
|
||||
- `E` for Multisig PSBT: `(randint)(data)` ... randint (4 byte nonce) indicates which
|
||||
derived subkey from pre-shared xpub associated with receiver
|
||||
|
||||
All the data is encrypted with the exception of the pubkey or randint. Keep in mind
|
||||
those are both nonce values picked uniquely for each transfer.
|
||||
|
||||
### PSBT Key Picking
|
||||
### PSBT Key Selection
|
||||
|
||||
When sending PSBT data, the keys involved are picked at random by the sender in range:
|
||||
5000..(2^30).
|
||||
When sending PSBT data, a nonce is picked at random by the sender
|
||||
in range: `0..(2^28)`
|
||||
|
||||
This is called `randint`. The receiver's pubkey will be
|
||||
This nonce is called `randint`. The receiver's pubkey will be
|
||||
|
||||
.../20250317/(randint)
|
||||
|
||||
where `...` is the derivation used in the multisig setup for the co-signer who will
|
||||
receive the package. The sender's keypair is implied by:
|
||||
|
||||
.../20250318/(randint)
|
||||
where `...` is the derivation used in the multisig wallet for the co-signer who will
|
||||
receive the package. The sender's keypair has the same sub key path assuming all
|
||||
co-signers have same derivation path from root (not required).
|
||||
|
||||
Because both the sender and receiver already have each other's XPUB they can derive
|
||||
the appropriate pubkeys (and privkey for their side) without communicating
|
||||
more than `randint`. The sending COLDCARD will pick a new random value each time.
|
||||
|
||||
When receiving a multisig PSBT encrypted this way, the receiver does not need
|
||||
to any setup (nor numeric password) and can receive a QR code at any time.
|
||||
This works because the shared multisig wallet is already setup. Receiver will
|
||||
take the nonce value (randint) and seach all pre-defined multisig wallets for
|
||||
any pubkey that can decrypt the package successfully (based on checksum inside
|
||||
first layer of ECDH encryption).
|
||||
|
||||
The next layer of encryption (paranoid password) is unchanged.
|
||||
|
||||
## Encryption Details
|
||||
|
||||
AES-256-CTR is used exclusively. Session key is picked via ECDH with final
|
||||
@ -175,7 +182,33 @@ and the site will be served over SSL.
|
||||
to send, from secure notes to seeds and so on.
|
||||
|
||||
- For PSBT multisig, user must pick a single co-signer (who hasn't already
|
||||
signed) and the QR is prepared for that receiver. They should get another
|
||||
chance to do the same for the other possible co-signers.
|
||||
signed) and the QR is prepared for that receiver. Because we
|
||||
cannot do arbitary conbining, it's best if the next signer continues
|
||||
to teleport the updated PSBT to further signers. In other words,
|
||||
a daisy-chain pattern is prefered to a star patter. The signer
|
||||
who completes the Mth (of N) signature will be able to finalize
|
||||
the transaction, and ideally with PushTx feature, broadcast it.
|
||||
|
||||
# Security Comments
|
||||
|
||||
## Such short passwords?
|
||||
|
||||
We are using 8-character passwords because we want them to be
|
||||
practical to share over non-digital channels such as a voice phone
|
||||
call, or hand-written note.
|
||||
|
||||
It is important to remind users that the passwords should be sent
|
||||
by a different channel from the QR itself. Best is to call up your
|
||||
other party and say the letters to them directly.
|
||||
|
||||
## Is it safe to save image of QR to cloud?
|
||||
|
||||
Yes, this seems safe. Of course, if you can control it, perhaps not
|
||||
a risk to accept... but the QR is encrypted via ECDH using a key
|
||||
that is forgotten after the transfer, so forward privacy is protected.
|
||||
Also your cloud service (or photo roll, chat app log, etc) will not
|
||||
have the 8-character password which is also required unpack the secrets.
|
||||
|
||||
The QR codes themselves are fully random and do not reveal the
|
||||
identity of your COLDCARD, your on chain funds or anything linked
|
||||
to you.
|
||||
|
||||
@ -1657,13 +1657,14 @@ async def list_files(*A):
|
||||
|
||||
async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
||||
choices=None, none_msg=None, force_vdisk=False, slot_b=None,
|
||||
allow_batch_sign=False, ux=True):
|
||||
allow_batch=False, ux=True):
|
||||
# present a menu w/ a list of files... to be read
|
||||
# - optionally, enforce a max size, and provide a "tasting" function
|
||||
# - if msg==None, don't prompt, just do the search and return list
|
||||
# - if (not ux), don't prompt, just do the search and return list
|
||||
# - if choices is provided; skip search process
|
||||
# - escape: allow these chars to skip picking process
|
||||
# - slot_b: None=>pick slot w/ card in it, or A if both.
|
||||
# - allow_batch: adds an "all of the above" choice: ("menu label", menu_handler)
|
||||
|
||||
if choices is None:
|
||||
choices = []
|
||||
@ -1707,7 +1708,7 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
||||
label = fn
|
||||
while label in sofar:
|
||||
# just the file name isn't unique enough sometimes?
|
||||
# - shouldn't happen anymore now that we dno't support internal FS
|
||||
# - shouldn't happen anymore now that we don't support internal FS
|
||||
# - unless we do muliple paths
|
||||
label += path.split('/')[-1] + '/' + fn
|
||||
|
||||
@ -1744,10 +1745,10 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
||||
choices.sort()
|
||||
|
||||
items = [MenuItem(label, f=clicked, arg=(path, fn)) for label, path, fn in choices]
|
||||
if allow_batch_sign and len(choices) > 1:
|
||||
# we know that each choices member is psbt as allow_batch_sign is only True
|
||||
# in Ready To Sign
|
||||
items.insert(0, MenuItem("[Sign All]", f=batch_sign, arg=choices))
|
||||
if allow_batch and len(choices) > 1:
|
||||
# Allow an "all" selection
|
||||
label, funct = allow_batch
|
||||
items.insert(0, MenuItem(label, f=funct, arg=choices))
|
||||
|
||||
menu = MenuSystem(items)
|
||||
the_ux.push(menu)
|
||||
@ -1895,7 +1896,7 @@ from your desktop wallet software or command line tools.\n\n'''
|
||||
input_psbt = path + '/' + fn
|
||||
else:
|
||||
# multiples - ask which, and offer batch to sign them all
|
||||
input_psbt = await file_picker(choices=choices, allow_batch_sign=True)
|
||||
input_psbt = await file_picker(choices=choices, allow_batch=("[Sign All]", batch_sign))
|
||||
if not input_psbt:
|
||||
return
|
||||
|
||||
|
||||
@ -1040,6 +1040,12 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
continue
|
||||
break
|
||||
|
||||
elif version.has_qwerty and self.psbt.active_multisig:
|
||||
# Offer to teleport the result
|
||||
from teleport import kt_send_psbt
|
||||
|
||||
await kt_send_psbt(self.psbt, self.result[0], post_signing=True)
|
||||
|
||||
async def txn_explorer(self):
|
||||
# Page through unlimited-sized transaction details
|
||||
# - shows all outputs (including change): their address and amounts.
|
||||
@ -1250,15 +1256,19 @@ def psbt_encoding_taster(taste, psbt_len):
|
||||
|
||||
async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False,
|
||||
output_encoder=None, slot_b=False, finalize=None):
|
||||
# User authorized PSBT for signing, and we added signatures.
|
||||
# - allow PushTX if enabled (first thing)
|
||||
# - can save final TXN out to SD card/VirtDisk, share by NFC, QR.
|
||||
|
||||
from glob import dis, PSRAM
|
||||
from files import CardSlot, CardMissingError
|
||||
from sffile import SFFile
|
||||
from ux import show_qr_code, import_export_prompt, ux_show_story
|
||||
|
||||
txid = None
|
||||
is_complete = psbt.is_complete()
|
||||
|
||||
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
|
||||
is_complete = psbt.is_complete()
|
||||
if is_complete:
|
||||
txid = psbt.finalize(psram)
|
||||
else:
|
||||
@ -1439,10 +1449,11 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False
|
||||
n += 1
|
||||
|
||||
|
||||
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, abort=False):
|
||||
|
||||
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False):
|
||||
# sign a PSBT file found on a MicroSD card
|
||||
# - or from VirtualDisk (mk4)
|
||||
from files import CardSlot
|
||||
# - to re-use reading/decoding logic, pass just_read
|
||||
from glob import dis
|
||||
from ux import the_ux
|
||||
|
||||
@ -1490,6 +1501,9 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, abort=False):
|
||||
assert total <= psbt_len
|
||||
psbt_len = total
|
||||
|
||||
if just_read:
|
||||
return psbt_len
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||
psbt_len,
|
||||
@ -1498,7 +1512,7 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, abort=False):
|
||||
"force_vdisk": force_vdisk,
|
||||
"output_encoder": output_encoder}
|
||||
)
|
||||
if abort:
|
||||
if ux_abort:
|
||||
# needed for auto vdisk mode
|
||||
abort_and_push(UserAuthorizedAction.active_request)
|
||||
else:
|
||||
|
||||
@ -39,13 +39,14 @@ if version.has_battery:
|
||||
from battery import battery_idle_timeout_chooser, brightness_chooser
|
||||
from q1 import scan_and_bag
|
||||
from notes import make_notes_menu
|
||||
from teleport import kt_start_rx
|
||||
from teleport import kt_start_rx, kt_send_file_psbt
|
||||
else:
|
||||
battery_idle_timeout_chooser = None
|
||||
brightness_chooser = None
|
||||
scan_and_bag = None
|
||||
make_notes_menu = None
|
||||
kt_start_rx = None
|
||||
kt_send_file_psbt = None
|
||||
|
||||
|
||||
#
|
||||
@ -215,6 +216,7 @@ FileMgmtMenu = [
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
|
||||
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
||||
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
|
||||
MenuItem('Teleport Multisig PSBT', predicate=version.has_qr, f=kt_send_file_psbt),
|
||||
MenuItem('List Files', f=list_files),
|
||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
||||
|
||||
@ -22,6 +22,9 @@ TRUST_VERIFY = const(0)
|
||||
TRUST_OFFER = const(1)
|
||||
TRUST_PSBT = const(2)
|
||||
|
||||
# Arbitrary value, not 0 or 1, used to derive a pubkey from preshared xpub in Key Teleport
|
||||
KT_RXPUBKEY_DERIV = const(20250317)
|
||||
|
||||
class MultisigOutOfSpace(RuntimeError):
|
||||
pass
|
||||
|
||||
@ -468,6 +471,10 @@ class MultisigWallet(WalletABC):
|
||||
return set(xp_idx for xp_idx, (wxfp, _, _) in enumerate(self.xpubs)
|
||||
if wxfp == xfp)
|
||||
|
||||
def xpubs_from_xfp(self, xfp):
|
||||
# return list of XPUB's which match xfp; typically one.
|
||||
return [xpub for (wxfp, _, xpub) in self.xpubs if wxfp == xfp]
|
||||
|
||||
def yield_addresses(self, start_idx, count, change_idx=0):
|
||||
# Assuming a suffix of /0/0 on the defined prefix's, yield
|
||||
# possible deposit addresses for this wallet.
|
||||
@ -1180,6 +1187,84 @@ Press (1) to see extended public keys, '''.format(M=M, N=N, name=self.name, exp=
|
||||
|
||||
return await ux_show_story(msg, title=self.name)
|
||||
|
||||
# Key Teleport support, where a co-signers pubkeys are used for ECDH
|
||||
|
||||
def kt_make_rxkey(self, xfp):
|
||||
# Derive the receiver's pubkey from preshared xpub and a special derivation
|
||||
# - also provide the keypair we're using from our side of connection
|
||||
# - returns 4 byte nonce which is sent un-encrypted, his_pubkey and my_keypair
|
||||
ri = ngu.random.uniform(1<<28)
|
||||
try:
|
||||
xpub, = self.xpubs_from_xfp(xfp)
|
||||
except ValueError:
|
||||
raise RuntimeError("dup or missing xfp")
|
||||
|
||||
node = self.chain.deserialize_node(xpub, AF_P2SH)
|
||||
node.derive(KT_RXPUBKEY_DERIV, False)
|
||||
node.derive(ri, False)
|
||||
pubkey = node.pubkey()
|
||||
|
||||
kp = self.kt_my_keypair(ri)
|
||||
|
||||
return ri.to_bytes(4, 'big'), pubkey, kp
|
||||
|
||||
def kt_my_keypair(self, ri):
|
||||
# Calc my keypair for sending PSBT files.
|
||||
#
|
||||
|
||||
my_xfp = settings.get('xfp')
|
||||
|
||||
# Find the derivation path used by my leg of this multisig
|
||||
deriv = list(self.xfp_paths[my_xfp])
|
||||
deriv.append(KT_RXPUBKEY_DERIV)
|
||||
deriv.append(ri)
|
||||
|
||||
path = keypath_to_str(deriv)
|
||||
|
||||
with stash.SensitiveValues() as sv:
|
||||
node = sv.derive_path(path)
|
||||
|
||||
kp = ngu.secp256k1.keypair(node.privkey())
|
||||
|
||||
return kp
|
||||
|
||||
@classmethod
|
||||
def kt_search_rxkey(cls, payload):
|
||||
# Construct the keypair for to be decryption
|
||||
# - has to try pubkey each all the unique XFP for all co-signers in all wallets
|
||||
# - checks checksum of ECDH unwrapped data to see if it's the right one
|
||||
# - returns session key, decrypted first layer, and XFP of sender
|
||||
from teleport import decode_step1
|
||||
|
||||
# this nonce is part of the derivation path so each txn gets new keys
|
||||
ri = int.from_bytes(payload[0:4], 'big')
|
||||
|
||||
my_xfp = settings.get('xfp')
|
||||
|
||||
kp = None
|
||||
for ms in cls.iter_wallets():
|
||||
if (not kp) or (kp_deriv != ms.xfp_paths[my_xfp]):
|
||||
# my keypair is cachable if path the same in 2nd MS wallet and so on
|
||||
kp = ms.kt_my_keypair(ri)
|
||||
kp_deriv = ms.xfp_paths[my_xfp]
|
||||
|
||||
for xfp, deriv, xpub in ms.xpubs:
|
||||
if xfp == my_xfp: continue
|
||||
|
||||
node = ms.chain.deserialize_node(xpub, AF_P2SH)
|
||||
node.derive(KT_RXPUBKEY_DERIV, False)
|
||||
node.derive(ri, False)
|
||||
|
||||
his_pubkey = node.pubkey()
|
||||
|
||||
# if implied session key decodes the checksum, it is right
|
||||
ses_key, body = decode_step1(kp, his_pubkey, payload[4:])
|
||||
|
||||
if ses_key:
|
||||
return ses_key, body, xfp
|
||||
|
||||
return None, None, None
|
||||
|
||||
async def no_ms_yet(*a):
|
||||
# action for 'no wallets yet' menu item
|
||||
await ux_show_story("You don't have any multisig wallets yet.")
|
||||
|
||||
@ -2232,6 +2232,19 @@ class psbtObject(psbtProxy):
|
||||
sigs = sigs[:self.active_multisig.M]
|
||||
return sigs
|
||||
|
||||
def multisig_xfps_needed(self):
|
||||
# provide the set of xfp's that still need to sign PSBT
|
||||
# - used to find which multisig-signer needs to go next
|
||||
rv = set()
|
||||
for inp in self.inputs:
|
||||
for pk, pth in inp.subpaths.items():
|
||||
if pk in inp.part_sigs:
|
||||
continue
|
||||
if pk in inp.added_sigs:
|
||||
continue
|
||||
rv.add(pth[0])
|
||||
return rv
|
||||
|
||||
def finalize(self, fd):
|
||||
# Stream out the finalized transaction, with signatures applied
|
||||
# - assumption is it's complete already.
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
# secure environment of two Q's.
|
||||
#
|
||||
import sys, uzlib, ngu, aes256ctr, bip39, json, stash
|
||||
from utils import problem_file_line, B2A, xfp2str, deserialize_secret
|
||||
from utils import problem_file_line, B2A, xfp2str, deserialize_secret, keypath_to_str
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from glob import settings
|
||||
@ -14,21 +14,17 @@ from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
from bbqr import b32encode, b32decode
|
||||
from menu import MenuItem, MenuSystem
|
||||
from notes import NoteContentBase
|
||||
from sffile import SFFile
|
||||
from multisig import MultisigWallet
|
||||
|
||||
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
|
||||
KT_DOMAIN = 'keyteleport.com'
|
||||
|
||||
'''
|
||||
- `w` - 12/18/24 words - 16/24/32 bytes follow
|
||||
- `m` - (one byte of length) + (up to 71 bytes) - BIP-32 raw master secret [rare]
|
||||
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
|
||||
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
|
||||
- `n` - secure note or password (JSON)
|
||||
- `e` - full notes export (JSON array)
|
||||
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
|
||||
- `p` - binary PSBT to be signed
|
||||
- `P` - a more-signed binary PSBT being returned back to sender
|
||||
'''
|
||||
|
||||
# No length/size worries with simple secrets, but massive notes and big PSBT,
|
||||
# with lots of UTXO, cannot be passed via NFC URL, because we are limited by
|
||||
# NFC chip (8k) and URL length (4k or less) inside. BBQr is not limited however.
|
||||
# - but the website is ready to make animated BBQr nicely
|
||||
NFC_SIZE_LIMIT = const(4096)
|
||||
|
||||
def short_bbqr(type_code, data):
|
||||
# Short-circuit basic BBQr encoding here: always Base32, single part: 1 of 1
|
||||
@ -134,7 +130,7 @@ async def tk_show_payload(type_code, payload, title, msg, cta=None):
|
||||
from ux_q1 import show_bbqr_codes
|
||||
|
||||
hints = KEY_QR
|
||||
if NFC:
|
||||
if NFC and len(payload) < NFC_SIZE_LIMIT:
|
||||
hints += KEY_NFC
|
||||
msg += ' or %s to view on your phone' % KEY_NFC
|
||||
|
||||
@ -147,7 +143,7 @@ async def tk_show_payload(type_code, payload, title, msg, cta=None):
|
||||
if ch == KEY_NFC and NFC:
|
||||
await nfc_push_kt(short_bbqr(type_code, payload))
|
||||
elif ch == KEY_QR or ch == 'y':
|
||||
# NOTE: CTA rarely seen, but maybe?
|
||||
# NOTE: CTA rarely seen, but maybe sometimes?
|
||||
await show_bbqr_codes(type_code, payload, msg=cta)
|
||||
elif ch == 'x':
|
||||
return
|
||||
@ -178,8 +174,8 @@ WARNING: Receiver will have full access to all Bitcoin controlled by these keys!
|
||||
menu = SecretPickerMenu(rx_pubkey)
|
||||
the_ux.push(menu)
|
||||
|
||||
async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None):
|
||||
# Example: cleartext = b'w'+ (b'A'*16)
|
||||
async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix='', rx_label='the receiver', kp=None):
|
||||
# We are rendering a QR and showing it to them for sending to another Q
|
||||
from glob import dis
|
||||
cleartext = dtype.encode() + (raw or json.dumps(obj).encode())
|
||||
|
||||
@ -189,22 +185,27 @@ async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None):
|
||||
dis.progress_bar_show(0.25)
|
||||
|
||||
# all new EC key
|
||||
my_keypair = ngu.secp256k1.keypair()
|
||||
my_keypair = kp or ngu.secp256k1.keypair()
|
||||
|
||||
dis.progress_bar_show(0.75)
|
||||
|
||||
payload = encode_payload(my_keypair, rx_pubkey, noid_key, cleartext)
|
||||
payload = prefix + encode_payload(my_keypair, rx_pubkey, noid_key, cleartext,
|
||||
for_psbt=bool(prefix))
|
||||
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
msg = "Share this password with the receiver, via some different channel:"\
|
||||
"\n\n %s = %s\n\n" % (txt, ' '.join(txt))
|
||||
msg = "Share this password with %s, via some different channel:"\
|
||||
"\n\n %s = %s\n\n" % (rx_label, txt, ' '.join(txt))
|
||||
msg += "ENTER to view QR"
|
||||
|
||||
await tk_show_payload('S', payload, 'Teleport Password', msg, cta='Show to Receiver')
|
||||
print(msg)
|
||||
await tk_show_payload('S' if not prefix else 'E',
|
||||
payload, 'Teleport Password', msg, cta='Show to Receiver')
|
||||
|
||||
from flow import goto_top_menu
|
||||
goto_top_menu()
|
||||
if not prefix:
|
||||
# not PSBT case ... reset menus, we are deep!
|
||||
from flow import goto_top_menu
|
||||
goto_top_menu()
|
||||
|
||||
def pick_noid_key():
|
||||
# pick an 40 bit password, shown as base32
|
||||
@ -218,6 +219,8 @@ def pick_noid_key():
|
||||
async def kt_decode_rx(is_psbt, payload):
|
||||
# we are getting data back from a sender, decode it.
|
||||
|
||||
prompt = 'Teleport Password (text)'
|
||||
|
||||
if not is_psbt:
|
||||
rx_key = settings.get("ktrx")
|
||||
if not rx_key:
|
||||
@ -230,28 +233,32 @@ async def kt_decode_rx(is_psbt, payload):
|
||||
body = payload[33:]
|
||||
pair = ngu.secp256k1.keypair(a2b_hex(rx_key))
|
||||
|
||||
ses_key, body = decode_step1(pair, his_pubkey, body)
|
||||
else:
|
||||
randint = payload[0:4]
|
||||
body = payload[4:]
|
||||
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
|
||||
if not MultisigWallet.exists():
|
||||
await ux_show_story("Incoming PSBT requires multisig wallet(s) to be already setup, but you have none.")
|
||||
return
|
||||
|
||||
# may need to iterate over a few wallets?
|
||||
# TODO: multisig
|
||||
ses_key, body, sender_xfp = MultisigWallet.kt_search_rxkey(payload)
|
||||
|
||||
ses_key, body = decode_step1(pair, his_pubkey, body)
|
||||
if sender_xfp is not None:
|
||||
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)
|
||||
|
||||
if not ses_key:
|
||||
# when ECDH fails, it's truncation or wrong RX key (due to sender using old rx key,
|
||||
# or the numeric code the sender entered was wrong, etc)
|
||||
await ux_show_story("QR code was damaged, numeric password was wrong, "
|
||||
"or it was sent to a different user. "
|
||||
"Sender must start again.", title="Teleport Fail")
|
||||
await ux_show_story("QR code was damaged, "+
|
||||
("numeric password was wrong, " if not is_psbt else "")+
|
||||
"or it was sent to a different user. "
|
||||
"Sender must start again.", title="Teleport Fail")
|
||||
return
|
||||
|
||||
from glob import dis
|
||||
while 1:
|
||||
# ask for noid key
|
||||
pw = await ux_input_text('', confirm_exit=False, hex_only=False, max_len=8,
|
||||
prompt='Teleport Password (text)', min_len=8, b39_complete=False, scan_ok=False,
|
||||
prompt=prompt, min_len=8, b39_complete=False, scan_ok=False,
|
||||
placeholder='********', funct_keys=None, force_xy=None)
|
||||
if not pw: return
|
||||
|
||||
@ -286,7 +293,6 @@ async def kt_accept_values(dtype, raw):
|
||||
- `n` - one or many notes export (JSON array)
|
||||
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
|
||||
- `p` - binary PSBT to be signed
|
||||
- `P` - a more-signed binary PSBT being returned back to sender
|
||||
'''
|
||||
from chains import current_chain, slip32_deserialize
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
@ -308,9 +314,9 @@ async def kt_accept_values(dtype, raw):
|
||||
assert ch.name == chains.current_chain.name, 'wrong chain'
|
||||
enc = stash.SecretStash.encode(node=node)
|
||||
|
||||
elif dtype in 'pP':
|
||||
# raw PSBT -- bigger
|
||||
from auth import sign_transaction
|
||||
elif dtype == 'p':
|
||||
# raw PSBT -- much bigger more complex
|
||||
from auth import sign_transaction, TXN_INPUT_OFFSET
|
||||
psbt_len = len(raw)
|
||||
|
||||
# copy into PSRAM
|
||||
@ -373,7 +379,7 @@ def noid_stretch(session_key, noid_key):
|
||||
# TODO: measure timing of this on real Q
|
||||
return ngu.hash.pbkdf2_sha512(session_key, noid_key, 5000)[0:32]
|
||||
|
||||
def encode_payload(my_keypair, his_pubkey, noid_key, body):
|
||||
def encode_payload(my_keypair, his_pubkey, noid_key, body, for_psbt=False):
|
||||
# do all the encryption for sender
|
||||
assert len(his_pubkey) == 33
|
||||
assert len(noid_key) == 5
|
||||
@ -389,6 +395,10 @@ def encode_payload(my_keypair, his_pubkey, noid_key, body):
|
||||
b2 = aes256ctr.new(session_key).cipher(b1)
|
||||
b2 += ngu.hash.sha256s(b1)[-2:]
|
||||
|
||||
if for_psbt:
|
||||
# no need to share pubkey for PSBT files
|
||||
return b2
|
||||
|
||||
return my_keypair.pubkey().to_bytes() + b2
|
||||
|
||||
def decode_step1(my_keypair, his_pubkey, body):
|
||||
@ -541,5 +551,165 @@ class SecretPickerMenu(MenuSystem):
|
||||
return
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 's', raw=raw)
|
||||
|
||||
|
||||
async def kt_send_psbt(psbt, psbt_len=None, post_signing=False):
|
||||
# We just finishing adding our signature to an incomplete PSBT.
|
||||
# User wants to send to one or more other senders for them to complete signing.
|
||||
|
||||
# who remains to sign? look at inputs
|
||||
ms = psbt.active_multisig
|
||||
all_xfps = [x for x,p in ms.get_xfp_paths()]
|
||||
need = [x for x in psbt.multisig_xfps_needed() if x in all_xfps]
|
||||
|
||||
# maybe it's not really a PSBT where we know the other signers? might be
|
||||
# a weird coinjoin we dont fully understand
|
||||
if not need:
|
||||
if not post_signing:
|
||||
await ux_show_story("No more signers?")
|
||||
return
|
||||
|
||||
num_to_complete = ms.M - (ms.N - len(need))
|
||||
|
||||
if post_signing:
|
||||
# They just approved and signed a MS txn perhaps via USB or QR or any source
|
||||
# - offer to save?
|
||||
# - offer them to teleport it (we only come this far if possible)
|
||||
|
||||
if num_to_complete <= 0:
|
||||
# fully signed. we can probably finalize it too
|
||||
# - they have no copy of the result, if it came in via teleport
|
||||
# - if from USB, we'd be uploading back, SD would be saved, etc
|
||||
return
|
||||
|
||||
ch = await ux_show_story("%d more signatures are still required. Press (T) to pick another co-signer to get it next using QR codes." % num_to_complete, title="Teleport PSBT?", escape='t')
|
||||
if ch != 't': return
|
||||
|
||||
|
||||
if not psbt_len:
|
||||
# we need it serialized, might have only saved into Base64 or something
|
||||
from io import BytesIO
|
||||
|
||||
with BytesIO() as fd:
|
||||
psbt.serialize(fd) # need prog bar?
|
||||
|
||||
psbt_len = fd.tell()
|
||||
bin_psbt = fd.getvalue()
|
||||
else:
|
||||
# move out of PSRAM
|
||||
from auth import TXN_OUTPUT_OFFSET
|
||||
|
||||
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
|
||||
bin_psbt = fd.read(psbt_len)
|
||||
|
||||
my_xfp = settings.get('xfp')
|
||||
|
||||
# if my_xfp in need:
|
||||
# - we haven't signed yet? let's do that now .. except we've lost some of the
|
||||
# data we need such as filename to save back into.
|
||||
# - so just keep going instead... maybe they want to be last signer?
|
||||
|
||||
# Make them pick a single next signer. It's not helpful to do multiple at once
|
||||
# here, since we need signatures to be added serially so that last
|
||||
# signer can do finalization. We don't have a general purpose combiner.
|
||||
|
||||
async def done_cb(m, idx, item):
|
||||
m.next_xfp = need[idx]
|
||||
the_ux.pop()
|
||||
|
||||
ci = []
|
||||
next_signer = None
|
||||
for idx, x in enumerate(all_xfps):
|
||||
txt = '[%s] Co-signer #%d' % (xfp2str(x), idx+1)
|
||||
f = done_cb
|
||||
if x == my_xfp:
|
||||
txt += ': YOU'
|
||||
f = None # we dont offer to QR to ourselves
|
||||
elif x not in need:
|
||||
txt += ': DONE'
|
||||
f = None
|
||||
mi = MenuItem(txt, f=f)
|
||||
if x not in need:
|
||||
# show check if we've got sig
|
||||
mi.is_chosen = lambda: True
|
||||
elif next_signer == None:
|
||||
next_signer = idx
|
||||
ci.append(mi)
|
||||
|
||||
m = MenuSystem(ci)
|
||||
m.next_xfp = None
|
||||
m.goto_idx(next_signer) # position cursor on next candidate
|
||||
the_ux.push(m)
|
||||
await m.interact()
|
||||
|
||||
if m.next_xfp:
|
||||
assert m.next_xfp != my_xfp
|
||||
ri, rx_pubkey, kp = ms.kt_make_rxkey(m.next_xfp)
|
||||
await kt_do_send(rx_pubkey, 'p', raw=bin_psbt, prefix=ri, kp=kp,
|
||||
rx_label='[%s] co-signer' % xfp2str(m.next_xfp))
|
||||
|
||||
async def kt_send_file_psbt(*a):
|
||||
# Menu item: choose a PSBT file from SD card, and send to co-signers.
|
||||
# Heavy code re-use here. Need to find the multisig wallet associated w/ file,
|
||||
# so we need to parse it and we must be one of the co-signers.
|
||||
|
||||
from actions import is_psbt, file_picker
|
||||
from auth import sign_psbt_file, TXN_INPUT_OFFSET
|
||||
from version import MAX_TXN_LEN
|
||||
from ux import import_export_prompt
|
||||
from psbt import psbtObject
|
||||
from glob import dis
|
||||
|
||||
# choose any PSBT from SD
|
||||
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)
|
||||
if picked == KEY_CANCEL:
|
||||
return
|
||||
choices = await file_picker(suffix='psbt', min_size=50, ux=False,
|
||||
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
|
||||
if not choices:
|
||||
# error msg already shown
|
||||
return
|
||||
|
||||
if len(choices) == 1:
|
||||
# single - skip the menu
|
||||
label,path,fn = choices[0]
|
||||
input_psbt = path + '/' + fn
|
||||
else:
|
||||
# multiples - make them pick one
|
||||
input_psbt = await file_picker(choices=choices)
|
||||
if not input_psbt:
|
||||
return
|
||||
|
||||
# read into PSRAM from wherever
|
||||
psbt_len = await sign_psbt_file(input_psbt, just_read=True, **picked)
|
||||
|
||||
dis.fullscreen("Validating...")
|
||||
try:
|
||||
dis.progress_sofar(1, 4)
|
||||
with SFFile(TXN_INPUT_OFFSET, length=psbt_len, message='Reading...') as fd:
|
||||
# NOTE: psbtObject captures the file descriptor and uses it later
|
||||
psbt = psbtObject.read_psbt(fd)
|
||||
|
||||
await psbt.validate() # might do UX: accept multisig import
|
||||
|
||||
dis.progress_sofar(2, 4)
|
||||
psbt.consider_inputs()
|
||||
dis.progress_sofar(3, 4)
|
||||
|
||||
psbt.consider_keys()
|
||||
|
||||
except Exception as exc:
|
||||
# not going to do full reporting here, use our other code for that!
|
||||
await ux_show_story("Cannot validate PSBT?\n\n"+str(exc), "PSBT Load Failed")
|
||||
return
|
||||
finally:
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
if not psbt.active_multisig:
|
||||
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
|
||||
return
|
||||
|
||||
await kt_send_psbt(psbt, psbt_len=psbt_len)
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -114,7 +114,7 @@ class VirtDisk:
|
||||
def new_psbt(self, filename):
|
||||
# New incoming PSBT has been detected, start to sign it.
|
||||
from auth import sign_psbt_file
|
||||
uasyncio.create_task(sign_psbt_file(filename, force_vdisk=True, abort=True))
|
||||
uasyncio.create_task(sign_psbt_file(filename, force_vdisk=True, ux_abort=True))
|
||||
|
||||
def new_firmware(self, filename, sz):
|
||||
# potential new firmware file detected
|
||||
|
||||
@ -1291,10 +1291,13 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
||||
assert False, 'timed out'
|
||||
|
||||
txid = None
|
||||
lines = story.split('\n\n')
|
||||
if 'Final TXID:' in story:
|
||||
txid = lines[-2].split("\n")[-1]
|
||||
result_fname = lines[-3]
|
||||
lines = story.split('\n')
|
||||
if 'Final TXID:' in lines:
|
||||
txid = lines[-1]
|
||||
result_fname = lines[-4]
|
||||
elif 'Key Teleport' in lines[-1]:
|
||||
# ignore "Press (T) to use Key Teleport to send PSBT to other co-signers" footer
|
||||
result_fname = lines[2]
|
||||
else:
|
||||
result_fname = lines[-2]
|
||||
|
||||
|
||||
@ -16,6 +16,6 @@ raw = main.ENCODED_SECRET
|
||||
pa.change(new_secret=raw)
|
||||
pa.new_main_secret(raw)
|
||||
|
||||
print("New key in effect: %s" % settings.get('xpub', 'MISSING'))
|
||||
print("Fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
|
||||
print("New key in effect (encoded): %s" % settings.get('xpub', 'MISSING'))
|
||||
print(".. w/ XFP= %s" % xfp2str(settings.get('xfp', 0)))
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ PassphraseMenu.pp_sofar = ''
|
||||
SettingsObject.master_sv_data = {}
|
||||
SettingsObject.master_nvram_key = None
|
||||
set_seed_value(main.WORDS)
|
||||
stash.SensitiveValues.clear_cache()
|
||||
|
||||
settings.set('chain', 'XTN')
|
||||
settings.set('words', len(main.WORDS))
|
||||
@ -30,7 +31,7 @@ settings.set('terms_ok', True)
|
||||
settings.set('idle_to', 0)
|
||||
|
||||
print("New key in effect: %s" % settings.get('xpub', 'MISSING'))
|
||||
print("Fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
|
||||
print(".. w/ fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
|
||||
|
||||
# impt: if going from xprv => seed words, main menu needs updating
|
||||
goto_top_menu()
|
||||
|
||||
@ -152,20 +152,30 @@ def render_bbqr(need_keypress, cap_screen_qr, sim_exec, readback_bbqr_ll):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def try_sign_bbqr(cap_story, scan_a_qr, press_select, press_cancel, need_keypress, goto_home,
|
||||
readback_bbqr):
|
||||
def doit(psbt, type_code="P", approve=True, nfc_push_tx=False, **kws):
|
||||
def split_scan_bbqr(scan_a_qr, goto_home, need_keypress):
|
||||
|
||||
# take big data and send it via series of BBQr thru emulated scanner
|
||||
def doit(raw_data, type_code, **kws):
|
||||
goto_home()
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
# def split_qrs(raw, type_code, encoding=None,
|
||||
# min_split=1, max_split=1295, min_version=5, max_version=40
|
||||
actual_vers, parts = split_qrs(psbt, type_code, **kws)
|
||||
actual_vers, parts = split_qrs(raw_data, type_code, **kws)
|
||||
random.shuffle(parts)
|
||||
|
||||
for p in parts:
|
||||
scan_a_qr(p)
|
||||
time.sleep(4.0 / len(parts)) # just so we can watch
|
||||
time.sleep(2.0 / len(parts)) # just so we can watch
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture
|
||||
def try_sign_bbqr(cap_story, scan_a_qr, press_select, press_cancel, need_keypress,
|
||||
readback_bbqr, split_scan_bbqr):
|
||||
def doit(psbt, type_code="P", approve=True, nfc_push_tx=False, **kws):
|
||||
|
||||
split_scan_bbqr(psbt, type_code, **kws)
|
||||
|
||||
for r in range(20):
|
||||
title, story = cap_story()
|
||||
|
||||
@ -1271,12 +1271,14 @@ def make_myself_wallet(dev, set_bip39_pw, offer_ms_import, press_select, clear_m
|
||||
|
||||
def select_wallet(idx):
|
||||
# select to specific pw
|
||||
print(f"--- switch to another leg of MS: {idx} ---")
|
||||
xfp = set_bip39_pw(passwords[idx])
|
||||
if do_import:
|
||||
offer_ms_import(config)
|
||||
time.sleep(.1)
|
||||
press_select()
|
||||
assert xfp == keys[idx][0]
|
||||
return xfp
|
||||
|
||||
return (keys, select_wallet)
|
||||
|
||||
|
||||
@ -4,14 +4,15 @@
|
||||
#
|
||||
# - you'll need v1.0.1 of bbqr library for this to work
|
||||
#
|
||||
import pytest, time, re
|
||||
from helpers import prandom
|
||||
import pytest, time, re, pdb
|
||||
from helpers import prandom, xfp2str
|
||||
from binascii import a2b_hex
|
||||
from bbqr import split_qrs, join_qrs
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
from base64 import b32encode
|
||||
from constants import *
|
||||
|
||||
from test_bbqr import readback_bbqr
|
||||
from test_bbqr import readback_bbqr, split_scan_bbqr
|
||||
from test_notes import need_some_notes, need_some_passwords
|
||||
from test_ephemeral import SEEDVAULT_TEST_DATA
|
||||
|
||||
@ -35,9 +36,9 @@ def rx_start(grab_payload, goto_home, pick_menu_item):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_story, nfc_block4rf, cap_screen_qr):
|
||||
def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_story, nfc_block4rf, cap_screen_qr, readback_bbqr):
|
||||
|
||||
# start the Rx process, capturing numeric code
|
||||
# started the process; capture pw/code and QR contents, verify NFC works
|
||||
def doit(tt_code, allow_reuse=True, reset_pubkey=False):
|
||||
expect_in_title = 'Receive' if tt_code == 'R' else 'Teleport Password'
|
||||
|
||||
@ -83,9 +84,15 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s
|
||||
|
||||
need_keypress(KEY_QR)
|
||||
|
||||
qr_data = cap_screen_qr().decode()
|
||||
if tt_code != 'E':
|
||||
qr_data = cap_screen_qr().decode()
|
||||
filetype, qr_raw = join_qrs([qr_data])
|
||||
else:
|
||||
# will be multi-frame BBQr in case of PSBT
|
||||
filetype, qr_raw = readback_bbqr()
|
||||
# this is un-split BBQR which didn't really happen, but useful
|
||||
qr_data = f'B$2{filetype}0100' + b32encode(qr_raw).decode('ascii')
|
||||
|
||||
filetype, qr_raw = join_qrs([qr_data])
|
||||
assert filetype == tt_code
|
||||
|
||||
if nfc_raw: assert nfc_raw == qr_raw
|
||||
@ -93,23 +100,29 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s
|
||||
press_cancel()
|
||||
press_cancel()
|
||||
|
||||
return code, qr_data
|
||||
return code, qr_data, qr_raw
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture()
|
||||
def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr, enter_complex, cap_screen, goto_home):
|
||||
def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr, enter_complex, cap_screen, goto_home, split_scan_bbqr):
|
||||
# finish the teleport by doing QR and getting data
|
||||
def doit(data, pw, expect_fail=False):
|
||||
goto_home()
|
||||
need_keypress(KEY_QR)
|
||||
time.sleep(.250) # required
|
||||
scan_a_qr(data)
|
||||
|
||||
if isinstance(data, tuple):
|
||||
bbrq_type, raw = data
|
||||
split_scan_bbqr(raw, bbrq_type, max_version=27)
|
||||
else:
|
||||
assert len(data) < 2000 # USB protocol limit
|
||||
scan_a_qr(data)
|
||||
|
||||
time.sleep(.250) # required
|
||||
if expect_fail: return
|
||||
scr = cap_screen()
|
||||
assert 'Teleport Password (text)' in scr
|
||||
assert 'Teleport Password' in scr
|
||||
|
||||
enter_complex(pw)
|
||||
time.sleep(.150) # required
|
||||
@ -175,7 +188,7 @@ def test_tx_quick_note(rx_start, tx_start, cap_menu, enter_complex, pick_menu_it
|
||||
enter_complex(msg)
|
||||
|
||||
time.sleep(.150) # required
|
||||
pw, data = grab_payload('S')
|
||||
pw, data, _ = grab_payload('S')
|
||||
assert len(pw) == 8
|
||||
|
||||
# now, send that back
|
||||
@ -219,7 +232,7 @@ def test_tx_master_send(rx_start, tx_start, cap_menu, enter_complex, pick_menu_i
|
||||
press_select()
|
||||
|
||||
time.sleep(.150) # required?
|
||||
pw, data = grab_payload('S')
|
||||
pw, data, _ = grab_payload('S')
|
||||
|
||||
# now, send that back
|
||||
rx_complete(data, pw)
|
||||
@ -252,7 +265,7 @@ def test_tx_notes(qty, rx_start, tx_start, cap_menu, enter_complex, pick_menu_it
|
||||
pick_menu_item('Export All Notes & Passwords')
|
||||
|
||||
time.sleep(.150) # required?
|
||||
pw, data = grab_payload('S')
|
||||
pw, data, _ = grab_payload('S')
|
||||
|
||||
# now, send that back
|
||||
rx_complete(data, pw)
|
||||
@ -299,7 +312,7 @@ def test_tx_seedvault(data, rx_start, tx_start, cap_menu, enter_complex, pick_me
|
||||
pick_menu_item(mi)
|
||||
|
||||
time.sleep(.150) # required?
|
||||
pw, data = grab_payload('S')
|
||||
pw, data, _ = grab_payload('S')
|
||||
|
||||
settings_set("seeds", [])
|
||||
|
||||
@ -323,7 +336,7 @@ def test_tx_seedvault(data, rx_start, tx_start, cap_menu, enter_complex, pick_me
|
||||
pick_menu_item('Restore Master')
|
||||
press_select()
|
||||
|
||||
def test_rx_truncated(rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, grab_payload, rx_complete, cap_story, press_cancel, press_select):
|
||||
def test_rx_truncated(rx_start, tx_start, cap_menu, enter_complex, pick_menu_item, rx_complete, cap_story, press_cancel, press_select):
|
||||
# Truncate the RX Code
|
||||
code, rx_pubkey = rx_start()
|
||||
pw = tx_start(rx_pubkey[:-3], code, expect_fail='Truncated KT RX')
|
||||
@ -342,7 +355,7 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite
|
||||
press_select()
|
||||
|
||||
time.sleep(.150) # required?
|
||||
pw, data = grab_payload('S')
|
||||
pw, data, _ = grab_payload('S')
|
||||
|
||||
# now, send that back
|
||||
rx_complete(data, pw, expect_fail=True)
|
||||
@ -355,4 +368,83 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite
|
||||
|
||||
press_cancel()
|
||||
|
||||
@pytest.mark.unfinalized
|
||||
@pytest.mark.parametrize('num_ins', [ 15 ])
|
||||
@pytest.mark.parametrize('M', [2]) # [ 2, 4, 1])
|
||||
@pytest.mark.parametrize('segwit', [True])
|
||||
@pytest.mark.parametrize('incl_xpubs', [ False, True ])
|
||||
def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms,
|
||||
fake_ms_txn, try_sign, incl_xpubs, bitcoind, cap_story, need_keypress,
|
||||
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select):
|
||||
|
||||
# IMPORTANT: wont work if you start simulator with --ms flag. Use no args
|
||||
|
||||
all_out_styles = list(unmap_addr_fmt.keys())
|
||||
num_outs = len(all_out_styles)
|
||||
|
||||
clear_ms()
|
||||
use_regtest()
|
||||
|
||||
# create a wallet, with 3 bip39 pw's
|
||||
keys, select_wallet = make_myself_wallet(M, do_import=(not incl_xpubs))
|
||||
N = len(keys)
|
||||
assert M<=N
|
||||
|
||||
psbt = fake_ms_txn(num_ins, num_outs, M, keys, segwit_in=segwit, incl_xpubs=incl_xpubs,
|
||||
outstyles=all_out_styles, change_outputs=list(range(1,num_outs)))
|
||||
|
||||
open(f'debug/myself-before.psbt', 'wb').write(psbt)
|
||||
|
||||
cur_wallet = 0
|
||||
my_xfp = select_wallet(cur_wallet)
|
||||
|
||||
_, updated = try_sign(psbt, accept_ms_import=incl_xpubs)
|
||||
open(f'debug/myself-after-1.psbt', 'wb').write(updated)
|
||||
assert updated != psbt
|
||||
|
||||
title, body = cap_story()
|
||||
assert title == 'Teleport PSBT?'
|
||||
assert 'Press (T)' in body
|
||||
|
||||
need_keypress('t')
|
||||
|
||||
time.sleep(.1)
|
||||
|
||||
# expect: a menu of xfp to pick from
|
||||
m = cap_menu()
|
||||
assert len(m) == N
|
||||
assert 'YOU' in [ln for ln in m if xfp2str(my_xfp) in ln][0]
|
||||
|
||||
idx = 1
|
||||
next_xfp = keys[1][0]
|
||||
assert next_xfp != my_xfp
|
||||
|
||||
# choose one that isn't me
|
||||
nm, = [mi for mi in m if xfp2str(next_xfp) in mi]
|
||||
pick_menu_item(nm)
|
||||
|
||||
pw, data, qr_raw = grab_payload('E')
|
||||
assert len(pw) == 8
|
||||
|
||||
open(f'debug/next_qr.txt', 'wt').write(f'{xfp2str(next_xfp)}\n\n{pw}\n\n{data}')
|
||||
|
||||
# switch personalities, and try to read that QR
|
||||
new_xfp = select_wallet(idx)
|
||||
assert new_xfp == next_xfp
|
||||
|
||||
# import and sign
|
||||
rx_complete(('E', qr_raw), pw)
|
||||
|
||||
title, body = cap_story()
|
||||
assert title == 'OK TO SEND?'
|
||||
|
||||
press_select()
|
||||
time.sleep(.25)
|
||||
|
||||
title, body = cap_story()
|
||||
assert title == 'Teleport PSBT?'
|
||||
assert '2 more signatures' in body
|
||||
|
||||
pdb.set_trace()
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user