From 3239dc6cd5185b7789a08e35785294745db8b289 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Mon, 7 Apr 2025 12:14:07 +0200 Subject: [PATCH] improve re-export UX; unify USB to done_signing; bugfixes --- shared/actions.py | 4 +- shared/auth.py | 203 +++++++++++--------------- shared/export.py | 11 +- shared/msgsign.py | 3 +- shared/multisig.py | 2 +- shared/nfc.py | 8 +- shared/notes.py | 6 +- shared/qrs.py | 13 +- shared/teleport.py | 62 +++----- shared/ux.py | 20 ++- shared/ux_q1.py | 16 +-- testing/conftest.py | 90 +++++++----- testing/test_multisig.py | 72 +++++----- testing/test_nfc.py | 19 +-- testing/test_sign.py | 298 +++++++++++++++++++-------------------- testing/test_teleport.py | 52 ++++--- testing/test_vdisk.py | 5 - 17 files changed, 425 insertions(+), 459 deletions(-) diff --git a/shared/actions.py b/shared/actions.py index 75f65c88..611afb0a 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -1878,9 +1878,9 @@ async def ready2sign(*a): Put the proposed transaction onto MicroSD card \ in PSBT format (Partially Signed Bitcoin Transaction) \ or upload a transaction to be signed \ -from your desktop wallet software or command line tools.\n\n''' +from your desktop wallet software or command line tools.''' - footnotes = ("\n\nYou will always be prompted to confirm the details " + footnotes = ("You will always be prompted to confirm the details " "before any signature is performed.") # if we have only one SD card inserted, at this point, we know no PSBTs on them diff --git a/shared/auth.py b/shared/auth.py index 7beb9fcf..67473a6f 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -8,17 +8,15 @@ from ubinascii import b2a_base64, a2b_base64 from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex from uhashlib import sha256 -from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS -from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P2WPKH_P2SH +from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED from sffile import SFFile -from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys -from ux import show_qr_code, OK, X, ux_input_text, ux_enter_bip32_index, abort_and_push +from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys +from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction from usb import CCBusyError -from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, chunk_checksum -from utils import B2A, to_ascii_printable, show_single_address +from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, show_single_address from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput -from files import CardSlot, CardMissingError, needs_microsd +from files import CardSlot, CardMissingError from exceptions import HSMDenied from version import MAX_TXN_LEN from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT @@ -74,7 +72,7 @@ class UserAuthorizedAction: if allowed_cls and isinstance(cls.active_request, allowed_cls): return - # check if UX actally was cleared, and we're not really doing that anymore; recover + # check if UX actually was cleared, and we're not really doing that anymore; recover # - happens if USB caller never comes back for their final results from ux import the_ux top_ux = the_ux.top_of_stack() @@ -264,20 +262,25 @@ async def try_push_tx(data, txid, txn_sha=None): return False class ApproveTransaction(UserAuthorizedAction): - def __init__(self, psbt_len, flags=0x0, approved_cb=None, psbt_sha=None, cb_kws=None): - from glob import settings + def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None, + output_encoder=None, filename=None): super().__init__() self.psbt_len = psbt_len - self.do_finalize = bool(flags & STXN_FINALIZE) - self.do_visualize = bool(flags & STXN_VISUALIZE) + + # do finalize is None if not USB, None = decide based on is_complete + if flags is None: + self.do_finalize = self.do_visualize = None + else: + self.do_finalize = bool(flags & STXN_FINALIZE) + self.do_visualize = bool(flags & STXN_VISUALIZE) + self.stxn_flags = flags self.psbt = None self.psbt_sha = psbt_sha - self.approved_cb = approved_cb + self.input_method = input_method + self.output_encoder = output_encoder + self.filename = filename self.result = None # will be (len, sha256) of the resulting PSBT - self.cb_kws = cb_kws or {} - self.is_sd = (self.cb_kws.get("input_method", None) is None) \ - and (not self.cb_kws.get("force_vdisk", False)) self.chain = chains.current_chain() def render_output(self, o): @@ -447,7 +450,7 @@ class ApproveTransaction(UserAuthorizedAction): if needs_txn_explorer: esc += "2" msg.write(" Press (2) to explore txn.") - if self.is_sd and CardSlot.both_inserted(): + if (self.input_method == "sd") and CardSlot.both_inserted(): esc += "b" msg.write(" (B) to write to lower SD slot.") msg.write(" %s to abort." % X) @@ -516,70 +519,17 @@ class ApproveTransaction(UserAuthorizedAction): except BaseException as exc: return await self.failure("Signing failed late", exc) - if self.approved_cb: - if self.is_sd and (ch == "b"): - self.cb_kws["slot_b"] = True - - await self.approved_cb(self.psbt, **self.cb_kws) - - self.done() - return - - txid = None try: - # re-serialize the PSBT back out - with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: - if self.do_finalize: - # user is forcing us to finalize over USB - # try it & fail with detailed msg - txid = self.psbt.finalize(fd) - else: - self.psbt.serialize(fd) - - self.result = (fd.tell(), fd.checksum.digest()) - - self.done(redraw=(not txid)) - + await done_signing(self.psbt, self, self.input_method, self.filename, self.output_encoder, + slot_b=True if ch == "b" else False, finalize=self.do_finalize) + self.done() + except AbortInteraction: + # user might have sent new sign cmd, while we still at export prompt + pass except BaseException as exc: + # sys.print_exception(exc) return await self.failure("PSBT output failed", exc) - from glob import NFC - - if self.do_finalize and txid and not hsm_active: - # Unsigned PSBT came in via NFC or QR or Teleport - - if await try_push_tx(self.result[0], txid, self.result[1]): - return # success, exit - - kq, kn = "(1)", "(3)" - if version.has_qwerty: - kq, kn = KEY_QR, KEY_NFC - - while 1: - # Show txid when we can; advisory - # - maybe even as QR, hex-encoded in alnum mode - tmsg = txid + '\n\nPress %s for QR Code of TXID. ' % kq - - if NFC: - tmsg += 'Press %s to share signed txn via NFC.' % kn - - ch = await ux_show_story(tmsg, "Final TXID", escape='13'+KEY_NFC+KEY_QR) - - if ch in '1'+KEY_QR: - await show_qr_code(txid, True) - continue - - if ch in KEY_NFC+"3" and NFC: - await NFC.share_signed_txn(txid, TXN_OUTPUT_OFFSET, - self.result[0], self.result[1]) - continue - break - - elif version.has_qwerty and self.psbt.active_multisig: - # Offer to teleport the result, which still needs signatures - 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 @@ -761,7 +711,7 @@ def sign_transaction(psbt_len, flags=0x0, psbt_sha=None): # transaction (binary) loaded into PSRAM already, checksum checked UserAuthorizedAction.check_busy(ApproveTransaction) UserAuthorizedAction.active_request = ApproveTransaction( - psbt_len, flags, psbt_sha=psbt_sha, cb_kws={"input_method": "usb"} + psbt_len, flags, psbt_sha=psbt_sha, input_method="usb", ) # kill any menu stack, and put our thing at the top @@ -789,19 +739,24 @@ def psbt_encoding_taster(taste, psbt_len): return decoder, output_encoder, psbt_len -async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False, +async def done_signing(psbt, tx_req, input_method=None, filename=None, 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 glob import PSRAM, hsm_active from sffile import SFFile - from ux import show_qr_code, import_export_prompt, ux_show_story + from ux import show_qr_code, import_export_prompt + + first_time = True + msg = None + title = None - txid = None is_complete = psbt.is_complete() + if finalize is not None: + # USB case - user can choose whether to attempt finalization + is_complete = finalize with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram: if is_complete: @@ -815,9 +770,18 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False data_len = psram.tell() data_sha2 = psram.checksum.digest() - UserAuthorizedAction.cleanup() + if input_method == "usb": + # return result over USB before going to all options + tx_req.result = data_len, data_sha2 + if hsm_active: + # it is enough to just return back via USB, other options + # are pointless + return + + first_time = False + msg = noun + " shared via USB." + title = "PSBT Signed" - first_time = True if txid and await try_push_tx(data_len, txid, data_sha2): # go directly to reexport menu after pushTX first_time = False @@ -830,32 +794,45 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False while True: ch = None if first_time: - # first time, assume they want to send out same way it came in -- dont prompt + # first time, assume they want to send out same way it came in -- don't prompt if input_method == "qr": ch = KEY_QR elif input_method == "nfc": ch = KEY_NFC elif input_method == "kt": - # not reached .. because we offer KT method before this is called ch = 't' else: # SD/VDisk - ch = {"force_vdisk": force_vdisk, "slot_b": slot_b} + ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b} if not ch: # show all possible export options (based on hardware enabled, features) - ch = await import_export_prompt(noun, no_qr=not version.has_qr, offer_kt=offer_kt) + intro = [] + if msg: + intro.append(msg) + if txid: + intro.append('TXID:\n' + txid) + ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt, + txid=txid, title=title) if ch == KEY_CANCEL: + UserAuthorizedAction.cleanup() break + elif txid and (ch == '6'): + await show_qr_code(txid, is_alnum=True, force_msg=True) + continue + elif ch == KEY_QR: here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len) msg = txid or 'Partly Signed PSBT' try: + if len(here) > 920: + # too big for simple QR - use BBQr instead + raise ValueError hex_here = b2a_hex(here).upper().decode() await show_qr_code(hex_here, is_alnum=True, msg=msg) - except (ValueError, RuntimeError): + except (ValueError, RuntimeError, TypeError): from ux_q1 import show_bbqr_codes await show_bbqr_codes('T' if txid else 'P', here, msg) @@ -871,33 +848,26 @@ async def done_signing(psbt, input_method=None, filename=None, force_vdisk=False msg = noun + " shared via NFC." - elif ch == 't': - # after saving to card, they might want to teleport it + elif (ch == 't') and not is_complete: + # they might want to teleport it, but only if we have PSBT + # there is no need to teleport PSBT if txn is already complete & ready to be broadcast from teleport import kt_send_psbt - ok = await kt_send_psbt(psbt) - msg = 'Failed to Teleport' if not ok else 'Sent by Teleport' + ok, num_sigs_needed = await kt_send_psbt(psbt, data_len) + title = 'Sent by Teleport' if ok else 'Failed to Teleport' + if num_sigs_needed > 0: + s, aux = ("", "is") if num_sigs_needed == 1 else ("s", "are") + msg = "%d more signature%s %s still required." % (num_sigs_needed, s, aux) + continue else: # typical case: save to SD card, show filenames we used assert isinstance(ch, dict) - msg = await _save_to_disk(psbt, txid, ch, is_complete, data_len, output_encoder, filename) - if not msg: - # it failed so start again with all possible options enabled. - ch = None - input_method = None - first_time = False - continue + msg = await _save_to_disk(psbt, txid, ch, is_complete, data_len, + output_encoder, filename) - msg += '\n\n' - esc = '0' - msg += 'Press (0) to save again by another method.' - - ch2 = await ux_show_story(msg, title='PSBT Signed', escape=esc) - if ch2 != '0': - break - else: - input_method = None - first_time = False + input_method = None + first_time = False + title = "PSBT Signed" async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None): # Saving a PSBT from PSRAM to something disk-like. @@ -995,7 +965,7 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_ except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc - sys.print_exception(exc) + # sys.print_exception(exc) # fall through to try again # If this point reached, some problem, we could not write. @@ -1023,8 +993,6 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_ if out2_fn: msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn - if txid and not del_after: - msg += '\n\nFinal TXID:\n' + txid return msg @@ -1085,11 +1053,8 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=Fal UserAuthorizedAction.cleanup() UserAuthorizedAction.active_request = ApproveTransaction( - psbt_len, - approved_cb=done_signing, - cb_kws={"filename": filename, - "force_vdisk": force_vdisk, - "output_encoder": output_encoder} + psbt_len, input_method="vdisk" if force_vdisk else "sd", + filename=filename, output_encoder=output_encoder, ) if ux_abort: # needed for auto vdisk mode diff --git a/shared/export.py b/shared/export.py index 5990683d..cd77086b 100644 --- a/shared/export.py +++ b/shared/export.py @@ -18,9 +18,7 @@ async def export_by_qr(body, label, type_code, force_bbqr=False): from ux import show_qr_code try: - # ignore label/title - provides no useful info - # makes qr smaller and harder to read - if force_bbqr: + if force_bbqr or len(body) > 2000: raise ValueError await show_qr_code(body) @@ -35,7 +33,7 @@ async def export_by_qr(body, label, type_code, force_bbqr=False): return -async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None, no_qr=None, +async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None, is_json=False, force_bbqr=False, force_prompt=False): # export text and json files while offering NFC, QR & Vdisk # produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt) @@ -49,8 +47,9 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt= dis.fullscreen('Generating...') contents, derive, addr_fmt = contents() - if no_qr is not None: - no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT) + # figure out if offering QR code export make sense given HW + # len() is O(1) + no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT) sig = not (derive is None and addr_fmt is None) diff --git a/shared/msgsign.py b/shared/msgsign.py index 05e97c64..43bcbaf8 100644 --- a/shared/msgsign.py +++ b/shared/msgsign.py @@ -420,8 +420,7 @@ async def ux_sign_msg(txt, approved_cb=None, kill_menu=True): the_ux.push(MenuSystem(rv)) async def msg_signing_done(signature, address, text): - ch = await import_export_prompt("Signed Msg", is_import=False, - no_qr=not version.has_qwerty) + ch = await import_export_prompt("Signed Msg") if ch == KEY_CANCEL: return diff --git a/shared/multisig.py b/shared/multisig.py index d0c668c8..3403ecd8 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -6,7 +6,7 @@ import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson, version from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str, to_ascii_printable from utils import str_to_keypath, problem_file_line, parse_extended_key, get_filesize, extract_cosigner from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys -from ux import import_export_prompt, ux_enter_bip32_index, ux_enter_number, OK, X +from ux import ux_enter_bip32_index, ux_enter_number, OK, X from files import CardSlot, CardMissingError, needs_microsd from descriptor import MultisigDescriptor, multisig_descriptor_template from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_CLASSIC diff --git a/shared/nfc.py b/shared/nfc.py index f2f52b48..f32f4de3 100644 --- a/shared/nfc.py +++ b/shared/nfc.py @@ -523,7 +523,6 @@ class NFCHandler: from auth import psbt_encoding_taster, TXN_INPUT_OFFSET from auth import UserAuthorizedAction, ApproveTransaction from ux import the_ux - from auth import done_signing from sffile import SFFile data = await self.start_nfc_rx() @@ -567,10 +566,9 @@ class NFCHandler: # start signing UX UserAuthorizedAction.cleanup() UserAuthorizedAction.active_request = ApproveTransaction( - psbt_len, 0x0, psbt_sha=psbt_sha, - approved_cb=done_signing, - cb_kws={"input_method": "nfc", - "output_encoder": output_encoder}) + psbt_len, psbt_sha=psbt_sha, input_method="nfc", + output_encoder=output_encoder + ) # kill any menu stack, and put our thing at the top the_ux.push(UserAuthorizedAction.active_request) diff --git a/shared/notes.py b/shared/notes.py index 81debbcb..fa4d8e5f 100644 --- a/shared/notes.py +++ b/shared/notes.py @@ -543,9 +543,9 @@ async def start_export(notes): singular = (len(notes) == 1) item = notes[0].type_label if singular else 'all notes & passwords' - choice = await import_export_prompt(item, is_import=False, title="Data Export", no_nfc=True, - footnotes="\n\nWARNING: No encryption happens here. " - "Your secrets will be cleartext.") + choice = await import_export_prompt(item, title="Data Export", no_nfc=True, + footnotes="WARNING: No encryption happens here." + " Your secrets will be cleartext.") if choice == KEY_CANCEL: return diff --git a/shared/qrs.py b/shared/qrs.py index 3b01c4ac..a933c25c 100644 --- a/shared/qrs.py +++ b/shared/qrs.py @@ -81,7 +81,16 @@ class QRDisplaySingle(UserInteraction): # draw display dis.busy_bar(False) - dis.draw_qr_display(self.qr_data, self.msg or body, self.is_alnum, + + if self.msg: + msg = self.msg + else: + msg = None + if isinstance(body, str) and not has_qwerty: + # on Mk4 if no self.msg, we show part of the body + msg = body + + dis.draw_qr_display(self.qr_data, msg, self.is_alnum, self.sidebar, self.idx_hint(), self.invert, is_addr=self.is_addrs, force_msg=self.force_msg) @@ -103,7 +112,7 @@ class QRDisplaySingle(UserInteraction): break else: # Share any QR over NFC! - await NFC.share_text(self.addrs[self.idx], secret=self.secret) + await NFC.share_text(self.addrs[self.idx], is_secret=self.is_secret) self.redraw() continue elif ch in 'xy'+KEY_ENTER+KEY_CANCEL: diff --git a/shared/teleport.py b/shared/teleport.py index dada51eb..5bfc5e19 100644 --- a/shared/teleport.py +++ b/shared/teleport.py @@ -4,11 +4,10 @@ # secure environment of two Q's. # import ngu, aes256ctr, bip39, json, ndef, chains -from io import BytesIO from utils import xfp2str, deserialize_secret from ubinascii import unhexlify as a2b_hex from ubinascii import hexlify as b2a_hex -from glob import settings +from glob import settings, dis from ux import ux_show_story, ux_confirm, the_ux, ux_dramatic_pause from ux_q1 import show_bbqr_codes, QRScannerInteraction, ux_input_text from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL @@ -192,8 +191,9 @@ WARNING: Receiver will have full access to all Bitcoin controlled by these keys! async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix=b'', 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 + dis.fullscreen("Wait...") cleartext = dtype.encode() + (raw or json.dumps(obj).encode()) + dis.progress_bar_show(0.1) # Pick and show noid key to sender noid_key, txt = pick_noid_key() @@ -214,8 +214,8 @@ async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix=b'', rx_label= "\n\n %s = %s\n\n" % (rx_label, txt, txt_grouper(txt)) msg += "ENTER to view QR" - await tk_show_payload('S' if not prefix else 'E', - payload, 'Teleport Password', msg, cta='Show to Receiver') + await tk_show_payload('S' if not prefix else 'E', payload, + 'Teleport Password', msg, cta='Show to Receiver') if not prefix: # not PSBT case ... reset menus, we are deep! @@ -269,7 +269,6 @@ async def kt_decode_rx(is_psbt, payload): "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, @@ -312,6 +311,7 @@ async def kt_accept_values(dtype, raw): ''' from flow import has_se_secrets, goto_top_menu + dis.fullscreen("Wait...") enc = None origin = 'Teleported' label = None @@ -341,7 +341,8 @@ async def kt_accept_values(dtype, raw): out.write(raw) # This will take over UX w/ the signing process - sign_transaction(psbt_len, flags=STXN_FINALIZE) + # flags=None --> whether to finalize is decided based on psbt.is_complete + sign_transaction(psbt_len, flags=None) return elif dtype == 'b': @@ -579,7 +580,6 @@ class SecretPickerMenu(MenuSystem): if ch != 'y': return from backups import render_backup_contents - from glob import dis dis.fullscreen("Buiding Backup...") @@ -620,7 +620,7 @@ class SecretPickerMenu(MenuSystem): await kt_do_send(self.rx_pubkey, 's', raw=raw) -async def kt_send_psbt(psbt, psbt_len=None, post_signing=False): +async def kt_send_psbt(psbt, psbt_len): # We just finished adding our signature to an incomplete PSBT. # User wants to send to one or more other senders for them to complete signing. @@ -632,44 +632,14 @@ async def kt_send_psbt(psbt, psbt_len=None, post_signing=False): # maybe it's not really a PSBT where we know the other signers? might be # a weird coinjoin we don't fully understand if not need: - if not post_signing: - await ux_show_story("No more signers?") + await ux_show_story("No more signers?") return - num_to_complete = ms.M - (ms.N - len(need)) + # move out of PSRAM + from auth import TXN_OUTPUT_OFFSET - 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: - # Sufficiently signed. We can probably finalize it too. - # - 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 sign next, using QR codes, or ENTER for other options." % num_to_complete, title="Teleport PSBT?", escape='t') - if ch != 't': - # ENTER/CANCEL both come here because we don't want to lose the PSBT - # - they can also do a "T" and teleport again - from auth import done_signing - await done_signing(psbt) - return - - - if not psbt_len: - # we need it serialized, might have only saved into Base64 or something - 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) + with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd: + bin_psbt = fd.read(psbt_len) my_xfp = settings.get('xfp') @@ -701,7 +671,8 @@ async def kt_send_psbt(psbt, psbt_len=None, post_signing=False): async def sign_now(*a): # this will reset the UX stack: - sign_transaction(psbt_len, flags=STXN_FINALIZE) + # flags=None --> whether to finalize is decided based on psbt.is_complete + sign_transaction(psbt_len, flags=None) f = sign_now @@ -743,7 +714,6 @@ async def kt_send_file_psbt(*a): 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) diff --git a/shared/ux.py b/shared/ux.py index 8fcb2799..c74a3254 100644 --- a/shared/ux.py +++ b/shared/ux.py @@ -393,7 +393,7 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False): def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False, - force_prompt=False): + force_prompt=False, txid=None): # Build the prompt for export # - key0 can be for special stuff from glob import NFC, VD @@ -430,6 +430,10 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe prompt += ", (4) to show QR code" escape += '4' + if txid: + prompt += ", (6) for QR Code of TXID" + escape += "6" + if offer_kt: prompt += ", (T) to " + offer_kt escape += 't' @@ -474,8 +478,9 @@ def import_export_prompt_decode(ch): return dict(force_vdisk=force_vdisk, slot_b=slot_b) async def import_export_prompt(what_it_is, is_import=False, no_qr=False, - no_nfc=False, title=None, intro='', footnotes='', - offer_kt=False, slot_b_only=False, force_prompt=False): + no_nfc=False, title=None, intro='', footnotes='', + offer_kt=False, slot_b_only=False, force_prompt=False, + txid=None): # Show story allowing user to select source for importing/exporting # - return either str(mode) OR dict(file_args) @@ -483,11 +488,12 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False, # - KEY_CANCEL for abort by user # - dict() => do file system thing, using file_args to control vdisk vs. SD vs slot_b # - 't' => key teleport, but only offered with offer_kt is set (contetxt, and Q only) + from glob import NFC if is_import: prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only) else: - prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, + prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, txid=txid, force_prompt=force_prompt, offer_kt=offer_kt) # TODO: detect if we're only asking A or B, when just one card is inserted @@ -498,8 +504,10 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False, # they don't have NFC nor VD enabled, and no second slots... so will be file. return dict(force_vdisk=False, slot_b=None) else: - ch = await ux_show_story(intro+prompt+footnotes, escape=escape, title=title, - strict_escape=True) + hints = ("" if no_qr else KEY_QR) + (KEY_NFC if not no_nfc and NFC else "") + msg_lst = [i for i in (intro, prompt, footnotes) if i] + ch = await ux_show_story("\n\n".join(msg_lst), escape=escape, title=title, + strict_escape=True, hint_icons=hints) return import_export_prompt_decode(ch) diff --git a/shared/ux_q1.py b/shared/ux_q1.py index 623efd76..91a696c6 100644 --- a/shared/ux_q1.py +++ b/shared/ux_q1.py @@ -993,7 +993,7 @@ class QRScannerInteraction: async def qr_psbt_sign(decoder, psbt_len, raw): # Got a PSBT coming in from QR scanner. Sign it. # - similar to auth.sign_psbt_file() - from auth import UserAuthorizedAction, ApproveTransaction, done_signing + from auth import UserAuthorizedAction, ApproveTransaction from ux import the_ux from sffile import SFFile from auth import TXN_INPUT_OFFSET, psbt_encoding_taster @@ -1024,9 +1024,8 @@ async def qr_psbt_sign(decoder, psbt_len, raw): UserAuthorizedAction.cleanup() UserAuthorizedAction.active_request = ApproveTransaction( - psbt_len, approved_cb=done_signing, - cb_kws={"input_method": "qr", - "output_encoder": output_encoder} + psbt_len, input_method="qr", + output_encoder=output_encoder ) the_ux.push(UserAuthorizedAction.active_request) @@ -1193,19 +1192,18 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False): # BBQr header hdr = 'B$' + encoding + type_code + int2base36(num_parts) + int2base36(pkt) - # encode the bytes assert pos < data_len, (pkt, pos, data_len) if already_hex: - # not encoding, just chars->bytes + # not encoding, just hex string hp = pos*2 - body = data[hp:hp+(part_size*2)].decode() + body = data[hp:hp+(part_size*2)] else: - # base32 encoding + # encode bytes to base32 encoding body = b32encode(data[pos:pos+part_size]) pos += part_size - # first first packet, want to discover a working small value for QR version + # first packet, want to discover a working small value for QR version if pkt == 0: mnv = 10 if num_parts > 1 else 1 else: diff --git a/testing/conftest.py b/testing/conftest.py index b29bf086..6591a510 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1287,14 +1287,15 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home, for r in range(10): time.sleep(0.1) title, story = cap_story() - if title == 'PSBT Signed': break + if 'Updated PSBT' in story: break + if 'Finalized transaction' in story: break else: assert False, 'timed out' lines = story.split('\n') txid = None - if 'Final TXID:' in lines: - txid = lines[lines.index('Final TXID:')+1] + if 'TXID:' in lines: + txid = lines[lines.index('TXID:')+1] # This is fragile! # ignore "Press (T) to use Key Teleport to send PSBT to other co-signers" footer @@ -1359,9 +1360,11 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home, @pytest.fixture def try_sign(start_sign, end_sign): - def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False): + def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False, + exit_export_loop=True): ip = start_sign(filename_or_data, finalize=finalize) - return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import) + return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import, + exit_export_loop=exit_export_loop) return doit @@ -1387,10 +1390,11 @@ def start_sign(dev): return doit @pytest.fixture -def end_sign(dev, need_keypress): +def end_sign(dev, need_keypress, press_cancel): from ckcc_protocol.protocol import CCUserRefused - def doit(accept=True, in_psbt=None, finalize=False, accept_ms_import=False, expect_txn=True): + def doit(accept=True, finalize=False, accept_ms_import=False, expect_txn=True, + exit_export_loop=True): if accept_ms_import: # XXX would be better to do cap_story here, but that would limit test to simulator @@ -1445,6 +1449,9 @@ def end_sign(dev, need_keypress): for sig in sigs: assert len(sig) <= 71, "overly long signature observed" + if exit_export_loop: + press_cancel() # landed back to export prompt - exit + return psbt_out return doit @@ -1871,10 +1878,48 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache return contents, address, fname return doit +@pytest.fixture +def file_tx_signing_done(virtdisk_path, microsd_path): + def doit(story, encoding="base64", is_vdisk=False): + path_f = virtdisk_path if is_vdisk else microsd_path + enc = "rb" if encoding == "binary" else "r" + _split = story.split("\n\n") + export = None + if 'Updated PSBT is:' == _split[0]: + fname = _split[1] + path = path_f(fname) + with open(path, enc) as f: + export = f.read().strip() + + export_tx = None + if "Finalized transaction (ready for broadcast)" in _split[2]: + fname_tx = _split[3] + path_tx = path_f(fname_tx) + with open(path_tx, enc) as f: + export_tx = f.read().strip() + else: + # just finalized tx + assert "Finalized transaction (ready for broadcast):" == _split[0] + fname_tx = _split[1] + path_tx = path_f(fname_tx) + with open(path_tx, enc) as f: + export_tx = f.read() + + txid = None + for l in _split: + if "TXID" in l: + txid = l.split("\n")[-1].strip() + assert len(txid) == 64, "wrong txid" + break + + return export, export_tx, txid + + return doit + @pytest.fixture def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json, load_export_and_verify_signature, is_q1, press_cancel, press_select, readback_bbqr, - cap_screen_qr, nfc_read_txn): + cap_screen_qr, nfc_read_txn, file_tx_signing_done): def doit(way, label, is_json, sig_check=True, addr_fmt=AF_CLASSIC, ret_sig_addr=False, tail_check=None, sd_key=None, vdisk_key=None, nfc_key=None, ret_fname=False, fpattern=None, qr_key=None, is_tx=False, encoding="base64"): @@ -1954,29 +1999,7 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_ label=label, tail_check=tail_check, fpattern=fpattern ) elif is_tx: - enc = "rb" if encoding == "binary" else "r" - _split = story.split("\n\n") - export = None - if 'Updated PSBT is:' == _split[0]: - fname = _split[1] - path = path_f(fname) - with open(path, enc) as f: - export = f.read() - - export_tx = None - if "Finalized transaction (ready for broadcast)" in _split[2]: - fname_tx = _split[3] - path_tx = path_f(fname_tx) - with open(path_tx, enc) as f: - export_tx = f.read() - else: - # just finalized tx - assert "Finalized transaction (ready for broadcast):" == _split[0] - fname_tx = _split[1] - path_tx = path_f(fname_tx) - with open(path_tx, enc) as f: - export_tx = f.read() - + export, export_tx, _ = file_tx_signing_done(story, encoding, is_vdisk=(way == "vdisk")) return export, export_tx else: assert f"{label} file written" in story @@ -2014,7 +2037,6 @@ def signing_artifacts_reexport(cap_story, need_keypress, load_export, press_canc def _check_story(the_way): time.sleep(.2) title, story = cap_story() - assert title == "PSBT Signed" if the_way in ["qr", "nfc"]: what = label + " shared via %s." % the_way.upper() @@ -2027,9 +2049,6 @@ def signing_artifacts_reexport(cap_story, need_keypress, load_export, press_canc if txid: assert txid in story - assert "Press (0) to save again" in story - need_keypress("0") - to_do = ["sd", "vdisk", "nfc", "qr"] if not is_usb: _check_story(way) @@ -2544,6 +2563,7 @@ from test_notes import need_some_notes, need_some_passwords from test_nfc import try_sign_nfc, ndef_parse_txn_psbt from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed from test_seed_xor import restore_seed_xor +from test_sign import txid_from_export_prompt from test_ux import pass_word_quiz, word_menu_entry, enable_hw_ux from txn import fake_txn diff --git a/testing/test_multisig.py b/testing/test_multisig.py index b512df2b..6c868df8 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -2706,7 +2706,7 @@ def bitcoind_multisig(bitcoind, bitcoind_d_sim_watch, need_keypress, cap_story, @pytest.mark.parametrize("script", ["p2wsh", "p2sh-p2wsh", "p2sh"]) @pytest.mark.parametrize('desc', ["multi", "sortedmulti"]) def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisig, bitcoind, - try_sign, cap_story, settings_set): + try_sign, cap_story, settings_set, txid_from_export_prompt, press_cancel): if desc == "multi": settings_set("unsort_ms", 1) @@ -2734,15 +2734,16 @@ def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisi psbt_bytes = base64.b64decode(psbt) # USB sign with COLDCARD & finalize - _, txn = try_sign(psbt_bytes, finalize=True) + _, txn = try_sign(psbt_bytes, finalize=True, exit_export_loop=False) tx_hex = txn.hex() res = wo.testmempoolaccept([tx_hex]) assert res[0]["allowed"] res = wo.sendrawtransaction(tx_hex) assert len(res) == 64 # tx id - time.sleep(0.1) - _, story = cap_story() - cc_tx_id = story.split("\n\n")[0] + + cc_tx_id = txid_from_export_prompt() + press_cancel() # exit QR display + press_cancel() # exit export loop assert res == cc_tx_id wo.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) @@ -2760,15 +2761,16 @@ def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisi psbt_bytes = base64.b64decode(psbt) # USB sign with COLDCARD & finalize - _, txn = try_sign(psbt_bytes, finalize=True) + _, txn = try_sign(psbt_bytes, finalize=True, exit_export_loop=False) tx_hex = txn.hex() res = wo.testmempoolaccept([tx_hex]) assert res[0]["allowed"] res = wo.sendrawtransaction(tx_hex) assert len(res) == 64 # tx id - time.sleep(0.1) - _, story = cap_story() - cc_tx_id = story.split("\n\n")[0] + + cc_tx_id = txid_from_export_prompt() + press_cancel() # exit QR display + press_cancel() # exit export loop assert res == cc_tx_id wo.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) @@ -2779,13 +2781,14 @@ def test_finalization(m_n, script, desc, use_regtest, clear_ms, bitcoind_multisi @pytest.mark.parametrize("m_n", [(2,3), (3,5), (15,15)]) @pytest.mark.parametrize("script", ["p2wsh", "p2sh-p2wsh", "p2sh"]) @pytest.mark.parametrize("sighash", list(SIGHASH_MAP.keys())) -@pytest.mark.parametrize("psbt_v2", [True, False]) @pytest.mark.parametrize('desc', ["multi", "sortedmulti"]) def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, pick_menu_item, sighash, cap_menu, cap_story, microsd_path, use_regtest, bitcoind, - microsd_wipe, settings_set, psbt_v2, is_q1, try_sign, - finalize_v2_v0_convert, press_select, desc, bitcoind_multisig): + microsd_wipe, settings_set, is_q1, try_sign, press_select, + finalize_v2_v0_convert, desc, bitcoind_multisig, press_cancel, + txid_from_export_prompt, pytestconfig, file_tx_signing_done): # 2of2 case here is described in docs with tutorial + # TODO This test MUST be run with --psbt2 flag on and off if desc == "multi": settings_set("unsort_ms", 1) @@ -2823,7 +2826,7 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, half_signed_psbt = signer.walletprocesspsbt(psbt, True, sighash, True) # do not finalize psbt = half_signed_psbt["psbt"] - if psbt_v2: + if pytestconfig.getoption('psbt2'): # below is noop if psbt is already v2 po = BasicPSBT().parse(base64.b64decode(psbt)) po.to_v2() @@ -2856,20 +2859,11 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, press_select() # confirm signing time.sleep(0.1) title, story = cap_story() - assert "PSBT Signed" == title assert "Updated PSBT is:" in story press_select() os.remove(microsd_path(name)) - split_story = story.split("\n\n") - fname = split_story[1] - fname_tx = split_story[3] - cc_tx_id = split_story[-2].split("\n")[-1] - with open(microsd_path(fname), "r") as f: - final_psbt = f.read().strip() - - with open(microsd_path(fname_tx), "r") as f: - final_tx = f.read().strip() + final_psbt, final_tx, cc_tx_id = file_tx_signing_done(story) po = BasicPSBT().parse(base64.b64decode(final_psbt)) res = finalize_v2_v0_convert(po) @@ -2897,7 +2891,7 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, half_signed_psbt = signer.walletprocesspsbt(psbt, True, sighash, True) # do not finalize psbt = half_signed_psbt["psbt"] - if psbt_v2: + if pytestconfig.getoption('psbt2'): # below is noop if psbt is already v2 po = BasicPSBT().parse(base64.b64decode(psbt)) po.to_v2() @@ -2905,15 +2899,15 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, psbt_bytes = base64.b64decode(psbt) # USB sign with COLDCARD & finalize - _, txn = try_sign(psbt_bytes, finalize=True) + _, txn = try_sign(psbt_bytes, finalize=True, exit_export_loop=False) tx_hex = txn.hex() res = bitcoind_watch_only.testmempoolaccept([tx_hex]) assert res[0]["allowed"] res = bitcoind_watch_only.sendrawtransaction(tx_hex) assert len(res) == 64 # tx id - time.sleep(0.1) - _, story = cap_story() - cc_tx_id = story.split("\n\n")[0] + cc_tx_id = txid_from_export_prompt() + press_cancel() # exit QR display + press_cancel() # exit export loop assert res == cc_tx_id bitcoind_watch_only.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # need to mine above tx @@ -2931,6 +2925,10 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, x = BasicPSBT().parse(base64.b64decode(psbt)) for idx, i in enumerate(x.inputs): i.sighash = SIGHASH_MAP[sighash] + + if pytestconfig.getoption('psbt2'): + x.to_v2() + psbt = x.as_b64_str() name = f"change_{M}of{N}_{script}.psbt" @@ -2957,12 +2955,11 @@ def test_bitcoind_MofN_tutorial(m_n, script, clear_ms, goto_home, need_keypress, assert "missing" in story return - assert "PSBT Signed" == title assert "Updated PSBT is:" in story - press_select() - fname = story.split("\n\n")[-2] - with open(microsd_path(fname), "r") as f: - cc_signed_psbt = f.read().strip() + cc_signed_psbt, _txn, _txid = file_tx_signing_done(story) + assert _txn is None and _txid is None + + press_cancel() # exit re-export loop po = BasicPSBT().parse(base64.b64decode(cc_signed_psbt)) cc_signed_psbt = finalize_v2_v0_convert(po)["psbt"] @@ -3330,7 +3327,6 @@ def test_bare_cc_ms_qr_import(N, make_multisig, scan_a_qr, clear_ms, goto_home, press_cancel() -@pytest.mark.parametrize("psbtv2", [True, False]) @pytest.mark.parametrize("desc", ["multi", "sortedmulti"]) @pytest.mark.parametrize("data", [ # (out_style, amount, is_change) @@ -3339,8 +3335,9 @@ def test_bare_cc_ms_qr_import(N, make_multisig, scan_a_qr, clear_ms, goto_home, [("p2wsh-p2sh", 1000000, 1)] * 18 + [("p2wsh", 50000000, 0)] * 12, [("p2sh", 1000000, 1), ("p2wsh-p2sh", 50000000, 0), ("p2wsh", 800000, 1)] * 14, ]) -def test_txout_explorer(psbtv2, data, clear_ms, import_ms_wallet, fake_ms_txn, - start_sign, txout_explorer, desc): +def test_txout_explorer(data, clear_ms, import_ms_wallet, fake_ms_txn, + start_sign, txout_explorer, desc, pytestconfig): + # TODO This test MUST be run with --psbt2 flag on and off clear_ms() M, N = 2, 3 descriptor, bip67 = False, True @@ -3362,7 +3359,8 @@ def test_txout_explorer(psbtv2, data, clear_ms, import_ms_wallet, fake_ms_txn, inp_amount = sum(outvals) + 100000 # 100k sat fee psbt = fake_ms_txn(1, len(data), M, keys, outstyles=outstyles, outvals=outvals, change_outputs=change_outputs, - input_amount=inp_amount, psbt_v2=psbtv2, bip67=bip67) + input_amount=inp_amount, psbt_v2=pytestconfig.getoption('psbt2'), + bip67=bip67) start_sign(psbt) txout_explorer(data) diff --git a/testing/test_nfc.py b/testing/test_nfc.py index 73dd1eb9..029ccf08 100644 --- a/testing/test_nfc.py +++ b/testing/test_nfc.py @@ -244,7 +244,7 @@ def try_sign_nfc(cap_story, pick_menu_item, goto_home, need_keypress, for r in range(10): time.sleep(0.1) title, story = cap_story() - if title == 'PSBT Signed': break + if "shared via NFC" in story: break else: assert False, 'timed out' @@ -347,7 +347,7 @@ def test_nfc_after(num_outs, fake_txn, try_sign, nfc_read, need_keypress, cap_story, is_q1, press_nfc, press_cancel): # Read signing result (transaction) over NFC, decode it. psbt = fake_txn(1, num_outs) - orig, result = try_sign(psbt, accept=True, finalize=True) + orig, result = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False) too_big = len(result) > 8000 @@ -356,19 +356,21 @@ def test_nfc_after(num_outs, fake_txn, try_sign, nfc_read, need_keypress, time.sleep(.1) title, story = cap_story() - assert 'TXID' in title, story - txid = a2b_hex(story.split()[0]) - assert f'Press {KEY_NFC if is_q1 else "(3)"}' in story + assert 'TXID' in story, story + txid = a2b_hex(story.split("\n")[3]) + assert f'press {KEY_NFC if is_q1 else "(3)"}' in story press_nfc() time.sleep(.2) if too_big: title, story = cap_story() assert 'is too large' in story + press_cancel() return contents = nfc_read() press_cancel() + press_cancel() #print("contents = " + B2A(contents)) for got in ndef.message_decoder(contents): @@ -482,7 +484,7 @@ def test_nfc_pushtx(num_outs, chain, enable_nfc, settings_set, settings_remove, psbt = fake_txn(2, num_outs) if way == "usb": - _, result = try_sign(psbt, finalize=True) + _, result = try_sign(psbt, finalize=True, exit_export_loop=False) elif way == "sd": ip, result, txid = try_sign_microsd(psbt, finalize=True, nfc_push_tx=True) elif way == "nfc": @@ -501,10 +503,9 @@ def test_nfc_pushtx(num_outs, chain, enable_nfc, settings_set, settings_remove, time.sleep(.1) title, story = cap_story() if way == "usb": - assert title == 'Final TXID' - assert 'to share signed txn' in story + assert 'TXID' in story elif way == "sd": - assert title == "PSBT Signed" + assert ('Updated PSBT' in story) or ('Finalized transaction' in story) else: assert False return diff --git a/testing/test_sign.py b/testing/test_sign.py index 79afed42..8f066995 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -134,7 +134,7 @@ def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec): @pytest.mark.unfinalized def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign, - press_select): + press_select, press_cancel): # measure time to sign a larger txn if is_mark4: # Mk4: expect @@ -169,6 +169,7 @@ def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign, print(" Tx time: %.1f" % tx_time) print("Sign time: %.1f" % ready_time) + press_cancel() if 0: # TODO: attempt to re-create the mega transaction: 5,569 inputs, one out @@ -1211,10 +1212,26 @@ def hist_count(sim_exec): 'import history; RV.write(str(len(history.OutptValueCache.runtime_cache)));')) return doit +@pytest.fixture +def txid_from_export_prompt(cap_story, cap_screen_qr, cap_screen, need_keypress): + def doit(): + time.sleep(.1) + title, story = cap_story() + assert "(6) for QR Code of TXID" in story + need_keypress("6") + time.sleep(.1) + screen_txid = cap_screen().strip().replace("\n", "").replace("~", "") + qr_txid = cap_screen_qr().decode().strip().lower() + assert qr_txid == screen_txid + return qr_txid + + return doit + @pytest.mark.parametrize('num_utxo', [9, 100]) @pytest.mark.parametrize('segwit_in', [False, True]) def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, settings_set, - settings_get, cap_story, sim_exec, hist_count): + settings_get, cap_story, sim_exec, hist_count, + txid_from_export_prompt, press_cancel): # cleanup prev runs, if very first time thru sim_exec('import history; history.OutptValueCache.clear()') @@ -1224,16 +1241,15 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set # make a txn, capture the outputs of that as inputs for another txn psbt = fake_txn(1, num_utxo+3, segwit_in=segwit_in, change_outputs=range(num_utxo+2), outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh']) - _, txn = try_sign(psbt, accept=True, finalize=True) + _, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False) open('debug/funding.psbt', 'wb').write(psbt) num_inp_utxo = (1 if segwit_in else 0) - time.sleep(.1) - title, story = cap_story() - assert 'TXID' in title, story - txid = story.strip().split()[0] + txid = txid_from_export_prompt() + press_cancel() + press_cancel() assert hist_count() in {128, hist_b4+num_utxo+num_inp_utxo} @@ -1273,21 +1289,17 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set @pytest.mark.parametrize('segwit', [False, True]) @pytest.mark.parametrize('num_ins', [1, 17]) -def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind, cap_story): +def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind, cap_story, + txid_from_export_prompt, press_cancel): # verify correct txid for transactions is being calculated xp = dev.master_xpub psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit) - _, txn = try_sign(psbt, accept=True, finalize=True) - - #print('Signed; ' + B2A(txn)) - - time.sleep(.1) - title, story = cap_story() - assert '0' in story - assert 'TXID' in title, story - txid = story.strip().split()[0] + _, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False) + txid = txid_from_export_prompt() + press_cancel() # exit QR + press_cancel() # exit re-export loop if 1: t = CTransaction() @@ -1305,6 +1317,7 @@ def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoin assert decoded['txid'] == txid + @pytest.mark.unfinalized # iff partial=1 @pytest.mark.reexport @pytest.mark.parametrize('encoding', ['binary', 'hex', 'base64']) @@ -1527,36 +1540,36 @@ def test_value_render(dev, units, fake_txn, start_sign, cap_story, settings_set, @pytest.mark.qrcode @pytest.mark.parametrize('num_in', [1,2,3]) @pytest.mark.parametrize('num_out', [1,2,3]) -def test_qr_txn(num_in, num_out, request, fake_txn, try_sign, dev, cap_screen_qr, qr_quality_check, cap_story, need_keypress): - segwit=True +@pytest.mark.parametrize('segwit', [True, False]) +def test_qr_txn(num_in, num_out, segwit, fake_txn, try_sign, dev, cap_screen_qr, + qr_quality_check, cap_story, need_keypress, is_q1, press_cancel): - psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=False) + psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit) - - _, txn = try_sign(psbt, accept=True, finalize=True) - open('debug/last.txn', 'wb').write(txn) - - print("txn len = %d bytes" % len(txn)) + _, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False) + with open('debug/last.txn', 'wb') as f: + f.write(txn) title, story = cap_story() - assert 'QR Code' in story + assert '(6) for QR Code of TXID' in story - if 1: - # check TXID qr code - need_keypress('1') + # check TXID qr code + need_keypress('6') + qr = cap_screen_qr().decode() + press_cancel() - qr = cap_screen_qr().decode() + t = CTransaction() + t.deserialize(BytesIO(txn)) + assert t.txid().hex() == qr.lower() - t = CTransaction() - t.deserialize(BytesIO(txn)) - assert t.txid().hex() == qr.lower() - - else: - # TODO: QR for txn itself yet - need_keypress('2') + if is_q1: + need_keypress(KEY_QR) qr = cap_screen_qr().decode() assert qr.lower() == txn.hex() + press_cancel() + + press_cancel() def test_missing_keypaths(dev, try_sign, fake_txn): @@ -1755,7 +1768,8 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p b"$$$$$$$$$$$$$$ Bitcoin", b"\xeb\x97\xf7\xb7\xf78\x9a';\x90F_\xfc\xe2b\xa4\x93)\xea\xac\xacR\xff\x9c\xbe\x1c\xf1\xad\xe9!\xee\xd9t1\x1f\x92\x83\x97\xb3\x98/\xff\xc8\xff\xc1\xc0\xdd\x1et\x00L\x13\xe0\xe3\x90\xe4\xd4\xf2x:\xf7Ab\x04\x91\x1e\xa8R\x92\xd3\x96OK\xc6I\x06\x9e\xce=\xb3", ]) -def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, bitcoind, start_sign, end_sign, cap_story): +def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, bitcoind, + start_sign, end_sign, cap_story): cc = bitcoind_d_sim_watch dest_address = cc.getnewaddress() bitcoind.supply_wallet.generatetoaddress(101, dest_address) @@ -1864,12 +1878,19 @@ def test_duplicate_unknow_values_in_psbt(dev, start_sign, end_sign, fake_txn): @pytest.fixture def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev, - bitcoind, bitcoind_d_dev_watch, settings_set, finalize_v2_v0_convert): + bitcoind, bitcoind_d_dev_watch, settings_set, + finalize_v2_v0_convert, pytestconfig): def doit(addr_fmt, sighash, num_inputs=2, num_outputs=2, consolidation=False, sh_checks=False, - psbt_v2=False, tx_check=True): + psbt_v2=None, tx_check=True): from decimal import Decimal, ROUND_DOWN + if psbt_v2 is None: + # anything passed directly to this function overrides + # pytest flag --psbt2 - only care about pytest flag + # if psbt_v2 is not specified (None) + psbt_v2 = pytestconfig.getoption('psbt2') + if dev.is_simulator: # if running against real HW you need to set CC to correct sighshchk mode # Below test need to run with sighshchk disabled: @@ -2044,38 +2065,34 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev, return doit +# TODO Locktime test MUST be run with --psbt2 flag on and off +# pytest test_sign.py -k locktime {--psbt2,} @pytest.mark.bitcoind @pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"]) @pytest.mark.parametrize("sighash", [sh for sh in SIGHASH_MAP if sh != 'ALL']) @pytest.mark.parametrize("num_outs", [1, 3, 5]) @pytest.mark.parametrize("num_ins", [2, 5]) -@pytest.mark.parametrize("psbt_v2", [True, False]) -def test_sighash_same(addr_fmt, sighash, num_ins, num_outs, psbt_v2, _test_single_sig_sighash): +def test_sighash_same(addr_fmt, sighash, num_ins, num_outs, _test_single_sig_sighash): # sighash is the same among all inputs - _test_single_sig_sighash(addr_fmt, [sighash], num_inputs=num_ins, num_outputs=num_outs, - psbt_v2=psbt_v2) + _test_single_sig_sighash(addr_fmt, [sighash], num_inputs=num_ins, num_outputs=num_outs) @pytest.mark.bitcoind @pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"]) @pytest.mark.parametrize("sighash", list(itertools.combinations(SIGHASH_MAP.keys(), 2))) @pytest.mark.parametrize("num_outs", [2, 3, 5]) -@pytest.mark.parametrize("psbt_v2", [True, False]) -def test_sighash_different(addr_fmt, sighash, num_outs, psbt_v2, _test_single_sig_sighash): +def test_sighash_different(addr_fmt, sighash, num_outs, _test_single_sig_sighash): # sighash differ among all inputs - _test_single_sig_sighash(addr_fmt, sighash, num_inputs=2, num_outputs=num_outs, - psbt_v2=psbt_v2) + _test_single_sig_sighash(addr_fmt, sighash, num_inputs=2, num_outputs=num_outs) @pytest.mark.bitcoind @pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"]) @pytest.mark.parametrize("num_outs", [5, 8]) -@pytest.mark.parametrize("psbt_v2", [True, False]) -def test_sighash_fullmix(addr_fmt, num_outs, psbt_v2, _test_single_sig_sighash): +def test_sighash_fullmix(addr_fmt, num_outs, _test_single_sig_sighash): # tx with 6 inputs representing all possible sighashes - _test_single_sig_sighash(addr_fmt, tuple(SIGHASH_MAP.keys()), num_inputs=6, - num_outputs=num_outs, psbt_v2=psbt_v2) + _test_single_sig_sighash(addr_fmt, tuple(SIGHASH_MAP.keys()), num_inputs=6, num_outputs=num_outs) @pytest.mark.bitcoind @@ -2207,7 +2224,7 @@ def test_batch_sign(num_tx, ui_path, action, fake_txn, need_keypress, time.sleep(.5) title, story = cap_story() assert "-signed.psbt" in story - press_select() + press_cancel() time.sleep(.5) title, story = cap_story() @@ -2337,7 +2354,7 @@ def test_psbt_v2_global_quantities(way, fake_txn, start_sign, end_sign, cap_stor ]) def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, microsd_path, cap_story, goto_home, press_select, - pick_menu_item, bitcoind, locktime): + pick_menu_item, bitcoind, locktime, file_tx_signing_done): use_regtest() sim = bitcoind_d_sim_watch addr = sim.getnewaddress() @@ -2366,12 +2383,15 @@ def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, psbt_fname = "locktime.psbt" with open(microsd_path(psbt_fname), "w") as f: f.write(psbt) + goto_home() pick_menu_item('Ready To Sign') time.sleep(0.1) - pick_menu_item(psbt_fname) - time.sleep(0.1) title, story = cap_story() + if 'OK TO SEND' not in title: + pick_menu_item(psbt_fname) + time.sleep(0.1) + title, story = cap_story() assert "WARNING" not in story if locktime != 0: @@ -2390,18 +2410,10 @@ def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, press_select() # confirm signing time.sleep(0.1) title, story = cap_story() - assert title == 'PSBT Signed' assert "Updated PSBT is:" in story assert "Finalized transaction (ready for broadcast)" in story assert "TXID" in story - split_story = story.split("\n\n") - story_txid = split_story[-2].split("\n")[-1] - signed_psbt_fname = split_story[1] - with open(microsd_path(signed_psbt_fname), "r") as f: - signed_psbt = f.read().strip() - signed_txn_fname = split_story[3] - with open(microsd_path(signed_txn_fname), "r") as f: - signed_txn = f.read().strip() + signed_psbt, signed_txn, story_txid = file_tx_signing_done(story) assert signed_psbt != psbt finalize_res = sim.finalizepsbt(signed_psbt) bitcoind_signed_txn = finalize_res["hex"] @@ -2426,7 +2438,7 @@ def test_locktime_ux(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, microsd_path, cap_story, goto_home, press_select, pick_menu_item, - bitcoind, num_ins, differ): + bitcoind, num_ins, differ, file_tx_signing_done): if differ and (sequence == 0): # this case makes no sense return @@ -2475,12 +2487,15 @@ def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitco psbt_fname = "rtl-blockheight.psbt" with open(microsd_path(psbt_fname), "w") as f: f.write(psbt) + goto_home() pick_menu_item('Ready To Sign') time.sleep(0.1) - pick_menu_item(psbt_fname) - time.sleep(0.1) title, story = cap_story() + if 'OK TO SEND' not in title: + pick_menu_item(psbt_fname) + time.sleep(0.1) + title, story = cap_story() assert "WARNING" not in story if sequence: @@ -2502,18 +2517,11 @@ def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitco press_select() # confirm signing time.sleep(0.1) title, story = cap_story() - assert title == 'PSBT Signed' assert "Updated PSBT is:" in story assert "Finalized transaction (ready for broadcast)" in story assert "TXID" in story - split_story = story.split("\n\n") - story_txid = split_story[-2].split("\n")[-1] - signed_psbt_fname = split_story[1] - with open(microsd_path(signed_psbt_fname), "r") as f: - signed_psbt = f.read().strip() - signed_txn_fname = split_story[3] - with open(microsd_path(signed_txn_fname), "r") as f: - signed_txn = f.read().strip() + press_select() # exit saved story + signed_psbt, signed_txn, story_txid = file_tx_signing_done(story) assert signed_psbt != psbt finalize_res = sim.finalizepsbt(signed_psbt) bitcoind_signed_txn = finalize_res["hex"] @@ -2538,13 +2546,13 @@ def test_nsequence_blockheight_relative_locktime_ux(sequence, use_regtest, bitco @pytest.mark.bitcoind -@pytest.mark.veryslow @pytest.mark.parametrize("num_ins", [1, 4, 11]) @pytest.mark.parametrize("differ", [True, False]) @pytest.mark.parametrize("seconds", [512, 10000, 1000000, 33554431]) def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind_d_sim_watch, start_sign, microsd_path, cap_story, goto_home, press_select, - pick_menu_item, bitcoind, end_sign, num_ins, differ): + pick_menu_item, bitcoind, end_sign, num_ins, differ, + file_tx_signing_done): sequence = SEQUENCE_LOCKTIME_TYPE_FLAG | (seconds >> 9) use_regtest() sim = bitcoind_d_sim_watch @@ -2587,12 +2595,15 @@ def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind psbt_fname = "rtl-time.psbt" with open(microsd_path(psbt_fname), "w") as f: f.write(psbt) + goto_home() - pick_menu_item('Ready To Sign') - time.sleep(0.1) - pick_menu_item(psbt_fname) + pick_menu_item("Ready To Sign") time.sleep(0.1) title, story = cap_story() + if 'OK TO SEND' not in title: + pick_menu_item(psbt_fname) + time.sleep(0.1) + title, story = cap_story() assert "WARNING" not in story assert "TX LOCKTIMES" in story @@ -2613,18 +2624,10 @@ def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind press_select() # confirm signing time.sleep(0.1) title, story = cap_story() - assert title == 'PSBT Signed' assert "Updated PSBT is:" in story assert "Finalized transaction (ready for broadcast)" in story assert "TXID" in story - split_story = story.split("\n\n") - story_txid = split_story[-2].split("\n")[-1] - signed_psbt_fname = split_story[1] - with open(microsd_path(signed_psbt_fname), "r") as f: - signed_psbt = f.read().strip() - signed_txn_fname = split_story[3] - with open(microsd_path(signed_txn_fname), "r") as f: - signed_txn = f.read().strip() + signed_psbt, signed_txn, story_txid = file_tx_signing_done(story) assert signed_psbt != psbt finalize_res = sim.finalizepsbt(signed_psbt) bitcoind_signed_txn = finalize_res["hex"] @@ -2650,12 +2653,11 @@ def test_nsequence_timebased_relative_locktime_ux(seconds, use_regtest, bitcoind @pytest.mark.bitcoind -@pytest.mark.veryslow @pytest.mark.parametrize("abs_lock", [True, False]) @pytest.mark.parametrize("num_rtl", [(2,3),(4,7),(8,3),(6,7)]) -def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign, - microsd_path, cap_story, goto_home, press_select, - pick_menu_item, bitcoind, end_sign, abs_lock): +def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign, microsd_path, + cap_story, goto_home, press_select, pick_menu_item, bitcoind, end_sign, + abs_lock, file_tx_signing_done): tb, bb = num_rtl num_ins = tb + bb sequence = SEQUENCE_LOCKTIME_TYPE_FLAG | (512 >> 9) @@ -2703,9 +2705,11 @@ def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign, goto_home() pick_menu_item('Ready To Sign') time.sleep(0.1) - pick_menu_item(psbt_fname) - time.sleep(0.1) title, story = cap_story() + if 'OK TO SEND' not in title: + pick_menu_item(psbt_fname) + time.sleep(0.1) + title, story = cap_story() assert "WARNING" not in story assert "TX LOCKTIMES" in story @@ -2726,18 +2730,10 @@ def test_mixed_locktimes(num_rtl, use_regtest, bitcoind_d_sim_watch, start_sign, press_select() # confirm signing time.sleep(0.1) title, story = cap_story() - assert title == 'PSBT Signed' assert "Updated PSBT is:" in story assert "Finalized transaction (ready for broadcast)" in story assert "TXID" in story - split_story = story.split("\n\n") - story_txid = split_story[-2].split("\n")[-1] - signed_psbt_fname = split_story[1] - with open(microsd_path(signed_psbt_fname), "r") as f: - signed_psbt = f.read().strip() - signed_txn_fname = split_story[3] - with open(microsd_path(signed_txn_fname), "r") as f: - signed_txn = f.read().strip() + signed_psbt, signed_txn, story_txid = file_tx_signing_done(story) assert signed_psbt != psbt finalize_res = sim.finalizepsbt(signed_psbt) bitcoind_signed_txn = finalize_res["hex"] @@ -2791,55 +2787,55 @@ def random_nLockTime_test_cases(num=10): ]) def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest, bitcoind_d_sim_watch, nLockTime): - # - works on simulator and connected USB real-device - nLockTime, expect_ux = nLockTime - num_ins = 10 - use_regtest() - bitcoind_d_sim_watch.keypoolrefill(20) - for i in range(num_ins): - addr = bitcoind_d_sim_watch.getnewaddress() - bitcoind.supply_wallet.sendtoaddress(addr, 1) + # - works on simulator and connected USB real-device + nLockTime, expect_ux = nLockTime + num_ins = 10 + use_regtest() + bitcoind_d_sim_watch.keypoolrefill(20) + for i in range(num_ins): + addr = bitcoind_d_sim_watch.getnewaddress() + bitcoind.supply_wallet.sendtoaddress(addr, 1) - bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) - dest_addr = bitcoind_d_sim_watch.getnewaddress() # self-spend - utxos = bitcoind_d_sim_watch.listunspent() - assert len(utxos) == num_ins + bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) + dest_addr = bitcoind_d_sim_watch.getnewaddress() # self-spend + utxos = bitcoind_d_sim_watch.listunspent() + assert len(utxos) == num_ins - ins = [] - for i, utxo in enumerate(utxos): - if i % 2 == 0: - nSeq = (SEQUENCE_LOCKTIME_TYPE_FLAG | i) - else: - confirmations = utxo["confirmations"] - nSeq = confirmations + (20*i) + ins = [] + for i, utxo in enumerate(utxos): + if i % 2 == 0: + nSeq = (SEQUENCE_LOCKTIME_TYPE_FLAG | i) + else: + confirmations = utxo["confirmations"] + nSeq = confirmations + (20*i) - inp = { - "txid": utxo["txid"], - "vout": utxo["vout"], - "sequence": nSeq, - } - ins.append(inp) + inp = { + "txid": utxo["txid"], + "vout": utxo["vout"], + "sequence": nSeq, + } + ins.append(inp) - psbt_resp = bitcoind_d_sim_watch.walletcreatefundedpsbt( - ins, [{dest_addr: (num_ins - 0.1)}], - nLockTime, {"fee_rate": 20} - ) - psbt = base64.b64decode(psbt_resp.get("psbt")) + psbt_resp = bitcoind_d_sim_watch.walletcreatefundedpsbt( + ins, [{dest_addr: (num_ins - 0.1)}], + nLockTime, {"fee_rate": 20} + ) + psbt = base64.b64decode(psbt_resp.get("psbt")) - open('debug/locktimes.psbt', 'wb').write(psbt) + open('debug/locktimes.psbt', 'wb').write(psbt) - # should be able to sign, but get warning + # should be able to sign, but get warning - # use new feature to have Coldcard return the 'visualization' of transaction - start_sign(psbt, False, stxn_flags=STXN_VISUALIZE) - story = end_sign(accept=None, expect_txn=False) + # use new feature to have Coldcard return the 'visualization' of transaction + start_sign(psbt, False, stxn_flags=STXN_VISUALIZE) + story = end_sign(accept=None, expect_txn=False) - story = story.decode('ascii') - assert datetime.datetime.utcfromtimestamp(nLockTime).strftime("%Y-%m-%d %H:%M:%S") == expect_ux - assert f"Abs Locktime: This tx can only be spent after {expect_ux} UTC (MTP)" in story - assert "Block height RTL: 5 inputs have relative block height timelock" in story - # when i=0 in loop time based RTL is zero - assert "Time-based RTL: 4 inputs have relative time-based timelock" in story + story = story.decode('ascii') + assert datetime.datetime.utcfromtimestamp(nLockTime).strftime("%Y-%m-%d %H:%M:%S") == expect_ux + assert f"Abs Locktime: This tx can only be spent after {expect_ux} UTC (MTP)" in story + assert "Block height RTL: 5 inputs have relative block height timelock" in story + # when i=0 in loop time based RTL is zero + assert "Time-based RTL: 4 inputs have relative time-based timelock" in story @pytest.mark.parametrize('in_out', [(4,1),(2,2),(2,1)]) @@ -2947,7 +2943,6 @@ def test_sorting_outputs_by_size(fake_txn, start_sign, cap_story, use_testnet, press_cancel() -@pytest.mark.parametrize("psbtv2", [True, False]) @pytest.mark.parametrize("chain", ["BTC", "XTN"]) @pytest.mark.parametrize("data", [ # (out_style, amount, is_change) @@ -2956,8 +2951,9 @@ def test_sorting_outputs_by_size(fake_txn, start_sign, cap_story, use_testnet, [("p2pkh", 1000000, 1)] * 11 + [("p2wpkh", 50000000, 0)] * 16, [("p2pkh", 1000000, 1), ("p2wpkh", 50000000, 0), ("p2wpkh-p2sh", 800000, 1)] * 11, ]) -def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign, - settings_set, txout_explorer, cap_story): +def test_txout_explorer(chain, data, fake_txn, start_sign, settings_set, txout_explorer, + cap_story, pytestconfig): + # TODO This test MUST be run with --psbt2 flag on and off settings_set("chain", chain) outstyles = [] outvals = [] @@ -2972,7 +2968,7 @@ def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign, inp_amount = sum(outvals) + 100000 # 100k sat fee psbt = fake_txn(1, len(data), segwit_in=True, outstyles=outstyles, outvals=outvals, change_outputs=change_outputs, - psbt_v2=psbtv2, input_amount=inp_amount) + psbt_v2=pytestconfig.getoption('psbt2'), input_amount=inp_amount) start_sign(psbt) txout_explorer(data, chain) diff --git a/testing/test_teleport.py b/testing/test_teleport.py index 98131a51..17c56f1e 100644 --- a/testing/test_teleport.py +++ b/testing/test_teleport.py @@ -82,7 +82,7 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s url = nfc_read_url().replace('%24', '$') - assert url.startswith('https://keyteleport.com#') + assert url.startswith('https://keyteleport.com/#') nfc_data = url.rsplit('#')[1] assert nfc_data.startswith(f'B$2{tt_code}0100') @@ -401,6 +401,7 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite # now, send that back rx_complete(data, pw, expect_fail=True) + time.sleep(.1) title, body = cap_story() assert title == 'Teleport Fail' @@ -416,11 +417,11 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite @pytest.mark.parametrize('incl_xpubs', [ False ]) 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, ndef_parse_txn_psbt, - press_nfc, nfc_read, settings_get, settings_set): + cap_menu, pick_menu_item, grab_payload, rx_complete, press_select, + ndef_parse_txn_psbt, press_nfc, nfc_read, settings_get, settings_set, + txid_from_export_prompt): # IMPORTANT: won't 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) @@ -440,13 +441,15 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d cur_wallet = 0 my_xfp = select_wallet(cur_wallet) - _, updated = try_sign(psbt, accept_ms_import=incl_xpubs) + _, updated = try_sign(psbt, accept_ms_import=incl_xpubs, exit_export_loop=False) 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 + assert title == "PSBT Signed" + assert '(T) to use Key Teleport to send PSBT to other co-signers' in body + + num_sigs_needed = M - 1 # we have already signed with first at this point while 1: # expect: a menu of other signers to pick from @@ -483,6 +486,13 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d nn = xfp2str(next_xfp) open(f'debug/next_qr_{nn}.txt', 'wt').write(f'{nn}\n\n{pw}\n\n{data}') + time.sleep(.1) + title, story = cap_story() + assert title == 'Sent by Teleport' + s, aux = ("", "is") if num_sigs_needed == 1 else ("s", "are") + msg = "%d more signature%s %s still required." % (num_sigs_needed, s, aux) + assert msg in story + # switch personalities, and try to read that QR new_xfp = select_wallet(idx) assert new_xfp == next_xfp @@ -499,14 +509,14 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d time.sleep(.25) title, body = cap_story() - if title != 'Teleport PSBT?': + if 'Finalized TX' in body: break - assert title == 'Teleport PSBT?' - assert 'more signatures' in body + assert '(T) to use Key Teleport to send PSBT to other co-signers' in body + num_sigs_needed -= 1 - assert title == 'Final TXID' - txid = body.split()[0] + txid = txid_from_export_prompt() + press_select() # exit QR # share signed txn via low-level NFC press_nfc() @@ -519,11 +529,11 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d assert got_txn -def test_teleport_big_ms(make_myself_wallet, clear_ms, - fake_ms_txn, try_sign, cap_story, need_keypress, - cap_menu, pick_menu_item, grab_payload, rx_complete, press_select, ndef_parse_txn_psbt, - set_master_key, - goto_home, press_nfc, nfc_read, settings_get, settings_set, open_microsd, import_ms_wallet): +def test_teleport_big_ms(make_myself_wallet, clear_ms, fake_ms_txn, try_sign, cap_story, + need_keypress, cap_menu, pick_menu_item, grab_payload, rx_complete, + press_select, ndef_parse_txn_psbt, set_master_key, goto_home, press_nfc, + nfc_read, settings_get, settings_set, open_microsd, import_ms_wallet, + press_cancel): # define lots of wallets and do teleport from SD disk @@ -567,8 +577,8 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms, # have 1 sigs now, need one more via teleport title, body = cap_story() - assert title == 'Teleport PSBT?' - need_keypress('t') + assert '(T) to use Key Teleport to send PSBT to other co-signers' in body + need_keypress('t') # pick another one randomly m = cap_menu() @@ -600,8 +610,8 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms, time.sleep(.25) title, body = cap_story() - - assert title == 'Final TXID' + assert 'Finalized TX' in body + press_cancel() @pytest.mark.manual diff --git a/testing/test_vdisk.py b/testing/test_vdisk.py index ce023e8c..1a44ee2c 100644 --- a/testing/test_vdisk.py +++ b/testing/test_vdisk.py @@ -94,7 +94,6 @@ def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, pre # wait for it to finish signing time.sleep(.1) title, story = cap_story() - assert "PSBT Signed" in title split_story = story.split("\n\n") result_fn = split_story[1] @@ -103,11 +102,7 @@ def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, pre if expect_finalize: result_txn = split_story[3] result_txid = split_story[4].split("\n")[-1] - reexport_msg = split_story[5] - else: - reexport_msg = split_story[2] - assert "Press (0) to save again by another method." == reexport_msg got_psbt = None got_txn = None txid, got_txid = None, None