Merge pull request #469 from scgbckbone/edge_feb2025

Edge feb2025
This commit is contained in:
doc-hex 2025-02-19 14:28:31 -05:00 committed by GitHub
commit 0427af8caa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 2583 additions and 1009 deletions

View File

@ -22,7 +22,7 @@ This one more solution for your game-theory arsenal.
- *Q*: I'm lazy, can I do this to my Existing Seed?
- *A*: Yes. You can split the words you have already in your Coldcard, making
2, 3 or 4 new SEEDPLATES. You could also any number of existing SEEDPLATES
2, 3 or 4 new SEEDPLATES. You could also use any number of existing SEEDPLATES
you have, and combine them to make a new random wallet that is the XOR of
their values. Effectively that makes a new random wallet.

View File

@ -25,7 +25,7 @@ MUST be generated with above-mentoned methods to be considered change.
## Provably unspendable internal key
There are few methods to provide/generate provably unspendable internal key, if users wish to only use tapscript script path.
There are 2 methods to provide provably unspendable internal key, if users wish to only use tapscript script path.
1. **(recommended)** Origin-less extended key serialization with H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs) as BIP-32 key and random chaincode.
@ -35,6 +35,7 @@ There are few methods to provide/generate provably unspendable internal key, if
`tr(unspend(77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76)/<0:1>/*, sortedmulti_a(2,@0,@1))`
### Below option were deprecated in version 6.3.5X & 6.3.5QX
3. use **static** provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs).
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`

@ -1 +1 @@
Subproject commit 97d35f058f504a354fc6df79a8b3db5c91862501
Subproject commit 4107246f8a080807b62c3b4838e71e812ea68b6f

View File

@ -74,4 +74,9 @@ special_chars = dict(small=[
x x x x x
'''),
# thin space
('\u2009', dict(y=0, w=5), '''\
'''),
])

View File

@ -4,53 +4,64 @@ This lists the changes in the most recent firmware, for each hardware platform.
# Shared Improvements - Both Mk4 and Q
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- New signing features:
- Sign message from note text, or password note
- JSON message signing. Use JSON object to pass data to sign in form
`{"msg":"<required msg>","subpath":"<optional sp>","addr_fmt": "<optional af>"}`
- Sign message with key resulting from positive ownership check. Press (0) and
enter or scan message text to be signed.
- Sign message with key selected from Address Explorer Custom Path menu. Press (2) and
enter or scan message text to be signed.
- Enhancement: New address display format improves address verification on screen (groups of 4).
- Deltamode enhancements:
- Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Catch more DeltaMode cases in XOR submenus. Thanks [@dmonakhov](https://github.com/dmonakhov)
- Enhancement: Add ability to switch between BIP-32 xpub, and obsolete SLIP-132 format
in `Export XPUB`
- Enhancement: Use the fact that master seed cannot be used as ephemeral seed, to show message
about successful master seed verification.
- Enhancement: Allow devs to override backup password.
- Enhancement: Add option to show/export full multisg addresses without censorship. Enable
in `Settings > Multisig Wallets > Full Address View`.
- Enhancement: If derivation path is omitted during message signing, derivation path
default is no longer root (m), instead it is based on requested address format
(`m/44h/0h/0h/0/0` for p2pkh, and `m/84h/0h/0h/0/0` for p2wpkh). Conversely,
if address format is not provided but subpath derivation starts with:
`m/84h/...` or `m/49h/...`, then p2wpkh or p2sh-p2wpkh respectively, is used.
- Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence.
On Q, result is blank screen, on Mk4, result is three-dots screen.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode.
- Bugfix: Bless Firmware causes hanging progress bar.
- Bugfix: Prevent yikes in ownership search.
- Bugfix: Factory-disabled NFC was not recognized correctly.
- Bugfix: Be more robust about flash filesystem holding the settings.
- Bugfix: Do not include sighash in PSBT input data, if sighash value is `SIGHASH_ALL`.
- Bugfix: Allow import of multisig descriptor with root (m) keys in it.
Thanks [@turkycat](https://github.com/turkycat)
- Change: Do not purge settings of current active tmp seed when deleting it from Seed Vault.
- Change: Rename Testnet3 -> Testnet4 (all parameters unchanged).
# Mk4 Specific Changes
## 5.4.0 - 2024-09-12
## 5.4.1 - 2024-02-13
- Shared enhancements and fixes listed above.
- Bugfix: Correct intermittent card inserted/not inserted detection error.
- Enhancement: Export single sig descriptor with simple QR.
# Q Specific Changes
## 1.3.0Q - 2024-09-12
## 1.3.1Q - 2024-02-13
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
- New Feature: Input backup password from QR scan.
- New Feature: (BB)QR file share of arbitrary files.
- New Feature: `Create Airgapped` now works with BBQRs.
- Change: Default brightness (on battery) adjusted from 80% to 95%.
- Bugfix: Properly clear LCD screen after BBQR is shown.
- Bugfix: Writing to empty slot B caused broken card reader.
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
- Bugfix: Stop re-wording UX stories using a regular expression.
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
- New Feature: Verify Signed RFC messages via BBQr
- New Feature: Sign message from QR scan (format has to be JSON)
- Enhancement: Sign/Verify Address in Sparrow via QR
- Enhancement: Sign scanned Simple Text by pressing (0). Next screen query information
about which key to use.
- Enhancement: Add option to "Sort By Title" in Secure Notes and Passwords. Thanks to
[@MTRitchey](https://x.com/MTRitchey) for suggestion.
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.

View File

@ -6,38 +6,28 @@
- This preview version of firmware has not yet been qualified
- and tested to the same standard as normal Coinkite products.
- It is recommended only for developers and early adopters
- for experimental use. DO NOT use for large Bitcoin amounts.
- for experimental use.
```
This lists the changes in the most recent EDGE firmware, for each hardware platform.
# Shared Improvements - Both Mk4 and Q
- Bugfix: Complex miniscript wallets with keys in policy that are not in strictly ascending order were incorrectly filled
upon load from settings. All users on versions `6.2.2X`+ needs to update.
- Bugfix: Single key miniscript descriptor support
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode
- Bugfix: Bless Firmware causes hanging progress bar
- Bugfix: Prevent yikes in ownership search
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
Change: Allow origin-less extended keys in multisig & miniscript descriptors
Change: Static internal keys disallowed - all keys need to be ranged extended keys
# Mk4 Specific Changes
## 6.3.4X - 2024-07-04
## 6.3.5X - 2024-07-04
- all updates from `5.4.0`
- Enhancement: Export single sig descriptor with simple QR
- all updates from `5.4.1`
# Q Specific Changes
## 6.3.4QX - 2024-07-04
## 6.3.5QX - 2024-07-04
- all updates from version `1.3.0Q`
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
- all updates from version `1.3.1Q`
# Release History

View File

@ -7,6 +7,30 @@
- for experimental use. DO NOT use for large Bitcoin amounts.
```
# 6.3.4X & 6.3.4QX Shared Improvements - Both Mk4 and Q
- Bugfix: Complex miniscript wallets with keys in policy that are not in strictly ascending order were incorrectly filled
upon load from settings. All users on versions `6.2.2X`+ needs to update.
- Bugfix: Single key miniscript descriptor support
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode
- Bugfix: Bless Firmware causes hanging progress bar
- Bugfix: Prevent yikes in ownership search
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
# Mk4 Specific Changes
- all updates from `5.4.0`
- Enhancement: Export single sig descriptor with simple QR
# Q Specific Changes
- all updates from version `1.3.0Q`
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
## 6.3.3X & 6.3.3QX Shared Improvements - Both Mk4 and Q (2024-07-04)
- New Feature: Ranged provably unspendable keys and `unspend(` support for Taproot descriptors

View File

@ -1,5 +1,34 @@
*See ChangeLog.md for more recent changes, these are historic versions*
## 5.4.0 - 2024-09-12
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- Shared enhancements and fixes listed above.
- Bugfix: Correct intermittent card inserted/not inserted detection error.
## 5.3.3 - 2024-07-05

View File

@ -1,6 +1,44 @@
*See ChangeLog.md for more recent changes, these are historic versions*
## 1.3.0Q - 2024-09-12
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
- New Feature: Input backup password from QR scan.
- New Feature: (BB)QR file share of arbitrary files.
- New Feature: `Create Airgapped` now works with BBQRs.
- Change: Default brightness (on battery) adjusted from 80% to 95%.
- Bugfix: Properly clear LCD screen after BBQR is shown.
- Bugfix: Writing to empty slot B caused broken card reader.
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
- Bugfix: Stop re-wording UX stories using a regular expression.
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
## 1.2.3Q - 2024-07-05
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD

View File

@ -4,25 +4,18 @@ This lists the new changes that have not yet been published in a normal release.
# Shared Improvements - Both Mk4 and Q
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence.
On Q, result is blank screen, on Mk4, result is three-dots screen.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode
- Bugfix: Bless Firmware causes hanging progress bar
- Bugfix: Prevent yikes in ownership search
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
- tbd
# Mk4 Specific Changes
## 5.4.1 - 2024-??-??
## 5.4.2 - 2024-03-??
- Enhancement: Export single sig descriptor with simple QR
- tbd
# Q Specific Changes
## 1.3.1Q - 2024-??-??
## 1.3.2Q - 2024-03-??
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
- tbd

View File

@ -2,11 +2,19 @@
Hash: SHA256
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
97107b5be1c8b65efa4bd36b7d1798e4ed15917861bd2d40784d66302a61d335 Next-ChangeLog.md
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
006898c20edc46bc642e3bd3b54ee72c22e858a564a31de6d1e90245fa86ff6d Next-ChangeLog.md
b6015f2f807bc78b6063ed6c12a12a47579a81a68f954cfc2e542e7ac6c02c0e History-Q.md
05228d2c59135c3fe251d877b519bec65f929ecf0aac8b727622359014236568 History-Mk4.md
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce ChangeLog.md
45cd0478996bb9da77075846122b8ba732b9b34dbbae0d12cb85ad0d931d40fc ChangeLog.md
eb750a4f095eacc6133b2c8b38fe0738a22b2496a6cdf423ca865acde8c9bc4e 2025-02-13T1415-v5.4.1-mk4-coldcard.dfu
4236453fea241fe044a462a560d8b42df43e560683110306a2714a2ef561eac5 2025-02-13T1415-v5.4.1-mk4-coldcard-factory.dfu
2e1aad0a7a3ceb84db34322b54855a0c5496699e46e53606bfa443fcc992adec 2025-02-13T1413-v1.3.1Q-q1-coldcard.dfu
e43932d04bf782f7b9ba218b54f29b9cd361b83ac3aadff9722714bca1ab7ee9 2025-02-13T1413-v1.3.1Q-q1-coldcard-factory.dfu
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
73f31fbcb064a6b763d50852aafcdff01d7ec72906b5cb0af6cf28328fd80a89 2024-12-18T1413-v6.3.4X-mk4-coldcard-factory.dfu
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
7e284bcead1f9c2f468230a588ddf62064014682772a552d05f453d91d55b6ae 2024-12-18T1407-v6.3.4QX-q1-coldcard-factory.dfu
237cfcb3fdf9217550eae1d9ea6fc828c1c8d09470bd60c9f72f9b00a3bb2d11 2024-09-12T1734-v5.4.0-mk4-coldcard.dfu
6d1178f07d543e1777dbbdca41d872b00ca9c40e0c0c1ffb8ef96e19c51daa52 2024-09-12T1734-v5.4.0-mk4-coldcard-factory.dfu
d840fa4e83ebc7b0f961f30f68d795bed61271e2314dda4ab0eb0b8bfe7192f4 2024-09-12T1733-v1.3.0Q-q1-coldcard.dfu
@ -94,12 +102,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmbjJicACgkQo6MbrVoq
WxAnMwf/e2kR1aK6AJiriRa1n3XDomw8ivaUQXUApmK0kawBhVBDLKw5aa3lvTcS
dg80wnenzNdE/QxctL+FkaZzKYsKbFpstkBEbZKcgbHVcinypKJJfICrhIBVVyZw
wdhJMGOLEyWMysqfaYMtYJQPkg5nIn0rRxn4yWXIeXAQLcFgdlWzVykqfGZW1xYr
CcVvxMqufXfc6c5aRFQzBO/YVHiRYzvK1NGDPztJEjXYU3zxnExAZFxk0vgpxvE3
CahKfSSTNv54u4CTLxYCdHPRq9OM6yL/w3OUyUQFklCizk2PjrObsJQW4szbbjlx
r7+587Pc5cpJCZn73Q0Y5/SWgnqm4g==
=/h9F
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmet/nAACgkQo6MbrVoq
WxC0Pwf/Tguk32raFEq/k0Ai0XYJE8wUs89Wy7V9JRc2gmYPSF39Qv+SePE0Cajn
AkFDepAFrxjF8sanNqR1g0RmXltncmJCOlDf/CmOt0MxeL3r1jxTeXuCpOH5qHcF
QBsWMjA39kv5DtZ4g6j6qXEDfiHQVBSDujK6Xgk6Gj9STGglJZVmwnYWuhMw/7MC
qw5MQ3IsJEXBu9G2eTqH4SEdPdgbmv1Zo/9OKLe7uXKcUo1BWL7jCBONxW1fAAkd
8YMhOAhhv99/B015LZjz0V1aPo2eMQqAq9NMNzCCEwN+RvwckkvyO0l5iIfSozdN
FWZT4Wr6NZcI0F5kLKjnJTCBzHQvLQ==
=/XFv
-----END PGP SIGNATURE-----

View File

@ -4,14 +4,14 @@
#
# Every function here is called directly by a menu item. They should all be async.
#
import ckcc, pyb, version, uasyncio, sys, uos
import ckcc, pyb, version, uasyncio, sys, uos, chains
from uhashlib import sha256
from uasyncio import sleep_ms
from ubinascii import hexlify as b2a_hex
from utils import imported, problem_file_line, get_filesize, encode_seed_qr
from utils import xfp2str, B2A, addr_fmt_label, txid_from_fname
from utils import xfp2str, B2A, txid_from_fname
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words
from export import make_json_wallet, make_summary_file, make_descriptor_wallet_export
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet
@ -579,7 +579,7 @@ async def clear_seed(*a):
if not await ux_confirm('Wipe seed words and reset wallet. '
'All funds will be lost. '
'You better have a backup of the seed words.'
'You better have a backup of the seed words. '
'All settings like multisig wallets are also wiped. '
'Saved temporary seed settings and Seed Vault are lost.'):
return await ux_aborted()
@ -603,7 +603,6 @@ consequences.''', escape='4')
def render_master_secrets(mode, raw, node):
# Render list of words, or XPRV / master secret to text.
import stash, chains
from ux import ux_render_words
c = chains.current_chain()
qr_alnum = False
@ -874,12 +873,10 @@ async def start_login_sequence():
# Version warning before HSM is offered
if version.is_edge and not ckcc.is_simulator():
await ux_show_story(
"This firmware version is qualified for use with wallets (such as
AnchorWatch, Liana, etc) that keep redundant key schemas for recovery
independant of COLDCARD. We support the very latest Bitcoin innovations
in the Edge Version."
title="Edge Version")
await ux_show_story("This firmware version is qualified for use with wallets (such as"
" AnchorWatch) that keep redundant key schemas for recovery"
" independent of COLDCARD. We support the very latest Bitcoin innovations"
" in the Edge Version.", title="Edge Version")
dis.draw_status(xfp=settings.get('xfp'))
@ -1018,6 +1015,7 @@ async def export_xpub(label, _2, item):
chain = chains.current_chain()
acct = 0
slip132 = False # non-slip is default from Oct 2024
# decode menu code => standard derivation
mode = item.arg
@ -1033,24 +1031,44 @@ async def export_xpub(label, _2, item):
else:
remap = {44:0, 49:1, 84:2,86:3}[mode]
_, path, addr_fmt = chains.CommonDerivations[remap]
path = path.format(account='{acct}', coin_type=chain.b44_cointype, change=0, idx=0)[:-4]
# always show SLIP-132 style, because defacto
show_slip132 = (addr_fmt != AF_CLASSIC)
path = path.format(account=acct, coin_type=chain.b44_cointype,
change=0, idx=0)[:-4]
while 1:
msg = '''Show QR of the XPUB for path:\n\n%s\n\n''' % path
msg = 'Show QR of the XPUB for path:\n\n%s\n\n' % path
esc = ""
if path != "m":
esc += "1"
msg += "Press (1) to select account other than %s." % (acct or "zero")
if addr_fmt not in (AF_CLASSIC, AF_P2TR):
esc += "2"
slp_af = addr_fmt
if slip132:
slp_af = AF_CLASSIC
if '{acct}' in path:
msg += "Press (1) to select account other than zero. "
slp = chain.slip132[slp_af].hint + "pub"
msg += " Press (2) to show %s %s." % (
slp, "(BIP-32)" if slip132 else "(SLIP-132)"
)
if glob.NFC:
msg += "Press %s to share via NFC. " % (KEY_NFC if version.has_qwerty else "(3)")
if version.has_qwerty:
esc += KEY_NFC
key_hint = KEY_NFC
else:
esc += "3"
key_hint = "(3)"
msg += " Press %s to share via NFC. " % key_hint
ch = await ux_show_story(msg, escape='13')
ch = await ux_show_story(msg, escape=esc)
if ch == 'x': return
if ch == "2":
slip132 = not slip132
continue
if ch == '1':
acct = await ux_enter_bip32_index('Account Number:') or 0
path = path.format(acct=acct)
pth_split = path.split("/")
pth_split[-1] = ("%dh" % acct)
path = "/".join(pth_split)
continue
# assume zero account if not picked
@ -1062,7 +1080,7 @@ async def export_xpub(label, _2, item):
# render xpub/ypub/zpub
with stash.SensitiveValues() as sv:
node = sv.derive_path(path) if path != 'm' else sv.node
xpub = chain.serialize_public(node, addr_fmt)
xpub = chain.serialize_public(node, addr_fmt if slip132 else AF_CLASSIC)
from ownership import OWNERSHIP
OWNERSHIP.note_wallet_used(addr_fmt, acct)
@ -1072,8 +1090,6 @@ async def export_xpub(label, _2, item):
else:
await show_qr_code(xpub, False)
break
def electrum_export_story(background=False):
# saves memory being in a function
@ -1096,9 +1112,9 @@ async def electrum_skeleton(*a):
return
rv = [
MenuItem(addr_fmt_label(af), f=electrum_skeleton_step2,
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
arg=(af, account_num))
for af in [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH]
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
@ -1112,7 +1128,7 @@ def ss_descriptor_export_story(addition="", background="", acct=True):
async def ss_descriptor_skeleton(_0, _1, item):
# Export of descriptor data (wallet)
int_ext, addition, f_pattern = None, "", "descriptor.txt"
allowed_af = [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2TR]
allowed_af = chains.SINGLESIG_AF
if item.arg:
int_ext, allowed_af, ll, f_pattern = item.arg
addition = " for " + ll
@ -1139,7 +1155,7 @@ async def ss_descriptor_skeleton(_0, _1, item):
fname_pattern=f_pattern)
else:
rv = [
MenuItem(addr_fmt_label(af), f=descriptor_skeleton_step2,
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
arg=(af, account_num, int_ext, f_pattern))
for af in allowed_af
]
@ -1405,6 +1421,63 @@ async def restore_everything_cleartext(*A):
if prob:
await ux_show_story(prob, title='FAILED')
async def bkpw_override(*A):
# allows user to:
# 1.) manually set bkpw
# 2.) remove existing bkpw setting
# 3.) view current active bkpw
# - some truncation of titles here on Mk4,
# which is okay because re-using strings to save space.
from backups import bkpw_min_len
if pa.is_secret_blank():
return
if pa.is_deltamode():
import callgate
callgate.fast_wipe()
while True:
pwd = settings.get("bkpw", None)
msg = ("Password used to encrypt COLDCARD backup files."
"\n\nPress (0) to change backup password")
esc = "0"
if pwd:
esc += "12"
msg += ", (1) to forget current password, (2) to show current active backup password."
else:
msg += "."
ch = await ux_show_story(title="BKPW Override", msg=msg, escape=esc)
if ch == "x": return
elif ch == "1":
if await ux_confirm("Delete current stored password?"):
settings.remove_key("bkpw")
settings.save()
await ux_dramatic_pause("Deleted.", 2)
elif ch == "2":
if await ux_confirm('The next screen will show current active backup password.'
'\n\nAnyone with knowledge of the password will '
'be able to decrypt your backups.'):
await ux_show_story(pwd, title="Your Backup Password")
elif ch == "0":
if version.has_qwerty:
from notes import get_a_password
npwd = await get_a_password(pwd, min_len=bkpw_min_len)
else:
npwd = await ux_input_text(pwd, prompt="Your Backup Password",
min_len=bkpw_min_len, max_len=128)
if (npwd is None) or (npwd == pwd): continue
settings.set('bkpw', npwd)
settings.save()
await ux_dramatic_pause("Saved.", 2)
async def wipe_filesystem(*A):
if not await ux_confirm('''\
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
@ -1433,11 +1506,13 @@ Erases and reformats MicroSD card. This is not a secure erase but more of a quic
wipe_microsd_card()
async def qr_share_file(*A):
async def qr_share_file(_1, _2, item):
# Pick file from SD card and share as (BB)Qr
from files import CardSlot, CardMissingError, needs_microsd
from export import export_by_qr
force_bbqr = item.arg
def is_suitable(fname):
f = fname.lower()
return f.endswith('.psbt') or f.endswith('.txn') \
@ -1482,7 +1557,7 @@ async def qr_share_file(*A):
else:
raise ValueError(ext)
await export_by_qr(data, txid, tc)
await export_by_qr(data, txid, tc, force_bbqr=force_bbqr)
async def nfc_share_file(*A):
@ -1880,10 +1955,11 @@ async def sign_message_on_sd(*a):
# min 1 line max 3 lines
return 1 <= len(lines) <= 3
fn = await file_picker(suffix='txt', min_size=2, max_size=500, taster=is_signable,
none_msg=('Must be one line of text, optionally '
fn = await file_picker(suffix=['txt', "json"], min_size=2, max_size=500, taster=is_signable,
none_msg=('Must be txt file with one msg line, optionally '
'followed by a subkey derivation path on a second line '
'and/or address format on third line.'))
'and/or address format on third line. JSON msg signing '
'format also supported'))
if not fn:
return
@ -2191,14 +2267,11 @@ async def change_virtdisk_enable(enable):
async def change_seed_vault(is_enabled):
# user has changed seed vault enable/disable flag
from glob import settings
if (not is_enabled) and settings.master_get('seeds'):
# problem: they still have some seeds... also this path blocks
# disable from within a tmp seed
settings.set('seedvault', 1) # restore it
await ux_show_story("Please remove all seeds from the vault before disabling.")
return
goto_top_menu()

View File

@ -8,17 +8,27 @@ import chains, stash, version
from ux import ux_show_story, the_ux, ux_enter_bip32_index
from ux import export_prompt_builder, import_export_prompt_decode
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_P2TR
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from uasyncio import sleep_ms
from uhashlib import sha256
from glob import settings
from auth import write_sig_file
from utils import addr_fmt_label, truncate_address
from charcodes import KEY_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT
from charcodes import KEY_CANCEL
from utils import show_single_address, problem_file_line
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
if not version.has_qwerty:
# - 16 chars screen width
# - but 2 lost at left (menu arrow, corner arrow)
# - want to show not truncated on right side
return addr[0:6] + '' + addr[-6:]
else:
# tons of space on Q1
return addr[0:12] + '' + addr[-12:]
class KeypathMenu(MenuSystem):
def __init__(self, path=None, nl=0):
@ -103,8 +113,8 @@ class PickAddrFmtMenu(MenuSystem):
def __init__(self, path, parent):
self.parent = parent
items = [
MenuItem(addr_fmt_label(af), f=self.done, arg=(path, af))
for af in [AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH]
MenuItem(chains.addr_fmt_label(af), f=self.done, arg=(path, af))
for af in chains.SINGLESIG_AF
]
super().__init__(items)
if path.startswith("m/84h"):
@ -169,8 +179,7 @@ class AddressListMenu(MenuSystem):
# Create list of choices (address_index_0, path, addr_fmt)
choices = []
for name, path, addr_fmt in chains.CommonDerivations:
if '{coin_type}' in path:
path = path.replace('{coin_type}', str(chain.b44_cointype))
path = path.replace('{coin_type}', str(chain.b44_cointype))
if self.account_num != 0 and '{account}' not in path:
# skip derivations that are not affected by account number
@ -179,7 +188,7 @@ class AddressListMenu(MenuSystem):
deriv = path.format(account=self.account_num, change=0, idx=self.start)
node = sv.derive_path(deriv, register=False)
address = chain.address(node, addr_fmt)
choices.append( (truncate_address(address), path, addr_fmt) )
choices.append((truncate_address(address), path, addr_fmt))
dis.progress_sofar(len(choices), len(chains.CommonDerivations))
@ -189,7 +198,7 @@ class AddressListMenu(MenuSystem):
indent = '' if version.has_qwerty else ''
for i, (address, path, addr_fmt) in enumerate(choices):
axi = address[-4:] # last 4 address characters
items.append(MenuItem(addr_fmt_label(addr_fmt), f=self.pick_single,
items.append(MenuItem(chains.addr_fmt_label(addr_fmt), f=self.pick_single,
arg=(path, addr_fmt, axi)))
items.append(MenuItem(indent+address, f=self.pick_single,
arg=(path, addr_fmt, axi)))
@ -298,7 +307,7 @@ Press (3) if you really understand and accept these risks.
for idx, addr, deriv in main.yield_addresses(start, n, change if allow_change else None):
addrs.append(addr)
msg += "%s =>\n%s\n\n" % (deriv, addr)
msg += "%s =>\n%s\n\n" % (deriv, show_single_address(addr))
dis.progress_sofar(idx-start+1, n or 1)
# export options
@ -317,6 +326,10 @@ Press (3) if you really understand and accept these risks.
msg += '\n\n'
if n:
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
else:
if addr_fmt != AF_P2TR:
escape += "0"
msg += " Press (0) to sign message with this key."
return msg, addrs, escape
@ -342,10 +355,10 @@ Press (3) if you really understand and accept these risks.
# continue on same screen in case they want to write to multiple cards
elif choice == KEY_QR:
# switch into a mode that shows them as QR codes
from ux import show_qr_codes
addr_fmt = addr_fmt or ms_wallet.addr_fmt
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
await show_qr_codes(addrs, is_alnum, start)
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
continue
elif NFC and (choice == KEY_NFC):
@ -357,8 +370,15 @@ Press (3) if you really understand and accept these risks.
continue
elif choice == '0' and allow_change:
change = 1
elif choice == '0':
if allow_change:
change = 1
else:
# only custom path sets allow_change to False
# msg sign
from auth import sign_with_own_address
await sign_with_own_address(path, addr_fmt)
elif n is None:
# makes no sense to do any of below, showing just single address
continue
@ -462,7 +482,6 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
await needs_microsd()
return
except Exception as e:
from utils import problem_file_line
await ux_show_story('Failed to write!\n\n\n%s\n%s' % (e, problem_file_line(e)))
return

View File

@ -13,12 +13,12 @@ from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P
from public_constants import STXN_FLAGS_MASK, 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
from ux import show_qr_code, OK, X, ux_input_text, ux_enter_bip32_index
from usb import CCBusyError
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path
from utils import B2A, parse_addr_fmt_str, to_ascii_printable
from utils import B2A, to_ascii_printable, show_single_address
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
from files import CardSlot
from files import CardSlot, CardMissingError, needs_microsd
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
@ -282,14 +282,13 @@ def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_
dis.progress_bar_show(i / 6)
return sig_nice
def validate_text_for_signing(text):
def validate_text_for_signing(text, only_printable=True):
# Check for some UX/UI traps in the message itself.
# - messages must be short and ascii only. Our charset is limited
# - too many spaces, leading/trailing can be an issue
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
result = to_ascii_printable(text)
result = to_ascii_printable(text, only_printable=only_printable)
length = len(result)
assert length >= 2, "msg too short (min. 2)"
@ -302,12 +301,80 @@ def validate_text_for_signing(text):
# looks ok
return result
def addr_fmt_from_subpath(subpath):
if not subpath:
af = "p2pkh"
elif subpath[:4] == "m/84":
af = "p2wpkh"
elif subpath[:4] == "m/49":
af = "p2sh-p2wpkh"
else:
af = "p2pkh"
return af
def parse_msg_sign_request(data):
subpath = ""
addr_fmt = None
is_json = False
# sparrow compat
if "signmessage" in data:
try:
mark, subpath, *msg_line = data.split(" ", 2)
assert mark == "signmessage"
# subpath will be verified & cleaned later
assert msg_line[0][:6] == "ascii:"
text = msg_line[0][6:]
return text, subpath, addr_fmt_from_subpath(subpath), is_json
except:pass
# ===
try:
data_dict = ujson.loads(data.strip())
text = data_dict.get("msg", None)
if text is None:
raise AssertionError("MSG required")
subpath = data_dict.get("subpath", subpath)
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
is_json = True
except ValueError:
lines = data.split("\n")
assert len(lines) >= 1, "min 1 line"
assert len(lines) <= 3, "max 3 lines"
if len(lines) == 1:
text = lines[0]
elif len(lines) == 2:
text, subpath = lines
else:
text, subpath, addr_fmt = lines
if not addr_fmt:
addr_fmt = addr_fmt_from_subpath(subpath)
if not subpath:
subpath = chains.STD_DERIVATIONS[addr_fmt]
subpath = subpath.format(
coin_type=chains.current_chain().b44_cointype,
account=0, change=0, idx=0
)
return text, subpath, addr_fmt, is_json
class ApproveMessageSign(UserAuthorizedAction):
def __init__(self, text, subpath, addr_fmt, approved_cb=None):
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, only_printable=True):
super().__init__()
self.text = validate_text_for_signing(text)
is_json = False
if msg_sign_request:
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
self.text = validate_text_for_signing(
text, only_printable=not is_json and only_printable
)
self.subpath = cleanup_deriv_path(subpath)
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
self.approved_cb = approved_cb
# temporary - no p2tr support
@ -330,14 +397,14 @@ class ApproveMessageSign(UserAuthorizedAction):
if hsm_active:
ch = await hsm_active.approve_msg_sign(self.text, self.address, self.subpath)
else:
story = MSG_SIG_TEMPLATE.format(msg=self.text, addr=self.address, subpath=self.subpath)
story = MSG_SIG_TEMPLATE.format(msg=self.text, addr=show_single_address(self.address),
subpath=self.subpath)
ch = await ux_show_story(story)
if ch != 'y':
# they don't want to!
self.refused = True
else:
# perform signing (progress bar shown)
digest = chains.current_chain().hash_message(self.text.encode())
self.result = sign_message_digest(digest, self.subpath, "Signing...", self.addr_fmt)[0]
@ -355,36 +422,166 @@ class ApproveMessageSign(UserAuthorizedAction):
def sign_msg(text, subpath, addr_fmt):
subpath = cleanup_deriv_path(subpath)
UserAuthorizedAction.check_busy()
UserAuthorizedAction.active_request = ApproveMessageSign(text, subpath, addr_fmt)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
async def msg_sign_ux_get_subpath(addr_fmt):
purpose = chains.af_to_bip44_purpose(addr_fmt)
chain_n = chains.current_chain().b44_cointype
acct = await ux_enter_bip32_index('Account Number:') or 0
ch = await ux_show_story(title="Change?",
msg="Press (0) to use internal/change address,"
" %s to use external/receive address." % OK, escape="0")
change = 1 if ch == '0' else 0
idx = await ux_enter_bip32_index('Index Number:') or 0
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
from menu import MenuSystem, MenuItem
from ux import the_ux
async def done(_1, _2, item):
from auth import approve_msg_sign, msg_sign_ux_get_subpath
text, af = item.arg
subpath = await msg_sign_ux_get_subpath(af)
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
kill_menu=kill_menu, only_printable=False)
# pick address format
rv = [
MenuItem(chains.addr_fmt_label(af), f=done, arg=(txt, af))
for af in (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH) # cannot use SINGLE_AF here as it contains taproot
]
the_ux.push(MenuSystem(rv))
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, kill_menu=False,
only_printable=True):
UserAuthorizedAction.cleanup()
UserAuthorizedAction.check_busy(ApproveMessageSign)
try:
UserAuthorizedAction.active_request = ApproveMessageSign(
text, subpath, addr_fmt,
approved_cb=approved_cb,
msg_sign_request=msg_sign_request,
only_printable=only_printable,
)
if kill_menu:
abort_and_goto(UserAuthorizedAction.active_request)
else:
# do not kill the menu stack! just append
from ux import the_ux
the_ux.push(UserAuthorizedAction.active_request)
except (AssertionError, ValueError) as exc:
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
return
async def msg_signing_done(signature, address, text):
from ux import import_export_prompt
ch = await import_export_prompt("Signed Msg", is_import=False,
no_qr=not version.has_qwerty)
if ch == KEY_CANCEL:
return
if isinstance(ch, dict):
await sd_sign_msg_done(signature, address, text, "msg_sign", **ch)
elif version.has_qr and ch == KEY_QR:
from ux_q1 import qr_msg_sign_done
await qr_msg_sign_done(signature, address, text)
elif ch in KEY_NFC+"3":
from glob import NFC
if NFC:
await NFC.msg_sign_done(signature, address, text)
async def sign_with_own_address(subpath, addr_fmt):
# used for cases where we already have the key picked, but need the message:
# * address_explorer custom path
# * positive ownership test
from glob import dis
to_sign = await ux_input_text("", scan_ok=True, prompt="Enter MSG") # max len is 100 only here
if not to_sign: return
await approve_msg_sign(to_sign, subpath, addr_fmt, approved_cb=msg_signing_done, kill_menu=True)
async def sd_sign_msg_done(signature, address, text, base=None, orig_path=None,
slot_b=None, force_vdisk=False):
from glob import dis
dis.fullscreen('Generating...')
out_fn = None
sig = b2a_base64(signature).decode('ascii').strip()
while 1:
# try to put back into same spot
# add -signed to end.
target_fname = base + '-signed.txt'
lst = [orig_path]
if orig_path:
lst.append(None)
for path in lst:
try:
with CardSlot(readonly=True, slot_b=slot_b, force_vdisk=force_vdisk) as card:
out_full, out_fn = card.pick_filename(target_fname, path)
out_path = path
if out_full: break
except CardMissingError:
prob = 'Missing card.\n\n'
out_fn = None
if not out_fn:
# need them to insert a card
prob = ''
else:
# attempt write-out
try:
dis.fullscreen("Saving...")
with CardSlot(slot_b=slot_b, force_vdisk=force_vdisk) as card:
with card.open(out_full, 'wt') as fd:
# save in full RFC style
# gen length is 6
gen = rfc_signature_template_gen(addr=address, msg=text, sig=sig)
for i, part in enumerate(gen):
fd.write(part)
dis.progress_bar_show(i / 6)
# success and done!
break
except OSError as exc:
prob = 'Failed to write!\n\n%s\n\n' % exc
sys.print_exception(exc)
# fall through to try again
# prompt them to input another card?
ch = await ux_show_story(prob + "Please insert an SDCard to receive signed message, "
"and press %s." % OK, title="Need Card")
if ch == 'x':
await ux_aborted()
return
# done.
msg = "Created new file:\n\n%s" % out_fn
await ux_show_story(msg, title='File Signed')
async def sign_txt_file(filename):
# sign a one-line text file found on a MicroSD card
# - not yet clear how to do address types other than 'classic'
from files import CardSlot, CardMissingError
from ux import the_ux
UserAuthorizedAction.cleanup()
# copy message into memory
with CardSlot() as card:
with card.open(filename, 'rt') as fd:
text = fd.readline().strip()
subpath = fd.readline().strip()
addr_fmt = fd.readline().strip()
if not subpath:
# default: top of wallet.
subpath = 'm'
if not addr_fmt:
addr_fmt = AF_CLASSIC
async def done(signature, address, text):
# complete. write out result
from glob import dis
@ -392,68 +589,19 @@ async def sign_txt_file(filename):
orig_path, basename = filename.rsplit('/', 1)
orig_path += '/'
base = basename.rsplit('.', 1)[0]
out_fn = None
sig = b2a_base64(signature).decode('ascii').strip()
while 1:
# try to put back into same spot
# add -signed to end.
target_fname = base+'-signed.txt'
for path in [orig_path, None]:
try:
with CardSlot(readonly=True) as card:
out_full, out_fn = card.pick_filename(target_fname, path)
out_path = path
if out_full: break
except CardMissingError:
prob = 'Missing card.\n\n'
out_fn = None
if not out_fn:
# need them to insert a card
prob = ''
else:
# attempt write-out
try:
dis.fullscreen("Saving...")
with CardSlot() as card:
with card.open(out_full, 'wt') as fd:
# save in full RFC style
# gen length is 6
gen = rfc_signature_template_gen(addr=address, msg=text, sig=sig)
for i, part in enumerate(gen):
fd.write(part)
dis.progress_bar_show(i / 6)
# success and done!
break
except OSError as exc:
prob = 'Failed to write!\n\n%s\n\n' % exc
sys.print_exception(exc)
# fall through to try again
# prompt them to input another card?
ch = await ux_show_story(prob+"Please insert an SDCard to receive signed message, "
"and press %s." % OK, title="Need Card")
if ch == 'x':
await ux_aborted()
return
# done.
msg = "Created new file:\n\n%s" % out_fn
await ux_show_story(msg, title='File Signed')
await sd_sign_msg_done(signature, address, text, base, orig_path)
UserAuthorizedAction.cleanup()
UserAuthorizedAction.check_busy()
try:
UserAuthorizedAction.active_request = ApproveMessageSign(text, subpath, addr_fmt, approved_cb=done)
# do not kill the menu stack!
the_ux.push(UserAuthorizedAction.active_request)
except AssertionError as exc:
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
return
# copy message into memory
with CardSlot() as card:
with card.open(filename, 'rt') as fd:
res = fd.read()
await approve_msg_sign(None, None, None, approved_cb=done,
msg_sign_request=res)
def verify_signature(msg, addr, sig_str):
warnings = ""
@ -541,7 +689,7 @@ async def verify_armored_signed_msg(contents, digest_check=True):
title = "CORRECT"
warn_msg = ""
err_msg = ""
story = "Good signature by address:\n %s" % addr
story = "Good signature by address:\n%s" % show_single_address(addr)
if digest_check:
digest_prob = verify_signed_file_digest(msg)
@ -570,7 +718,6 @@ async def verify_armored_signed_msg(contents, digest_check=True):
await ux_show_story('\n\n'.join(m for m in [err_msg, story, warn_msg] if m), title=title)
async def verify_txt_sig_file(filename):
from files import CardSlot, CardMissingError, needs_microsd
# copy message into memory
try:
with CardSlot() as card:
@ -625,7 +772,7 @@ class ApproveTransaction(UserAuthorizedAction):
try:
dest = self.chain.render_address(o.scriptPubKey)
return '%s\n - to address -\n%s\n' % (val, dest)
return '%s\n - to address -\n%s\n' % (val, show_single_address(dest))
except ValueError:
pass
@ -927,8 +1074,10 @@ class ApproveTransaction(UserAuthorizedAction):
msg = make_msg(start, n)
async def save_visualization(self, msg, sign_text=False):
# write text into spi flash, maybe signing it as we go
# write story text out, maybe signing it as we go
# - return length and checksum
from charcodes import OUT_CTRL_ADDRESS
txt_len = msg.seek(0, 2)
msg.seek(0)
@ -936,7 +1085,8 @@ class ApproveTransaction(UserAuthorizedAction):
with SFFile(TXN_OUTPUT_OFFSET, max_size=txt_len+300, message="Visualizing...") as fd:
while 1:
blk = msg.read(256).encode('ascii')
# replace with empty space, to keep correct txt_len - already hashed
blk = msg.read(256).replace(OUT_CTRL_ADDRESS, ' ').encode('ascii')
if not blk: break
if chk:
chk.update(blk)
@ -949,7 +1099,7 @@ class ApproveTransaction(UserAuthorizedAction):
fd.write(b2a_base64(sig).decode('ascii').strip())
fd.write('\n')
return (fd.tell(), fd.checksum.digest())
return fd.tell(), fd.checksum.digest()
def output_summary_text(self, msg):
# Produce text report of where their cash is going. This is what
@ -1027,13 +1177,13 @@ class ApproveTransaction(UserAuthorizedAction):
visible_change_sum = 0
if len(largest_change) == 1:
visible_change_sum += largest_change[0][0]
msg.write(' - to address -\n%s\n' % largest_change[0][1])
msg.write(' - to address -\n%s\n' % show_single_address(largest_change[0][1]))
else:
msg.write(' - to addresses -\n')
for val, addr in largest_change:
visible_change_sum += val
msg.write(addr)
msg.write('\n')
msg.write(show_single_address(addr))
msg.write('\n\n')
left_c = self.psbt.num_change_outputs - len(largest_change)
if left_c:
@ -1043,7 +1193,7 @@ class ApproveTransaction(UserAuthorizedAction):
msg.write("\n")
# if we didn't already show all outputs, then give user a chance to
# if we didn't already show all outputs, then give user a chance to
# view them individually
return needs_txn_explorer
@ -1080,7 +1230,6 @@ def psbt_encoding_taster(taste, psbt_len):
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None):
# sign a PSBT file found on a MicroSD card
# - or from VirtualDisk (mk4)
from files import CardSlot, CardMissingError
from glob import dis
from ux import the_ux
@ -1389,7 +1538,7 @@ class ShowAddressBase(UserAuthorizedAction):
hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
if ch in '4'+KEY_QR:
await show_qr_code(self.address, (self.addr_fmt & AFC_BECH32))
await show_qr_code(self.address, (self.addr_fmt & AFC_BECH32), is_addrs=True)
continue
if NFC and (ch in '3'+KEY_NFC):
@ -1400,7 +1549,7 @@ class ShowAddressBase(UserAuthorizedAction):
else:
# finish the Wait...
dis.progress_bar_show(1)
dis.progress_bar_show(1)
if self.restore_menu:
self.pop_menu()
@ -1421,7 +1570,8 @@ class ShowPKHAddress(ShowAddressBase):
self.address = sv.chain.address(node, addr_fmt)
def get_msg(self):
return '''{addr}\n\n= {sp}''' .format(addr=self.address, sp=self.subpath)
return '''{addr}\n\n= {sp}''' .format(addr=show_single_address(self.address),
sp=self.subpath)
class ShowP2SHAddress(ShowAddressBase):
@ -1448,8 +1598,8 @@ Wallet:
Paths:
{sp}'''.format(addr=self.address, name=self.ms.name,
M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help))
{sp}'''.format(addr=show_single_address(self.address), name=self.ms.name,
M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help))
class ShowMiniscriptAddress(ShowAddressBase):
@ -1466,6 +1616,7 @@ class ShowMiniscriptAddress(ShowAddressBase):
def get_msg(self):
return '''\
{addr}
Wallet:
{name}
@ -1473,7 +1624,8 @@ Index:
{idx}
Change:
{change}'''.format(addr=self.address, name=self.msc.name, idx=self.idx, change=bool(self.change))
{change}'''.format(addr=show_single_address(self.address), name=self.msc.name,
idx=self.idx, change=bool(self.change))
def start_show_miniscript_address(msc, change, index):

View File

@ -15,6 +15,7 @@ from pincodes import pa
# we make passwords with this number of words
num_pw_words = const(12)
bkpw_min_len = const(32)
# max size we expect for a backup data file (encrypted or cleartext)
# - limited by size of LFS area of flash, since all settings are held there
@ -309,7 +310,7 @@ async def restore_from_dict(vals):
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
from stash import bip39_passphrase
words = None
pwd = None
skip_quiz = False
bypass_tmp = False
@ -329,28 +330,40 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
"so backup will be of that seed."):
return
stored_words = settings.get('bkpw', None)
# first check if bkpw already defined on tmp seed settings
stored_pwd = None
master_pwd = settings.master_get("bkpw", None)
if pa.tmp_value:
stored_pwd = settings.get('bkpw', None)
if stored_words:
stored_words = stored_words.split()
ch = await ux_show_story("Use same backup file password as last time?\n\n"
" 1: %s\n ...\n%d: %s"
% (stored_words[0], len(stored_words), stored_words[-1]), sensitive=True)
if not stored_pwd and master_pwd:
stored_pwd = master_pwd
if stored_pwd:
# we can have words or other type of password here
split_pwd = stored_pwd.split()
if len(split_pwd) == num_pw_words: # weak
hint = " 1: %s\n ...\n%d: %s" % (split_pwd[0], len(split_pwd), split_pwd[-1])
else:
hint = " %s...%s" % (stored_pwd[0], stored_pwd[-1])
ch = await ux_show_story("Use same backup file password as last time?\n\n" + hint,
sensitive=True)
if ch == 'y':
words = stored_words
pwd = stored_pwd # string, not list
skip_quiz = True
if not words:
if not pwd:
# Pick a password: like bip39 but no checksum word
#
b = bytearray(32)
while 1:
ckcc.rng_bytes(b)
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
pwd = bip39.b2a_words(b).rsplit(' ', num_pw_words)[0]
ch = await seed.show_words(words,
prompt="Record this (%d word) backup file password:\n", escape='6')
ch = await seed.show_words(prompt="Record this (%d word) backup file password:\n",
words=pwd.split(" "), escape='6')
if ch == '6' and not write_sflash:
# Secret feature: plaintext mode
@ -367,43 +380,43 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
break
if words and not skip_quiz:
if pwd and not skip_quiz:
# quiz them, but be nice and do a shorter test.
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
ch = await seed.word_quiz(pwd.split(" "), limited=(num_pw_words//3))
if ch == 'x': return
if words and words != stored_words:
if pwd and pwd != stored_pwd:
ch = await ux_show_story("Would you like to use these same words next time you perform a backup?"
" Press (1) to save them into this Coldcard for next time.", escape='1')
if ch == '1':
settings.put('bkpw', ' '.join(words))
settings.save()
elif stored_words:
settings.remove_key('bkpw')
settings.set('bkpw', pwd) # if on tmp save to tmp, do not update master
settings.save()
# stop droping bkpw just because someone decided to use differrent password
# elif stored_words:
# settings.remove_key('bkpw')
# settings.save()
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash,
return await write_complete_backup(pwd, fname_pattern, write_sflash=write_sflash,
bypass_tmp=bypass_tmp)
async def write_complete_backup(words, fname_pattern, write_sflash=False,
async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
allow_copies=True, bypass_tmp=False):
# Just do the writing
from glob import dis
from files import CardSlot
# Show progress:
dis.fullscreen('Encrypting...' if words else 'Generating...')
dis.fullscreen('Encrypting...' if pwd else 'Generating...')
body = render_backup_contents(bypass_tmp=bypass_tmp).encode()
gc.collect()
if words:
if pwd:
# NOTE: Takes a few seconds to do the key-streching, but little actual
# time to do the encryption.
pw = ' '.join(words)
zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show)
zz = compat7z.Builder(password=pwd, progress_fcn=dis.progress_bar_show)
zz.add_data(body)
# pick random filename, but ending in .txt
@ -742,11 +755,9 @@ async def clone_write_data(*a):
my_pubkey = pair.pubkey().to_bytes(False)
session_key = pair.ecdh_multiply(his_pubkey)
words = [b2a_hex(session_key).decode()]
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
await write_complete_backup(words, fname, allow_copies=False, bypass_tmp=True)
await write_complete_backup(b2a_hex(session_key).decode(), fname, allow_copies=False, bypass_tmp=True)
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")

View File

@ -13,6 +13,9 @@ from serializations import hash160, ser_compact_size, disassemble, ser_string
from ucollections import namedtuple
from opcodes import OP_RETURN, OP_1, OP_16
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2TR, AF_P2WPKH_P2SH)
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
@ -51,8 +54,6 @@ def tapleaf_hash(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
class ChainsBase:
curve = 'secp256k1'
menu_name = None # use 'name' if this isn't defined
core_name = None # name of chain's "core" p2p software
# b44_cointype comes from
# <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>
@ -319,8 +320,7 @@ class ChainsBase:
class BitcoinMain(ChainsBase):
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
ctype = 'BTC'
name = 'Bitcoin'
core_name = 'Bitcoin Core'
name = 'Bitcoin Mainnet'
slip132 = {
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
@ -340,9 +340,9 @@ class BitcoinMain(ChainsBase):
b44_cointype = 0
class BitcoinTestnet(BitcoinMain):
# testnet4 (was testnet3 up until 2025 but all parameters are the same)
ctype = 'XTN'
name = 'Bitcoin Testnet'
menu_name = 'Testnet: BTC'
name = 'Bitcoin Testnet 4'
slip132 = {
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
@ -365,7 +365,6 @@ class BitcoinTestnet(BitcoinMain):
class BitcoinRegtest(BitcoinMain):
ctype = 'XRT'
name = 'Bitcoin Regtest'
menu_name = 'Regtest: BTC'
slip132 = {
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
@ -446,6 +445,53 @@ CommonDerivations = [
AF_P2TR), # generates bc1p bech32m addresses
]
STD_DERIVATIONS = {
"p2pkh": CommonDerivations[0][1],
"p2sh-p2wpkh": CommonDerivations[1][1],
"p2wpkh-p2sh": CommonDerivations[1][1],
"p2wpkh": CommonDerivations[2][1],
}
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
try:
if isinstance(addr_fmt, int):
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
return addr_fmt
else:
raise ValueError
addr_fmt = addr_fmt.lower()
if addr_fmt in ("p2sh-p2wpkh", "p2wpkh-p2sh"):
return AF_P2WPKH_P2SH
elif addr_fmt == "p2pkh":
return AF_CLASSIC
elif addr_fmt == "p2wpkh":
return AF_P2WPKH
elif addr_fmt == "p2tr":
return AF_P2TR
else:
raise ValueError
except ValueError:
raise ValueError("Unsupported address format: '%s'" % addr_fmt)
def af_to_bip44_purpose(addr_fmt):
# single signature only
return {AF_CLASSIC: 44,
AF_P2WPKH_P2SH: 49,
AF_P2WPKH: 84,
AF_P2TR: 86}[addr_fmt]
def addr_fmt_label(addr_fmt):
return {
AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH",
AF_P2TR: "Taproot P2TR",
AF_P2WSH: "Segwit P2WSH",
AF_P2WSH_P2SH: "P2SH-P2WSH"
}[addr_fmt]
def verify_recover_pubkey(sig, digest):
# verifies a message digest against a signature and recovers

View File

@ -107,4 +107,9 @@ if has_qwerty:
assert DECODER[KEYNUM_SYMBOL] == KEY_SYMBOL
assert DECODER[KEYNUM_LAMP] == KEY_LAMP
# These affect how 'ux stories' are rendered; they are control
# characters on the output side of things, not input.
OUT_CTRL_TITLE = '\x01' # must be first char in line: be a title line
OUT_CTRL_ADDRESS = '\x02' # must be first char in line: it's a payment address
# EOF

View File

@ -4,7 +4,7 @@
#
# included in Q builds only, not Mk4 --> manifest_q1.py
#
import ngu, bip39, ure, stash
import ngu, bip39, ure, stash, json
from ubinascii import unhexlify as a2b_hex
from exceptions import QRDecodeExplained
from bbqr import TYPE_LABELS
@ -131,7 +131,11 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
pass
elif ty == 'J':
return 'json', (got,)
what = "json"
if "msg" in got:
what = "smsg"
return what, (got,)
else:
msg = TYPE_LABELS.get(ty, 'Unknown FileType')
raise QRDecodeExplained("Sorry, %s not useful." % msg)
@ -159,6 +163,16 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
if expect_secret:
raise QRDecodeExplained("Not a secret?")
try:
dct = json.loads(got)
if "msg" in dct:
return "smsg", (got,)
except: pass
# Sparrow compat
if "signmessage" in got:
return "smsg", (got,)
# try to recognize various bitcoin-related text strings...
return decode_short_text(got)
@ -178,6 +192,9 @@ def decode_short_text(got):
# might be a PSBT?
if len(got) > 100:
if got.lstrip().startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----"):
return "vmsg", (got,)
from auth import psbt_encoding_taster
try:
decoder, _, psbt_len = psbt_encoding_taster(got[0:10].encode(), len(got))

View File

@ -146,8 +146,10 @@ class KeyOriginInfo:
return cls(xfp, derivation)
def __str__(self):
return "%s/%s" % (b2a_hex(self.fingerprint).decode(),
keypath_to_str(self.derivation, prefix='', skip=0).replace("'", "h"))
rv = "%s" % b2a_hex(self.fingerprint).decode()
if self.derivation:
rv += "/%s" % keypath_to_str(self.derivation, prefix='', skip=0).replace("'", "h")
return rv
class KeyDerivationInfo:
@ -303,52 +305,27 @@ class Key:
# parse key
node, chain_type = cls.parse_key(k)
der = KeyDerivationInfo.from_string(der.decode())
if origin is None:
origin = KeyOriginInfo(ustruct.pack('<I', swab32(node.my_fp())), [])
return cls(node, origin, der, chain_type=chain_type)
@classmethod
def parse_key(cls, key_str):
chain_type = None
if key_str[1:4].lower() == b"pub":
# extended key
# or xpub or tpub as we use descriptors (SLIP-132 NOT allowed)
hint = key_str[0:1].lower()
if hint == b"x":
chain_type = "BTC"
else:
assert hint == b"t", "no slip"
chain_type = "XTN"
node = ngu.hdnode.HDNode()
node.deserialize(key_str)
assert key_str[1:4].lower() == b"pub", "only extended keys allowed"
# extended key
# or xpub or tpub as we use descriptors (SLIP-132 NOT allowed)
hint = key_str[0:1].lower()
if hint == b"x":
chain_type = "BTC"
else:
# only unspendable keys can be bare pubkeys - for now
H = PROVABLY_UNSPENDABLE[1:]
if b"r=" in key_str:
_, r = key_str.split(b"=")
if r == b"@":
# pick a fresh integer r in the range 0...n-1 uniformly at random and use H + rG
kp = ngu.secp256k1.keypair()
else:
# H + rG where r is provided from user
r = a2b_hex(r)
assert len(r) == 32, "r != 32"
kp = ngu.secp256k1.keypair(r)
H_xo = ngu.secp256k1.xonly_pubkey(H)
node = H_xo.tweak_add(kp.xonly_pubkey().to_bytes()).to_bytes()
elif a2b_hex(key_str) == H:
node = H
else:
node = a2b_hex(key_str)
assert len(node) == 32, "invalid pk %d %s" % (len(node), node)
assert hint == b"t", "no slip"
chain_type = "XTN"
node = ngu.hdnode.HDNode()
node.deserialize(key_str)
return node, chain_type
def derive(self, idx=None, change=False):
if isinstance(self.node, bytes):
return self
if isinstance(idx, list):
for i in idx:
mp_i = self.derivation.multi_path_index or 0
@ -397,22 +374,20 @@ class Key:
@property
def is_provably_unspendable(self):
if isinstance(self.node, bytes):
return True
if PROVABLY_UNSPENDABLE == self.node.pubkey():
return True
return False
@property
def prefix(self):
if self.origin:
if self.origin and self.origin.derivation:
return "[%s]" % self.origin
# jut a bare [xfp]key - omit origin info (jut xfp)
# or no origin at all
return ""
def key_bytes(self):
kb = self.node
if not isinstance(kb, bytes):
kb = self.node.pubkey()
kb = self.node.pubkey()
if self.taproot:
if len(kb) == 33:
kb = kb[1:]
@ -424,12 +399,9 @@ class Key:
def to_string(self, external=True, internal=True, subderiv=True):
key = self.prefix
if isinstance(self.node, ngu.hdnode.HDNode):
key += self.extended_public_key()
if self.derivation and subderiv:
key += "/" + self.derivation.to_string(external, internal)
else:
key += b2a_hex(self.node).decode()
key += self.extended_public_key()
if self.derivation and subderiv:
key += "/" + self.derivation.to_string(external, internal)
return key
@ -495,9 +467,12 @@ def fill_policy(policy, keys, external=True, internal=True):
k_orig = k.to_string(external, internal, subderiv=False)
else:
_idx = k.find("]") # end of key origin info - no more / expected besides subderivation
assert _idx != -1
ek = k[_idx+1:].split("/")[0]
k_orig = k[:_idx+1] + ek
if _idx != -1:
ek = k[_idx+1:].split("/")[0]
k_orig = k[:_idx+1] + ek
else:
# no origin info
k_orig = k.split("/")[0]
if k_orig not in orig_keys:
orig_keys.append(k_orig)

View File

@ -281,9 +281,8 @@ class Descriptor:
if self.key.origin:
# spendable internal key
res.append(self.key.origin.psbt_derivation())
elif not isinstance(self.key.node, bytes):
if self.key.is_provably_unspendable:
res.append([swab32(self.key.node.my_fp())])
elif self.key.is_provably_unspendable:
res.append([swab32(self.key.node.my_fp())])
for k in self.keys:
if k.origin:

View File

@ -7,6 +7,7 @@ from ssd1306 import SSD1306_SPI
from version import is_devmode, is_edge
import framebuf
from graphics_mk4 import Graphics
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
# we support 4 fonts
from zevvpeep import FontSmall, FontLarge, FontTiny
@ -310,9 +311,14 @@ class Display:
for ln in lines:
if ln == 'EOT':
self.hline(y+3)
elif ln and ln[0] == '\x01':
elif ln and ln[0] == OUT_CTRL_TITLE:
self.text(0, y, ln[1:], FontLarge)
y += 21
elif ln and ln[0] == OUT_CTRL_ADDRESS:
from utils import chunk_address
fmt = '\u2009'.join(chunk_address(ln[1:]))
self.text(14, y, fmt) # fixed indent, to be centered
y += 15 # a bit extra vertical line height
else:
self.text(0, y, ln)
@ -328,9 +334,10 @@ class Display:
# no status bar on Mk4
return
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert):
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, is_addr=False):
# 'sidebar' is a pre-formated obj to show to right of QR -- oled life
# - 'msg' will appear to right if very short, else under in tiny
# - ignores "is_addr" because exactly zero space to do anything special
from utils import word_wrap
self.clear()

View File

@ -13,13 +13,16 @@ from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from ownership import OWNERSHIP
async def export_by_qr(body, label, type_code):
async def export_by_qr(body, label, type_code, force_bbqr=False):
# render as QR and show on-screen
from ux import show_qr_code
try:
# ignore label/title - provides no useful info
# makes qr smaller and harder to read
if force_bbqr:
raise ValueError
await show_qr_code(body)
except (ValueError, RuntimeError, TypeError):
if version.has_qwerty:
@ -73,14 +76,7 @@ be needed for different systems.
sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp))
for name, path, addr_fmt in chains.CommonDerivations:
if '{coin_type}' in path:
path = path.replace('{coin_type}', str(chain.b44_cointype))
if '{' in name:
name = name.format(core_name=chain.core_name)
show_slip132 = ('Core' not in name)
path = path.replace('{coin_type}', str(chain.b44_cointype))
yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path))
yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx)
@ -103,7 +99,7 @@ be needed for different systems.
node = sv.derive_path(hard_sub, register=False)
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
if show_slip132 and addr_fmt not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
if addr_fmt not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
yield ("%s => %s ##SLIP-132##\n" % (
hard_sub, chain.serialize_public(node, addr_fmt)))
@ -449,14 +445,7 @@ def generate_electrum_wallet(addr_type, account_num):
xfp = settings.get('xfp')
# Must get the derivation path, and the SLIP32 version bytes right!
if addr_type == AF_CLASSIC:
mode = 44
elif addr_type == AF_P2WPKH:
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
else:
raise ValueError(addr_type)
mode = chains.af_to_bip44_purpose(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num)
@ -552,16 +541,7 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
xfp = settings.get('xfp')
dis.progress_bar_show(0.1)
if mode is None:
if addr_type == AF_CLASSIC:
mode = 44
elif addr_type == AF_P2WPKH:
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
elif addr_type == AF_P2TR:
mode = 86
else:
raise ValueError(addr_type)
mode = chains.af_to_bip44_purpose(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num)

View File

@ -179,8 +179,8 @@ XpubExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
MenuItem("Taproot/P2TR(86)", f=export_xpub, arg=86),
MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49),
MenuItem("Taproot/P2TR"+("(BIP-86)" if version.has_qwerty else "(86)"), f=export_xpub, arg=86),
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49),
MenuItem("Master XPUB", f=export_xpub, arg=0),
MenuItem("Current XFP", f=export_xpub, arg=-1),
]
@ -218,6 +218,7 @@ FileMgmtMenu = [
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),
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
MenuItem('Format SD Card', f=wipe_sd_card),
@ -236,6 +237,7 @@ DevelopersMenu = [
MenuItem("Serial REPL", f=dev_enable_repl),
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
MenuItem("BKPW Override", menu=bkpw_override),
]
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
@ -308,7 +310,7 @@ If you disable sighash flag restrictions, and ignore the \
warnings, funds can be stolen by specially crafted PSBT or MitM.
Keep blocked unless you intend to sign special transactions.'''),
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3', 'Regtest'],
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet4', 'Regtest'],
value_map=['BTC', 'XTN', 'XRT'],
on_change=change_which_chain,
story="Testnet must only be used by developers because \

View File

@ -3,11 +3,11 @@
# lcd_display.py - LCD rendering for Q1's 320x240 pixel *colour* display!
#
import machine, uzlib, utime, array
from uasyncio import sleep_ms
from graphics_q1 import Graphics
from st7788 import ST7788
from utils import xfp2str, word_wrap
from utils import xfp2str, word_wrap, chunk_address
from ucollections import namedtuple
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
# the one font: fixed-width (except for a few double-width chars)
from font_iosevka import CELL_W, CELL_H, TEXT_PALETTES, COL_TEXT, COL_DARK_TEXT, COL_SCROLL_DARK
@ -612,25 +612,50 @@ class Display:
self.clear()
y=0
prev_x = None
for ln in lines:
if ln == 'EOT':
self.text(0, y, ''*CHARS_W, dark=True)
continue
elif ln and ln[0] == '\x01':
elif ln and ln[0] == OUT_CTRL_TITLE:
# title ... but we have no special font? Inverse!
self.text(0, y, ' '+ln[1:]+' ', invert=True)
if hint_icons:
# maybe show that [QR] can do something
self.text(-1, y, hint_icons, dark=True)
elif ln and ln[0] == OUT_CTRL_ADDRESS:
# we can assume this will be a single line for our display
# thanks to code in utils.word_wrap
prev_x = self._draw_addr(y, ln[1:], prev_x=prev_x)
else:
self.text(0, y, ln)
prev_x = None
y += 1
self.scroll_bar(top, num_lines, CHARS_H)
self.show()
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None):
def _draw_addr(self, y, addr, prev_x=None):
# Draw a single-line of an address
# - use prev_x=0 to start centered
if prev_x is None:
# left justify (for stories)
prev_x = x = 1
elif prev_x == 0:
# center first line, following line(s) will be left-justified to match that
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
else:
x = prev_x
self.text(x, y, ' '+' '.join(chunk_address(addr))+' ', invert=True)
return prev_x
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None, is_addr=False):
# Show a QR code on screen w/ some text under it
# - invert not supported on Q1
# - sidebar not supported here (see users.py)
@ -638,14 +663,24 @@ class Display:
assert not sidebar
# maybe show something other than QR contents under it
if msg:
if is_addr:
# With fancy display, no address, even classic can fit in single line,
# so always split nicely in middle and at mod4
hh = len(msg) // 2
if hh <= 20:
hh = (hh + 3) & ~0x3
parts = [msg[0:hh], msg[hh:]]
num_lines = 2
else:
# p2wsh address would need 3 lines to show, so we won't
num_lines = 0
elif msg:
if len(msg) <= CHARS_W:
parts = [msg]
elif ' ' not in msg and (len(msg) <= CHARS_W*2):
# fits in two lines, but has no spaces (ie. payment addr)
# so split nicely, and shift off center
# fits in two lines, but has no spaces
hh = len(msg) // 2
parts = [msg[0:hh] + ' ', ' '+msg[hh:]]
parts = [msg[0:hh], msg[hh:]]
else:
# do word wrap
parts = list(word_wrap(msg, CHARS_W))
@ -723,17 +758,21 @@ class Display:
if num_lines:
# centered text under that
y = CHARS_H - num_lines
prev_x = 0
for line in parts:
self.text(None, y, line)
if not is_addr:
self.text(None, y, line)
else:
prev_x = self._draw_addr(y, line, prev_x=prev_x)
y += 1
if idx_hint:
lh = len(idx_hint)
assert lh <= 10
if lh > 6:
if lh > 5:
# needs 2 lines
self.text(-1, 0, idx_hint[:6])
self.text(-1, 1, idx_hint[6:])
self.text(-1, 0, idx_hint[:5])
self.text(-1, 1, idx_hint[5:])
else:
self.text(-1, 0, idx_hint)

View File

@ -362,12 +362,6 @@ class MenuSystem:
self.cursor = 0
self.ypos = 0
def goto_n(self, n):
# goto N from top of (current) screen
# change scroll only if needed to make it visible
self.cursor = max(min(n + self.ypos, self.count-1), 0)
self.ypos = max(self.cursor - n, 0)
def goto_idx(self, n):
# skip to any item, force cusor near middle of screen
n = self.count-1 if n >= self.count else n
@ -474,7 +468,7 @@ class MenuSystem:
self.ypos = 0
elif '1' <= key <= '9':
# jump down, based on screen postion
self.goto_n(ord(key)-ord('1'))
self.goto_idx(ord(key)-ord('1'))
elif key in self.shortcuts:
# run the function, if predicate allows
m = self.shortcuts[key]
@ -489,7 +483,7 @@ class MenuSystem:
return self.items[self.cursor]
# search downwards for a menu item that starts with indicated letter
# if found, select it but dont drill down
# if found, select it but don't drill down
lst = list(range(self.cursor+1, self.count)) + list(range(0, self.cursor))
for n in lst:
if self.items[n].label[0].upper() == key.upper():

View File

@ -13,7 +13,7 @@ from wallet import BaseStorageWallet
from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause
from files import CardSlot, CardMissingError, needs_microsd
from utils import problem_file_line, xfp2str, addr_fmt_label, truncate_address, to_ascii_printable, swab32
from utils import problem_file_line, xfp2str, to_ascii_printable, swab32, show_single_address
from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC, KEY_ENTER
@ -158,9 +158,8 @@ class MiniScriptWallet(BaseStorageWallet):
ik = Key.from_string(self.key)
if ik.origin:
res.append(ik.origin.psbt_derivation())
elif not isinstance(ik.node, bytes):
if ik.is_provably_unspendable:
res.append([swab32(ik.node.my_fp())])
elif ik.is_provably_unspendable:
res.append([swab32(ik.node.my_fp())])
for k in self.keys:
k = Key.from_string(k)
@ -241,7 +240,7 @@ class MiniScriptWallet(BaseStorageWallet):
async def _detail(self, new_wallet=False, is_duplicate=False, short=False):
s = addr_fmt_label(self.addr_fmt) + "\n\n"
s = chains.addr_fmt_label(self.addr_fmt) + "\n\n"
if self.taproot:
s += self.taproot_internal_key_detail(short=short)
@ -281,14 +280,10 @@ class MiniScriptWallet(BaseStorageWallet):
if short:
s += note
else:
if isinstance(key.node, bytes):
s += b2a_hex(key.node).decode()
s += self.key
if type(key) is Key:
# it is unspendable, BUT not unspend(
s += "\n (%s)" % note
else:
s += self.key
if type(key) is Key:
# it is unspendable, BUT not unspend(
s += "\n (%s)" % note
s += "\n\n"
else:
xfp, deriv, xpub = key.to_cc_data()
@ -421,7 +416,7 @@ class MiniScriptWallet(BaseStorageWallet):
msg += '.../%d =>\n' % idx
addrs.append(addr)
msg += truncate_address(addr) + '\n\n'
msg += show_single_address(addr) + '\n\n'
dis.progress_sofar(idx - start + 1, n)
return msg, addrs
@ -878,7 +873,6 @@ class Miniscript:
keys = self.keys
# provably unspendable taproot internal key is not covered here
# all other keys (miniscript,tapscript) require key origin info
assert all(k.origin for k in keys), "Key origin info is required"
assert len(keys) == len(set(keys)), "Insane"
if taproot:
forbiden = (Sortedmulti, Multi)

View File

@ -11,7 +11,6 @@ def make_flash_fs():
os.VfsLfs2.mkfs(fl)
os.mount(fl, '/flash')
os.mkdir('/flash/settings')
def make_psram_fs():

View File

@ -5,7 +5,7 @@
import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson, version
from ubinascii import hexlify as b2a_hex
from utils import xfp2str, str2xfp, cleanup_deriv_path, keypath_to_str, to_ascii_printable
from utils import str_to_keypath, problem_file_line, check_xpub, truncate_address, get_filesize
from utils import str_to_keypath, problem_file_line, check_xpub, get_filesize, show_single_address
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys
from ux import import_export_prompt, ux_enter_bip32_index, show_qr_code, ux_enter_number, OK, X
from files import CardSlot, CardMissingError, needs_microsd
@ -13,7 +13,7 @@ from descriptor import Descriptor
from miniscript import Key, Sortedmulti, Number, Multi
from desc_utils import multisig_descriptor_template
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_P2TR
from menu import MenuSystem, MenuItem, NonDefaultMenuItem
from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem
from opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue
from glob import settings
@ -473,7 +473,7 @@ class MultisigWallet(BaseStorageWallet):
msg += '.../%d/%d =>\n' % (change, idx)
addrs.append(addr)
msg += truncate_address(addr) + '\n\n'
msg += show_single_address(addr) + '\n\n'
dis.progress_sofar(idx - start + 1, n)
return msg, addrs
@ -552,7 +552,7 @@ class MultisigWallet(BaseStorageWallet):
# obscure case: xpub isn't deep enough to represent
# indicated path... not wrong really.
too_shallow = True
continue
dp = 0
for sp in path[dp:]:
assert not (sp & 0x80000000), 'hard deriv'
@ -1172,7 +1172,6 @@ def disable_checks_chooser():
return int(MultisigWallet.disable_checks), ch, xset
async def disable_checks_menu(*a):
from menu import start_chooser
if not MultisigWallet.disable_checks:
ch = await ux_show_story('''\
@ -1205,7 +1204,6 @@ def psbt_xpubs_policy_chooser():
async def trust_psbt_menu(*a):
# show a story then go into chooser
from menu import start_chooser
ch = await ux_show_story('''\
This setting controls what the Coldcard does \
@ -1230,25 +1228,23 @@ exists, otherwise 'Verify'.''')
if ch == 'x': return
start_chooser(psbt_xpubs_policy_chooser)
def unsorted_ms_chooser():
ch = ['Do Not Allow', 'Allow']
def unsort_ms_chooser():
def xset(idx, text):
settings.set('unsort_ms', idx)
from actions import goto_top_menu
goto_top_menu()
if idx:
settings.set('unsort_ms', idx)
else:
settings.remove_key('unsort_ms')
return settings.get('unsort_ms', 0), ch, xset
return settings.get('unsort_ms', 0), ['Do Not Allow', 'Allow'], xset
async def unsorted_ms_menu(*a):
from menu import start_chooser
if not settings.get("unsort_ms", None):
ch = await ux_show_story(
'With this setting ON, it is allowed to import and operate'
' "multi(...)" unsorted multisig wallets that do not follow BIP-67.'
' It is of CRUCIAL importance for unsorted wallets, to backup multisig descriptor'
' and preserve order of the keys in it.'
'Enable this to allow import and operation with'
' "multi(...)" unsorted multisig wallets that DO NOT follow BIP-67.'
' It is of CRUCIAL importance to backup multisig descriptor for unsorted wallets'
' in order to preserve key ordering.'
' Many popular wallets like Sparrow and Electrum do NOT support "multi(...)".'
'\n\nUSE AT YOUR OWN RISK. Disabling BIP-67 is discouraged!'
'\n\nPress (4) to confirm allowing "multi(...)"', escape='4')
@ -1269,7 +1265,7 @@ async def unsorted_ms_menu(*a):
)
return
start_chooser(unsorted_ms_chooser)
start_chooser(unsort_ms_chooser)
class MultisigMenu(MenuSystem):
@ -1298,9 +1294,9 @@ class MultisigMenu(MenuSystem):
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
rv.append(MenuItem('Skip Checks?', f=disable_checks_menu))
rv.append(NonDefaultMenuItem('Unsorted Multisig' if version.has_qwerty else "Unsorted Multi",
'unsort_ms',
f=unsorted_ms_menu))
rv.append(NonDefaultMenuItem(
'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?',
'unsort_ms', f=unsorted_ms_menu))
return rv

View File

@ -7,7 +7,7 @@
# - has GPIO signal "??" which is multipurpose on its own pin
# - this chip chosen because it can disable RF interaction
#
import utime, ngu, ndef, stash
import utime, ngu, ndef, stash, chains
from uasyncio import sleep_ms
import uasyncio as asyncio
from ustruct import pack, unpack
@ -15,7 +15,7 @@ from ubinascii import unhexlify as a2b_hex
from ubinascii import b2a_base64, a2b_base64
from ux import ux_show_story, ux_wait_keydown, OK, X
from utils import B2A, problem_file_line, parse_addr_fmt_str, txid_from_fname
from utils import B2A, problem_file_line, txid_from_fname
from public_constants import AF_CLASSIC
from charcodes import KEY_ENTER, KEY_CANCEL
@ -726,7 +726,7 @@ class NFCHandler:
else:
subpath, addr_fmt_str = winner
try:
addr_fmt = parse_addr_fmt_str(addr_fmt_str)
addr_fmt = chains.parse_addr_fmt_str(addr_fmt_str)
except AssertionError as e:
await ux_show_story(str(e))
return
@ -737,42 +737,21 @@ class NFCHandler:
await the_ux.interact() # need this otherwise NFC animation takes over
async def start_msg_sign(self):
from auth import UserAuthorizedAction, ApproveMessageSign
from ux import the_ux
UserAuthorizedAction.cleanup()
from auth import approve_msg_sign
def f(m):
m = m.decode()
split_msg = m.split("\n")
if 1 <= len(split_msg) <= 3:
return split_msg
return m
winner = await self._nfc_reader(f, 'Unable to find correctly formated message to sign.')
if not winner:
return
if len(winner) == 1:
text = winner[0]
subpath = "m"
addr_fmt = AF_CLASSIC
elif len(winner) == 2:
text, subpath = winner
addr_fmt = AF_CLASSIC # maybe default to native segwit?
else:
# len(winner) == 3
text, subpath, addr_fmt = winner
await approve_msg_sign(None, None, None, approved_cb=self.msg_sign_done,
msg_sign_request=winner)
UserAuthorizedAction.check_busy(ApproveMessageSign)
try:
UserAuthorizedAction.active_request = ApproveMessageSign(
text, subpath, addr_fmt, approved_cb=self.msg_sign_done
)
the_ux.push(UserAuthorizedAction.active_request)
except AssertionError as exc:
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
return
async def msg_sign_done(self, signature, address, text):
from auth import rfc_signature_template_gen

View File

@ -50,7 +50,7 @@ Press ENTER to enable and get started otherwise CANCEL.''',
return NotesMenu(NotesMenu.construct())
async def get_a_password(old_value):
async def get_a_password(old_value, min_len=0, max_len=128):
# Get a (new) password as a string.
# - does some fun generation as well.
@ -104,9 +104,9 @@ async def get_a_password(old_value):
handlers = {KEY_F1: _pick_12, KEY_F2: _pick_24, KEY_F3: _pick_dense,
KEY_F4: _do_dumb, KEY_F6: _toggle_case, KEY_F5: _bip85}
return await ux_input_text(old_value, confirm_exit=False, max_len=128, scan_ok=True,
b39_complete=True, prompt='Password', placeholder='(optional)',
funct_keys=(fmsg, handlers))
return await ux_input_text(old_value, confirm_exit=False, max_len=max_len, min_len=min_len,
scan_ok=True, b39_complete=True, prompt='Password',
placeholder='(optional)', funct_keys=(fmsg, handlers))
class NotesMenu(MenuSystem):
@ -118,7 +118,8 @@ class NotesMenu(MenuSystem):
MenuItem('New Password', f=cls.new_note, arg='p'),
ShortcutItem(KEY_QR, f=cls.quick_create)]
if not NoteContent.count():
cnt = NoteContent.count()
if not cnt:
rv = news + [ MenuItem('Disable Feature', f=cls.disable_notes) ]
else:
rv = []
@ -129,6 +130,9 @@ class NotesMenu(MenuSystem):
rv.append(MenuItem('Export All', f=cls.export_all))
if cnt >= 2:
rv.append(MenuItem('Sort By Title', f=cls.sort_titles))
rv.append(MenuItem('Import', f=import_from_other))
return rv
@ -137,6 +141,14 @@ class NotesMenu(MenuSystem):
async def export_all(cls, *a):
await start_export(NoteContent.get_all())
@classmethod
async def sort_titles(cls, menu, _, item):
# sort by title, one time and then reconstruct menu
NoteContent.sort_all()
# force redraw
menu.update_contents()
@classmethod
async def quick_create(cls, menu, _, item):
# using QR, created a Note (never a password) with auto-generated title.
@ -232,6 +244,17 @@ class NoteContentBase:
# how many do we have?
return len(settings.get('notes', []))
@classmethod
def sort_all(cls):
# sort and resave all notes based on title
# - careful: self.idx values will be wrong for any existing instances
# - 'title' is only common field to subclasses
notes = cls.get_all()
notes.sort(key=lambda j: j.title.lower())
settings.put('notes', [n.serialize() for n in notes])
settings.save()
async def delete(self, *a):
# Remove note
ok = await ux_confirm("Everything about this note/password will be lost.")
@ -298,6 +321,15 @@ class NoteContentBase:
# single export
await start_export([self])
async def sign_txt_msg(self, a, b, item):
from auth import ux_sign_msg, msg_signing_done
txt = item.arg
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
def sign_misc_menu_item(self):
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc)
class PasswordContent(NoteContentBase):
# "Passwords" have a few more fields and are more structured
flds = ['title', 'user', 'password', 'site', 'misc' ]
@ -317,6 +349,7 @@ class PasswordContent(NoteContentBase):
MenuItem('Edit Metadata', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Change Password', f=self.change_pw),
self.sign_misc_menu_item(),
ShortcutItem(KEY_QR, f=self.view_qr),
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='password'),
]
@ -446,6 +479,7 @@ class NoteContent(NoteContentBase):
MenuItem('Edit', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Export', f=self.export),
self.sign_misc_menu_item(),
ShortcutItem(KEY_QR, f=self.view_qr),
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
]

View File

@ -8,13 +8,13 @@
# - recover from empty/blank/failed chips w/o user action
#
# Result:
# - up to 4k of values supported (after json encoding)
# - encrypted and stored in SPI flash, in last 128k area
# - up to a few k of values supported (after json encoding)
# - encrypted and stored in main flash, in a dedicated 512k area
# - AES encryption key is derived from actual wallet secret
# - if logged out, then use fixed key instead (ie. it's public)
# - you cannot move data between slots because AES-CTR with CTR seed based on slot #
# - SHA-256 check on decrypted data
# - (Mk4) each slot is a file on /flash/settings
# - each "slot" is a file in /flash/settings; in Mk1-3 was SPI flash block
# - os.sync() not helpful because block device under filesystem doesnt implement it
#
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr, version
@ -84,10 +84,11 @@ from utils import call_later_ms
# prelogin settings - do not need to be part of other saved settings
# PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"]
# keep these settings only if unspecified on the other end
KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz", "b39skip",
"axskip", "del", "pms", "idle_to", "batt_to", "bright"]
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
"axskip", "del", "pms", "idle_to", "batt_to",
"bright"]
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words']
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw"]
NUM_SLOTS = const(100)
SLOTS = range(NUM_SLOTS)
@ -177,6 +178,13 @@ class SettingsObject:
return (blocks-bfree) / blocks
def _open_file(self, pos, mode='rb'):
if 'w' in mode:
# make directory, when needed (recovery/robustness)
try:
os.stat(MK4_WORKDIR)
except OSError: # ENOENT
os.mkdir(MK4_WORKDIR[:-1])
return open(MK4_FILENAME(pos), mode)
def _slot_is_blank(self, pos, buf):
@ -193,13 +201,13 @@ class SettingsObject:
fn = MK4_FILENAME(pos)
try:
os.remove(fn)
except Exception:
# Error (ENOENT) expected here when saving first time, because the
except:
# OSError (ENOENT) expected here when saving first time, because the
# "old" slot was not in use
pass
def _read_slot(self, pos, decryptor):
# Mk4 is just reading a binary file and decrypt as we go.
# read a binary file and decrypt as we go.
with self._open_file(pos) as fd:
# missing ftell(), so emulate
ln = fd.seek(0, 2)
@ -244,9 +252,12 @@ class SettingsObject:
fd.write(aes(chk.digest()))
def _used_slots(self):
# mk4: faster list of slots in use; doesn't open them
files = os.listdir(MK4_WORKDIR)
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
# list of slots in use; doesn't open them
try:
files = os.listdir(MK4_WORKDIR)
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
except:
return []
def _nonempty_slots(self, dis=None):
# generate slots that are non-empty
@ -393,8 +404,9 @@ class SettingsObject:
set = put
def remove_key(self, kn):
self.current.pop(kn, None)
self.changed()
if kn in self.current:
self.current.pop(kn, None)
self.changed()
def merge_previous_active(self, previous):
import pyb
@ -452,11 +464,8 @@ class SettingsObject:
call_later_ms(250, self.write_out)
def find_spot(self, not_here=0):
# search for a blank sector to use
# - check randomly and pick first blank one (wear leveling, deniability)
# - we will write and then erase old slot
# search for a blank slot to use
# - if "full", blow away a random one
# on mk4, use the filesystem to see what's already taken
avail = set(SLOTS) - set(self._used_slots())
avail.discard(not_here)

View File

@ -7,8 +7,8 @@ from glob import settings
from ucollections import namedtuple
from ubinascii import hexlify as b2a_hex
from exceptions import UnknownAddressExplained
from utils import problem_file_line, show_single_address
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR
from utils import problem_file_line
# Track many addresses, but in compressed form
# - map from random Bech32/Base58 payment address to (wallet) + keypath
@ -217,8 +217,7 @@ class OwnershipCache:
addr_fmt = ch.possible_address_fmt(addr)
if not addr_fmt:
# might be valid address over on testnet vs mainnet
nm = ch.name if ch.ctype != 'BTC' else 'Bitcoin Mainnet'
raise UnknownAddressExplained('That address is not valid on ' + nm)
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
possibles = []
@ -256,7 +255,7 @@ class OwnershipCache:
if af == addr_fmt and acct_num:
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
possibles.append(w)
except ValueError: pass # if not single sig address format
except (KeyError, ValueError): pass # if not single sig address format
if not possibles:
# can only happen w/ scripts; for single-signer we have things to check
@ -316,31 +315,52 @@ class OwnershipCache:
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
from ux import ux_show_story, show_qr_code
from charcodes import KEY_QR
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from public_constants import AFC_BECH32, AFC_BECH32M
try:
wallet, subpath = OWNERSHIP.search(addr)
is_complex = isinstance(wallet, MultisigWallet) or isinstance(wallet, MiniScriptWallet)
msg = addr
sp = None
msg = show_single_address(addr)
msg += '\n\nFound in wallet:\n ' + wallet.name
if hasattr(wallet, "render_path"):
msg += '\nDerivation path:\n ' + wallet.render_path(*subpath)
if version.has_qwerty:
esc = KEY_QR
sp = wallet.render_path(*subpath)
msg += '\nDerivation path:\n ' + sp
if is_complex:
esc = ""
else:
msg += '\n\nPress (1) for QR'
esc = '1'
esc = "0"
msg += "\n\nPress (0) to sign message with this key."
title = "Verified"
if version.has_qwerty:
esc += KEY_QR
title += " Address"
else:
msg += ' (1) for address QR'
esc += '1'
title += "!"
while 1:
ch = await ux_show_story(msg, title="Verified Address",
escape=esc, hint_icons=KEY_QR)
if ch != esc: break
await show_qr_code(addr,
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
msg=addr)
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
if ch in ("1"+KEY_QR):
await show_qr_code(
addr,
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
msg=addr, is_addrs=True
)
elif not is_complex and (ch == "0"): # only singlesig
from auth import sign_with_own_address
await sign_with_own_address(sp, wallet.addr_fmt)
else:
break
except UnknownAddressExplained as exc:
await ux_show_story(addr + '\n\n' + str(exc), title="Unknown Address")
await ux_show_story(show_single_address(addr) + '\n\n' + str(exc), title="Unknown Address")
except Exception as e:
await ux_show_story('Ownership search failed.\n\n%s\n%s' % (e, problem_file_line(e)))
@ -377,8 +397,6 @@ class OwnershipCache:
# - if they explore it (non-zero subaccount)
# - if they sign those paths
# - but ignore testnet vs. not
from glob import settings
if subaccount == 0:
# only interested in non-zero subaccounts
return

View File

@ -473,6 +473,7 @@ class PinAttempt:
def tmp_secret(self, encoded, chain=None, bip39pw=''):
# Use indicated secret and stop using the SE; operate like this until reboot
from glob import settings
from utils import xfp2str
from nvstore import SettingsObject
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
@ -483,7 +484,9 @@ class PinAttempt:
target_nvram_key = None
if encoded is not None:
# disallow using master seed as temporary
master_err = "Cannot use master seed as temporary."
xfp = xfp2str(settings.master_get("xfp", 0))
master_err = ("Cannot use master seed as temporary. BUT you have just successfully "
"tested recovery of your master seed [%s].") % xfp
target_nvram_key = settings.hash_key(val)
if SettingsObject.master_nvram_key:
assert self.tmp_value

View File

@ -2185,6 +2185,7 @@ class psbtObject(psbtProxy):
# check the pubkey of this BIP-32 node
if pubkey == node.pubkey():
good += 1
OWNERSHIP.note_subpath_used(subpath)
if oup.taproot_subpaths:
for xonly_pk, val in oup.taproot_subpaths.items():
@ -2201,7 +2202,6 @@ class psbtObject(psbtProxy):
# check the pubkey of this BIP-32 node
if xonly_pk == node.pubkey()[1:]:
good += 1
OWNERSHIP.note_subpath_used(subpath)
if not good:
@ -2425,17 +2425,16 @@ class psbtObject(psbtProxy):
stash.blank_object(node)
del sk, node
# Could remove sighash from input object - it is not required, takes space,
# and is already in signature or is implicit by not being part of the
# signature (taproot SIGHASH_DEFAULT)
## inp.sighash = None
success.add(in_idx)
gc.collect()
if self.is_v2:
self.set_modifiable_flag(inp)
# drop sighash if default (SIGHASH_ALL)
if inp.sighash == SIGHASH_ALL:
inp.sighash = None
# done.
dis.progress_bar_show(1)

View File

@ -7,6 +7,7 @@ from files import CardSlot, CardMissingError, needs_microsd
from ux import ux_dramatic_pause, ux_confirm, ux_show_story, OK, X
from utils import xfp2str, problem_file_line, B2A
from menu import MenuItem, MenuSystem
from glob import settings
class PassphraseSaver:
@ -110,7 +111,6 @@ class PassphraseSaverMenu(MenuSystem):
from ux import ux_show_story
from seed import set_bip39_passphrase
from pincodes import pa
from glob import settings
bypass_tmp = True
pw, expect_xfp = item.arg
@ -253,7 +253,6 @@ class MicroSD2FA(PassphraseSaver):
@classmethod
def get_nonces(cls):
# this is the only setting: list of nonce values we have saved to various cards
from glob import settings
return settings.get('sd2fa') or []
def read_card(self):
@ -288,7 +287,6 @@ class MicroSD2FA(PassphraseSaver):
except:
# die. wrong
import callgate
from glob import settings
settings.remove_key("sd2fa")
settings.save()
callgate.fast_wipe(silent=False)
@ -353,8 +351,6 @@ class MicroSD2FA(PassphraseSaver):
async def remove(self, nonce):
# remove indicated nonce from records
# - doesn't delete file, since might not have card anymore and useless w/o nonce
from glob import settings
v = self.get_nonces()
assert nonce in v, 'missing card nonce'
v2 = [i for i in v if i != nonce]

View File

@ -17,13 +17,14 @@ MAX_V11_CHAR_LIMIT = const(321)
class QRDisplaySingle(UserInteraction):
# Show a single QR code for (typically) a list of addresses, or a single value.
def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None):
def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None, is_addrs=False):
self.is_alnum = is_alnum
self.idx = 0 # start with first address
self.invert = False # looks better, but neither mode is ideal
self.addrs = addrs
self.sidebar = sidebar
self.start_n = start_n
self.is_addrs = is_addrs
self.msg = msg
self.qr_data = None
@ -67,13 +68,16 @@ class QRDisplaySingle(UserInteraction):
# make the QR, if needed.
if not self.qr_data:
dis.busy_bar(True)
self.calc_qr(body)
try:
self.calc_qr(body)
except Exception:
dis.busy_bar(False)
raise
# draw display
dis.busy_bar(False)
dis.draw_qr_display(self.qr_data, self.msg or body, self.is_alnum,
self.sidebar, self.idx_hint(), self.invert)
self.sidebar, self.idx_hint(), self.invert, is_addr=self.is_addrs)
async def interact_bare(self):
from glob import NFC, dis

View File

@ -964,7 +964,6 @@ class SeedVaultMenu(MenuSystem):
@classmethod
def construct(cls):
# Dynamic menu with user-defined names of seeds shown
from glob import settings
from pincodes import pa
if pa.is_deltamode():
@ -1209,7 +1208,7 @@ class PassphraseMenu(MenuSystem):
@classmethod
async def add_numbers(cls, *a):
# Mk4 only: add some digits (quick, easy)
pw = await ux_input_numbers(cls.pp_sofar, cls.check_length)
pw = await ux_input_numbers(cls.pp_sofar)
if pw is not None:
cls.pp_sofar = pw
cls.check_length()

View File

@ -37,7 +37,17 @@ if not has_lcd:
x, y, msg = a[0:3]
global contents
contents[y] = msg
is_idx = False
if x == 0 and len(msg) == 1:
# something on index zero - is it index num in top right with QR display?
# msg will just single int without any dot or smthg
try:
int(msg)
is_idx = True
except: pass
if not is_idx:
contents[y] = msg
#print('text (%s, %s): %s' % (x,y, msg))

View File

@ -6,6 +6,7 @@ import gc, sys, ustruct, ngu, chains, ure, time, version, uos, uio, bip39
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64
from charcodes import OUT_CTRL_ADDRESS
from uhashlib import sha256
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR, MAX_PATH_DEPTH
from public_constants import AF_P2WSH, AF_P2WSH_P2SH
@ -13,6 +14,14 @@ from public_constants import AF_P2WSH, AF_P2WSH_P2SH
B2A = lambda x: str(b2a_hex(x), 'ascii')
STD_DERIVATIONS = {
"p2pkh": "m/44h/{chain}h/0h/0/0",
"p2sh-p2wpkh": "m/49h/{chain}h/0h/0/0",
"p2wpkh-p2sh": "m/49h/{chain}h/0h/0/0",
"p2wpkh": "m/84h/{chain}h/0h/0/0",
"p2tr": "m/86h/{chain}h/0h/0/0",
}
try:
from font_iosevka import FontIosevka
DOUBLE_WIDE = FontIosevka.DOUBLE_WIDE
@ -206,16 +215,17 @@ def is_printable(s):
return False
return True
def to_ascii_printable(s, strip=False):
def to_ascii_printable(s, strip=False, only_printable=True):
try:
s = str(s, 'ascii')
if strip:
s = s.strip()
assert is_ascii(s)
assert is_printable(s)
if only_printable:
assert is_printable(s)
return s
except:
raise AssertionError('must be ascii printable')
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
def problem_file_line(exc):
@ -262,7 +272,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# regex for valid chars, m at start, maybe /*h or /* at end sometimes
mat = ure.match(r"(m|m/|)[0-9/h]*" + ('' if not allow_star else r"(\*h|\*|)"), s)
assert mat.group(0) == s, "invalid characters"
assert mat.group(0) == s, "invalid characters in path"
parts = s.split('/')
@ -421,7 +431,7 @@ def check_firmware_hdr(hdr, binary_size):
ok = (hw_compat & MK_4_OK)
elif hw_label == 'q1':
ok = (hw_compat & MK_Q1_OK)
if not ok:
return "That firmware doesn't support this version of Coldcard hardware (%s)."%hw_label
@ -486,11 +496,23 @@ def word_wrap(ln, w):
return
while ln:
# find a space in (width) first part of remainder
sp = ln.rfind(' ', 0, w-1)
if sp == -1:
if ln[0] == OUT_CTRL_ADDRESS:
# special handling for lines w/ payment address in them
# - add same marker to newly split lines
addr = ln[1:]
# - 3 4-char groups on Mk4
# - 6 4-char groups on Q
aw = 24 if version.has_qwerty else 12
pos = 0
while pos < len(addr):
yield OUT_CTRL_ADDRESS + addr[pos:pos+aw]
pos += aw
return
# bad-break the line
sp = min(txtlen(ln), w)
nsp = sp
@ -510,23 +532,6 @@ def word_wrap(ln, w):
yield left
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC, AF_P2TR]:
return addr_fmt
addr_fmt = addr_fmt.lower()
if addr_fmt in ("p2sh-p2wpkh", "p2wpkh-p2sh"):
return AF_P2WPKH_P2SH
elif addr_fmt == "p2pkh":
return AF_CLASSIC
elif addr_fmt == "p2wpkh":
return AF_P2WPKH
elif addr_fmt == "p2tr":
return AF_P2TR
else:
raise ValueError("Unsupported address format: '%s'" % addr_fmt)
def parse_extended_key(ln, private=False):
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
# - can handle any garbage line
@ -563,17 +568,6 @@ def chunk_writer(fd, body):
dis.progress_bar_show(1)
def addr_fmt_label(addr_fmt):
return {
AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH",
AF_P2TR: "Taproot P2TR",
AF_P2WSH: "Segwit P2WSH",
AF_P2WSH_P2SH: "P2SH-P2WSH"
}[addr_fmt]
def pad_raw_secret(raw_sec_str):
# Chip can hold 72-bytes as a secret
# every secret has 0th byte as marker
@ -758,11 +752,18 @@ def check_xpub(xfp, xpub, deriv, expect_chain, my_xfp, disable_checks=False):
# path length of derivation given needs to match xpub's depth
if not disable_checks:
p_len = deriv.count('/')
assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % (
p_len, depth, xfp2str(xfp))
if p_len:
# only check this for keys that have origin derivation
# originless keys are expected to be blinded
assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % (
p_len, depth, xfp2str(xfp)
)
else:
# depth can be more than zero here - keys can be blinded
assert xfp == swab32(node.my_fp()), "xpub xfp wrong %s" % xfp2str(xfp)
if xfp == my_xfp:
# its supposed to be my key, so I should be able to generate pubkey
# it's supposed to be my key, so I should be able to generate pubkey
# - might indicate collision on xfp value between co-signers,
# and that's not supported
with stash.SensitiveValues() as sv:
@ -774,21 +775,16 @@ def check_xpub(xfp, xpub, deriv, expect_chain, my_xfp, disable_checks=False):
# - this has effect of stripping SLIP-132 confusion away
return xfp == my_xfp, (xfp, deriv, chain.serialize_public(node, AF_P2SH))
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
if not version.has_qwerty:
# - 16 chars screen width
# - but 2 lost at left (menu arrow, corner arrow)
# - want to show not truncated on right side
return addr[0:6] + '' + addr[-6:]
else:
# tons of space on Q1
return addr[0:12] + '' + addr[-12:]
def encode_seed_qr(words):
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
def show_single_address(addr):
# insert some metadata so display layer can do special rendering
# of addresses (based on hardware capabilities)
return OUT_CTRL_ADDRESS + addr
def chunk_address(addr):
# useful to show payment addresses specially
return [addr[i:i+4] for i in range(0, len(addr), 4)]
# EOF

View File

@ -7,7 +7,8 @@ from queues import QueueEmpty
import utime, gc, version
from utils import word_wrap
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR,
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL)
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE)
from exceptions import AbortInteraction
DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
@ -181,8 +182,8 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False,
lines = []
if title:
# kinda weak rendering but it works.
lines.append('\x01' + title)
# render the title line specially, see display/lcd_display.py
lines.append(OUT_CTRL_TITLE + title)
if version.has_qwerty:
# big screen always needs blank after title
@ -321,14 +322,14 @@ def abort_and_push(m):
the_ux.push(m)
numpad.abort_ux()
async def show_qr_codes(addrs, is_alnum, start_n):
async def show_qr_codes(addrs, is_alnum, start_n, **kw):
from qrs import QRDisplaySingle
o = QRDisplaySingle(addrs, is_alnum, start_n, sidebar=None)
o = QRDisplaySingle(addrs, is_alnum, start_n, **kw)
await o.interact_bare()
async def show_qr_code(data, is_alnum=False, msg=None):
async def show_qr_code(data, is_alnum=False, msg=None, **kw):
from qrs import QRDisplaySingle
o = QRDisplaySingle([data], is_alnum, msg=msg)
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
await o.interact_bare()
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):

View File

@ -122,7 +122,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=False):
# cleanup leading zeros and such
value = str(min(int(value), max_value))
async def ux_input_numbers(val, validate_func):
async def ux_input_numbers(val):
# collect a series of digits
from glob import dis
from display import FontTiny
@ -161,7 +161,6 @@ async def ux_input_numbers(val, validate_func):
ch = await press.wait()
if ch == 'y':
val += here
validate_func()
return val
elif ch == 'x':
if here:

View File

@ -2,7 +2,7 @@
#
# ux_q1.py - UX/UI interactions that are Q1 specific and use big screen, keyboard.
#
import utime, gc, ngu, sys
import utime, gc, ngu, sys, chains
import uasyncio as asyncio
from uasyncio import sleep_ms
from charcodes import *
@ -12,7 +12,10 @@ import bip39
from decoders import decode_qr_result
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from utils import problem_file_line
from ubinascii import b2a_base64
from utils import problem_file_line, show_single_address
from public_constants import MSG_SIGNING_MAX_LENGTH
from glob import numpad # may be None depending on import order, careful
class PressRelease:
@ -138,7 +141,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=False):
# cleanup leading zeros and such
value = str(min(int(value), max_value))
async def ux_input_numbers(val, validate_func):
async def ux_input_numbers(val):
# collect a series of digits
# - not wanted on Q1; just get the digits mixed in w/ the text.
pass
@ -159,7 +162,6 @@ async def ux_input_text(value, confirm_exit=False, hex_only=False, max_len=100,
# to make longer single-line value onto screen
# - confirm_exit default False here, because so easy to re-enter w/ qwerty, True on mk4
from glob import dis
from ux import ux_show_story
MAX_LINES = 7 # without scroll
can_scroll = False
@ -952,6 +954,20 @@ class QRScannerInteraction:
await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
return
if what == "vmsg":
data, = vals
from auth import verify_armored_signed_msg
await verify_armored_signed_msg(data)
return
if what == "smsg":
data, = vals
from auth import approve_msg_sign, msg_signing_done
await approve_msg_sign(None, None, None,
msg_sign_request=data, kill_menu=True,
approved_cb=msg_signing_done)
return
if what == 'text' or what == 'xpub':
# we couldn't really decode it.
txt, = vals
@ -1069,7 +1085,7 @@ async def ux_visualize_bip21(proto, addr, args):
# - validate address ownership on request
from ux import ux_show_story
msg = addr + '\n\n'
msg = show_single_address(addr) + '\n\n'
args = args or {}
if 'amount' in args:
@ -1108,15 +1124,49 @@ async def ux_visualize_wif(wif_str, kp, compressed, testnet):
msg += "public key sec:\n" + b2a_hex(kp.pubkey().to_bytes(not compressed)).decode() + "\n\n"
await ux_show_story(msg, title="WIF")
async def ux_visualize_textqr(txt, maxlen=200):
async def qr_msg_sign_done(signature, address, text):
from ux import ux_show_story
from auth import rfc_signature_template_gen
from export import export_by_qr
sig = b2a_base64(signature).decode('ascii').strip()
while True:
ch = await ux_show_story("Press ENTER to export signature QR only, "
"(0) to export full RFC template, "
"CANCEL if done.", escape="0")
if ch == "x": break
if ch == "y":
await export_by_qr(sig, "Signature", "U")
if ch == "0":
armored_str = "".join(rfc_signature_template_gen(addr=address, msg=text,
sig=sig))
await show_bbqr_codes("U", armored_str, "Armored MSG")
async def qr_sign_msg(txt):
from auth import ux_sign_msg
await ux_sign_msg(txt, approved_cb=qr_msg_sign_done, kill_menu=True)
async def ux_visualize_textqr(txt, maxlen=MSG_SIGNING_MAX_LENGTH):
# Show simple text. Don't crash on huge things, but be
# able to show a full xpub.
from ux import ux_show_story
if len(txt) > maxlen:
txt_len = len(txt)
escape = "0"
if txt_len > maxlen:
escape = None
txt = txt[0:maxlen] + '...'
await ux_show_story("%s\n\nAbove is text that was scanned. "
"We can't do any more with it." % txt, title="Simple Text")
msg = "%s\n\nAbove is text that was scanned. " % txt
if escape:
msg += " Press (0) to sign the text. "
else:
msg += "We can't do any more with it."
ch = await ux_show_story(title="Simple Text", msg=msg, escape=escape)
if escape and (ch == "0"):
await qr_sign_msg(txt)
async def show_bbqr_codes(type_code, data, msg, already_hex=False):
# Compress, encode and split data, then show it animated...

View File

@ -83,7 +83,6 @@ def probe_system():
hw_label = 'mk4'
has_608 = True
nfc_presence_check() # hardware present; they might not be using it
has_qr = False # QR scanner
num_sd_slots = 1 # might have dual slots on Q1
mk_num = 4
@ -91,7 +90,7 @@ def probe_system():
has_qwerty = False
is_edge = False
supports_hsm = True
has_nfc = True
has_nfc = nfc_presence_check() # hardware present; they might not use it.
cpuid = ckcc.get_cpu_id()
assert cpuid == 0x470 # STM32L4S5VI

View File

@ -4,7 +4,6 @@
#
import chains
from glob import settings
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from stash import SensitiveValues
@ -45,20 +44,10 @@ class MasterSingleSigWallet(WalletABC):
# Construct a wallet based on current master secret, and chain.
# - path is optional, and then we use standard path for addr_fmt
# - path can be overriden when we come here via address explorer
if addr_fmt == AF_P2TR:
n = 'Taproot P2TR'
prefix = path or 'm/86h/{coin_type}h/{account}h'
elif addr_fmt == AF_P2WPKH:
n = 'Segwit P2WPKH'
prefix = path or 'm/84h/{coin_type}h/{account}h'
elif addr_fmt == AF_CLASSIC:
n = 'Classic P2PKH'
prefix = path or 'm/44h/{coin_type}h/{account}h'
elif addr_fmt == AF_P2WPKH_P2SH:
n = 'P2WPKH-in-P2SH'
prefix = path or 'm/49h/{coin_type}h/{account}h'
else:
raise ValueError(addr_fmt)
n = chains.addr_fmt_label(addr_fmt)
purpose = chains.af_to_bip44_purpose(addr_fmt)
prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose
if chain_name:
self.chain = chains.get_chain(chain_name)

View File

@ -70,6 +70,11 @@ Otherwise, press {ok} to continue.'''.format(n=num_parts, ok=OK), escape='2')
raw_secret = bytes(32)
try:
with stash.SensitiveValues() as sv:
if sv.deltamode:
# die rather than give up our secrets
import callgate
callgate.fast_wipe()
words = None
if sv.mode == 'words':
words = bip39.b2a_words(sv.raw).split(' ')
@ -290,6 +295,11 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
if ch == '1':
dis.fullscreen("Wait...")
with stash.SensitiveValues() as sv:
if sv.deltamode:
# die rather than give up our secrets
import callgate
callgate.fast_wipe()
if sv.mode == 'words':
# needs copy here [:] otherwise rewritten with zeros in __exit__
import_xor_parts.append(sv.raw[:])
@ -297,17 +307,20 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
# Add from Seed Vault?
# filter only those that are correct length and type from seed vault
opt = []
for i, (xfp_str, hex_str, _, _) in enumerate(settings.master_get("seeds", [])):
seeds = [] if pa.is_deltamode() else settings.master_get("seeds", [])
for i, (xfp_str, hex_str, _, _) in enumerate(seeds):
raw = pad_raw_secret(hex_str)
if raw[0] & 0x80:
# seed phrase
sk = raw[1:1 + stash.len_from_marker(raw[0])]
if stash.len_to_numwords(len(sk)) == desired_num_words:
opt.append((i, xfp_str, sk))
del seeds
if opt:
escape = "2"
msg = ("Seed Vault is enabled. %d stored seeds have suitable type and length."
"\n\nPress (2) to add from Seed Vault, press %s to continue normally.") % (len(opt), OK)
"\n\nPress (2) to add from Seed Vault and then (1) to select seeds,"
" press %s to continue normally.") % (len(opt), OK)
ch = await ux_show_story(msg, escape=escape)
if ch == 'x': return
if ch == "2":

View File

@ -36,8 +36,8 @@ class FontSmall(FontBase):
_bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3, 7,
14, 5), (0, -3, 7, 14, 7), (0, -3, 7, 14, 9), (0, -3, 7, 14, 10),
(0, -3, 7, 14, 11), (0, -3, 7, 14, 12), (0, -3, 7, 14, 13), (0, -3,
7, 14, 14), (0, 0, 8, 8, 8), (0, 0, 11, 8, 16), (0, 0, 11, 9, 18),
(0, 0, 14, 10, 20)]
7, 14, 14), (0, 0, 5, 2, 2), (0, 0, 8, 8, 8), (0, 0, 11, 8, 16), (0,
0, 11, 9, 18), (0, 0, 14, 10, 20)]
_code_points = [
(range(32, 127), [1, 2, 14, 20, 31, 43, 55, 67, 73, 87, 101, 111, 122,
@ -48,11 +48,11 @@ class FontSmall(FontBase):
755, 767, 779, 791, 803, 815, 827, 842, 854, 866, 880, 892, 904,
916, 928, 940, 954, 968, 980, 992, 1004, 1016, 1028, 1040, 1052,
1067, 1079, 1093, 1106, 1120]),
(range(8226, 8227), [1126]), #
(range(8592, 8593), [1145]), # ←
(range(8594, 8595), [1166]), #
(range(8627, 8628), [1187]), # ↳
(range(8943, 8944), [1208]), # ⋯
(range(8201, 8202), [1126]), #
(range(8226, 8227), [1129]), # •
(range(8592, 8595), [1148, 0, 1169]), # ←
(range(8627, 8628), [1190]), # ↳
(range(8943, 8944), [1211]), # ⋯
]
_bitmaps = b"""\
@ -63,7 +63,7 @@ class FontSmall(FontBase):
\x10\x09\x04\x08\x10\x10\x20\x20\x20\x20\x20\x10\x10\x08\x04\x09\x20\x10\
\x08\x08\x04\x04\x04\x04\x04\x08\x08\x10\x20\x05\x00\x00\x00\x00\x24\x18\
\x7e\x18\x24\x06\x00\x00\x00\x08\x08\x08\x3e\x08\x08\x08\x09\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x18\x30\x20\x40\x0b\x00\x00\x00\x00\x00\x00\x3e\
\x00\x00\x00\x00\x00\x00\x18\x30\x20\x40\x0c\x00\x00\x00\x00\x00\x00\x3e\
\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x38\x10\x08\x02\x02\x04\
\x04\x08\x08\x10\x10\x20\x20\x40\x40\x07\x00\x18\x24\x42\x42\x4a\x52\x42\
\x42\x24\x18\x07\x00\x08\x18\x28\x48\x08\x08\x08\x08\x08\x08\x07\x00\x3c\
@ -118,13 +118,13 @@ class FontSmall(FontBase):
\x3a\x02\x02\x42\x3c\x07\x00\x00\x00\x00\x7e\x02\x04\x08\x10\x20\x7e\x09\
\x06\x08\x08\x08\x08\x08\x30\x08\x08\x08\x08\x08\x06\x08\x10\x10\x10\x10\
\x10\x10\x10\x10\x10\x10\x10\x10\x09\x60\x10\x10\x10\x10\x10\x0c\x10\x10\
\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0d\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0e\x00\x00\x00\x00\x00\x00\
\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0e\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\x00\x00\x0e\
\x00\x00\x10\x00\x10\x00\x10\x00\x10\x20\x10\x30\x1f\xf8\x00\x30\x00\x20\
\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2a\xa0\x00\
\x00\
\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0b\x00\x00\x0e\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0f\x00\x00\x00\
\x00\x00\x00\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0f\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\
\x00\x00\x0f\x00\x00\x10\x00\x10\x00\x10\x00\x10\x20\x10\x30\x1f\xf8\x00\
\x30\x00\x20\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x2a\xa0\x00\x00\
"""

View File

@ -1,13 +1,13 @@
// (c) Copyright 2020-2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
// (c) Copyright 2020-2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
//
// AUTO-generated.
//
// built: 2024-07-04
// version: 6.3.3X
// built: 2025-01-17
// version: 5.4.1
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x58e43060UL;
return 0x5a312880UL;
}

View File

@ -501,28 +501,31 @@ static void psram_init_vfs(fs_user_mount_t *vfs, bool readonly) {
// psram_memset4()
//
static inline void
psram_memset4(void *dest_addr, uint32_t value, uint32_t byte_len)
static void
psram_memset4(void *dest_addr, uint32_t byte_len)
{
// Fast, aligned, and bug-fixing memset
// - PSRAM can starve the internal bus with too many writes, too fast
// - leads to a weird crash where SRAM bus (at least) is locked up, but flash works
// - and/or just call w/ interrupts off for reliable non-crashing behaviour
uint32_t *dest = (uint32_t *)dest_addr;
for(; byte_len; byte_len-=4, dest++) {
*dest = value;
asm("nop; nop; nop;"); // tested value, do not reduce
*dest = 0x12345678;
}
}
// mp_obj_t psram_wipe_and_setup()
//
mp_obj_t psram_wipe_and_setup(mp_obj_t unused_self)
{
// Erase and reformat filesystem
// - you probably should unmount it, before calling this
// Wipe contents for security.
psram_memset4(PSRAM_TOP_BASE, 0x12345678, BLOCK_SIZE * BLOCK_COUNT);
mp_uint_t before = disable_irq();
psram_memset4(PSRAM_TOP_BASE, BLOCK_SIZE * BLOCK_COUNT);
enable_irq(before);
// Build obj to handle blockdev protocol
fs_user_mount_t vfs = {0};

View File

@ -1,13 +1,13 @@
// (c) Copyright 2020-2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
// (c) Copyright 2020-2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
//
// AUTO-generated.
//
// built: 2024-09-12
// version: 1.3.0Q
// built: 2025-02-07
// version: 1.3.1Q
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x592c0860UL;
return 0x5a470860UL;
}

View File

@ -19,7 +19,7 @@ LATEST_RELEASE = $(shell ls -t1 ../releases/*-mk4-*.dfu | head -1)
# Our version for this release.
# - caution, the bootrom will not accept version < 3.0.0
VERSION_STRING = 6.3.4X
VERSION_STRING = 6.3.5X
# keep near top, because defined default target (all)
include shared.mk

View File

@ -16,7 +16,7 @@ BOOTLOADER_DIR = q1-bootloader
LATEST_RELEASE = $(shell ls -t1 ../releases/*-q1-*.dfu | head -1)
# Our version for this release.
VERSION_STRING = 6.3.4QX
VERSION_STRING = 6.3.5QX
# Remove this closer to shipping.
#$(warning "Forcing debug build")

View File

@ -3,7 +3,7 @@
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, bech32, pdb
from subprocess import check_output
from ckcc.protocol import CCProtocolPacker
from helpers import B2A, U2SAT, hash160, taptweak
from helpers import B2A, U2SAT, hash160, taptweak, addr_from_display_format
from base58 import decode_base58_checksum
from bip32 import BIP32Node
from msg import verify_message
@ -560,7 +560,7 @@ def cap_screen_qr(cap_image):
if orig_img.width == 128:
# Mk3/4 - pull out just the QR, blow it up 16x
x, w = 2, 64
x, w = 2, 66
img = orig_img.crop( (x, 0, x+w, w) )
img = ImageOps.expand(img, 16, 0) # add border
img = img.resize( (256, 256))
@ -595,6 +595,42 @@ def cap_screen_qr(cap_image):
return doit
@pytest.fixture
def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
# check we can read QR and that it has exact value expected
# plus text version of address, if any, is right.
from ckcc_protocol.constants import AFC_BECH32
def doit(addr_fmt, expect_addr=None):
qr = cap_screen_qr().decode('ascii')
if (addr_fmt & AFC_BECH32) or (addr_fmt & AFC_BECH32M):
qr = qr.lower()
# check text --if any-- matches QR contents
# - remove spaces and newlines
# - ok if no text, which happens when QR is productively using screen space
# - skips first line, which on Q shows the index number sometimes
# - insists on some spaces
full = cap_screen()
if is_q1:
txt = ''.join(full.split()[2:]).replace('~', '')
else:
txt = ''.join(full.split())
if txt:
assert txt == qr
if is_q1:
# addr is not spaced out on Mk4, but check it was on Q
assert (qr[0:4] + ' ' + qr[4:8]) in full, 'was not spaced out'
if expect_addr is not None:
assert qr == expect_addr
return qr
return doit
@pytest.fixture(scope='module')
def get_pp_sofar(sim_exec):
# get entry value for bip39 passphrase
@ -945,8 +981,9 @@ def reset_seed_words(sim_exec, sim_execfile, simulator):
@pytest.fixture()
def settings_set(sim_exec):
def doit(key, val):
x = sim_exec("settings.set('%s', %r)" % (key, val))
def doit(key, val, prelogin=False):
source = "from nvstore import SettingsObject;SettingsObject.prelogin()" if prelogin else "settings"
x = sim_exec("%s.set('%s', %r)" % (source, key, val))
assert x == ''
return doit
@ -954,8 +991,9 @@ def settings_set(sim_exec):
@pytest.fixture()
def settings_get(sim_exec):
def doit(key, def_val=None):
cmd = f"RV.write(repr(settings.get('{key}', {def_val!r})))"
def doit(key, def_val=None, prelogin=False):
source = "from nvstore import SettingsObject;SettingsObject.prelogin()" if prelogin else "settings"
cmd = f"RV.write(repr({source}.get('{key}', {def_val!r})))"
resp = sim_exec(cmd)
assert 'Traceback' not in resp, resp
return eval(resp)
@ -1829,6 +1867,7 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
need_keypress(key_map["qr"])
time.sleep(0.3)
try:
assert is_q1
file_type, data = readback_bbqr()
if file_type == "J":
return json.loads(data)
@ -1837,7 +1876,6 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
else:
raise NotImplementedError
except:
raise
res = cap_screen_qr().decode('ascii')
try:
return json.loads(res)
@ -1979,6 +2017,7 @@ def check_and_decrypt_backup(microsd_path):
with open(xfn_path, "r") as f:
res = f.read()
os.remove(xfn_path)
return res
return doit
@ -2156,6 +2195,7 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
assert f"Output {i}:" == sa
txt_amount, _, addr = sb.split("\n")
addr = addr_from_display_format(addr)
assert txt_amount == f'{amount / 100000000:.8f} {chain}'
if af == "p2pkh":
if chain == "BTC":
@ -2298,8 +2338,9 @@ from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
from test_ephemeral import verify_ephemeral_secret_ui, get_identity_story, get_seed_value_ux, seed_vault_enable
from test_msg import verify_msg_sign_story, sign_msg_from_text, msg_sign_export, sign_msg_from_address
from test_multisig import import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn
from test_miniscript import offer_minsc_import
from test_miniscript import offer_minsc_import, get_cc_key, bitcoin_core_signer
from test_multisig import make_ms_address, clear_ms, make_myself_wallet, import_multisig
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

View File

@ -44,6 +44,7 @@ addr_fmt_names = {
AF_P2WSH: 'p2wsh',
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
AF_P2WSH_P2SH: 'p2wsh-p2sh',
AF_P2TR: "p2tr",
}

View File

@ -35,7 +35,10 @@ if 'destinations' in expect:
for (val, addr), (idx, txo) in zip(expect['destinations'], p.output_iter()):
assert val == txo.nValue
txt = active_request.render_output(txo)
assert addr in txt
# normalize from display format
address = txt.split("\n")[-2]
assert address[0] == "\x02"
assert addr == address[1:]
assert '%.8f'%(val/1E8) in txt
if 'sw_inputs' in expect:

View File

@ -103,8 +103,11 @@ def xfp2str(xfp):
from struct import pack
return b2a_hex(pack('<I', xfp)).decode('ascii').upper()
def parse_change_back(story):
def addr_from_display_format(dis_addr):
assert dis_addr[0] == '\x02' # OUT_CTRL_ADDRESS
return dis_addr[1:]
def parse_change_back(story):
lines = story.split('\n')
s = lines.index('Change back:')
assert s > 3
@ -113,8 +116,12 @@ def parse_change_back(story):
assert 'address' in lines[s+2]
addrs = []
for y in range(s+3, len(lines)):
if not lines[y]: break
addrs.append(lines[y])
line = lines[y].strip()
if line:
if line[0] == "\x02":
addrs.append(addr_from_display_format(line))
if line.startswith(("3","2","1","m","n","tb1","bc1","bcrt")):
addrs.append(line)
if len(addrs) >= 2:
assert 'to addresses' in lines[s+2]

View File

@ -22,11 +22,12 @@ RFC_SIGNATURE_TEMPLATE = '''\
def parse_signed_message(msg):
msplit = msg.strip().split("\n")
assert msplit[0] == "-----BEGIN BITCOIN SIGNED MESSAGE-----"
assert msplit[2] == "-----BEGIN BITCOIN SIGNATURE-----"
assert msplit[5] == "-----END BITCOIN SIGNATURE-----"
return msplit[1], msplit[3], msplit[4]
msplit = msg.strip().rsplit("\n", 4)
assert msplit[0].startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----\n")
msg = msplit[0].replace("-----BEGIN BITCOIN SIGNED MESSAGE-----\n", "")
assert msplit[1] == "-----BEGIN BITCOIN SIGNATURE-----"
assert msplit[4] == "-----END BITCOIN SIGNATURE-----"
return msg, msplit[2], msplit[3]
def sig_hdr_base(addr_fmt):

View File

@ -138,7 +138,8 @@ class BasicPSBTInput(PSBTSection):
def __eq__(a, b):
if a.sighash != b.sighash:
if a.sighash is not None and b.sighash is not None:
# no sighash == SIGHASH_ALL
if {a.sighash, b.sighash} != {None, 1}:
return False
rv = a.utxo == b.utxo and \

View File

@ -10,6 +10,7 @@ from ckcc_protocol.protocol import CCProtocolPacker
from ckcc_protocol.constants import *
from charcodes import KEY_QR
from constants import msg_sign_unmap_addr_fmt
from helpers import addr_from_display_format
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'"])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR ])
@ -29,7 +30,7 @@ def test_show_addr_usb(dev, press_select, addr_vs_path, path, addr_fmt, is_simul
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'", "m/0h/500h"])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR ])
def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
cap_story, cap_screen_qr, qr_quality_check,
cap_story, verify_qr_address, qr_quality_check,
press_cancel, is_q1):
time.sleep(0.1)
@ -45,8 +46,7 @@ def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
else:
assert path in story
assert addr in story
assert addr in story.split('\n')
assert addr in addr_from_display_format(story.split("\n\n")[0])
# check expected addr was used
addr_vs_path(addr, path, addr_fmt)
@ -55,9 +55,8 @@ def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
need_keypress(KEY_QR if is_q1 else '4')
time.sleep(0.1)
qr = cap_screen_qr().decode('ascii')
assert qr == addr or qr == addr.upper()
verify_qr_address(addr_fmt, addr)
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", [
@ -137,7 +136,7 @@ def test_show_addr_nfc(path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_m
_, story = cap_story()
split_story = story.split("\n\n")
story_addr = split_story[0]
story_addr = addr_from_display_format(split_story[0])
story_path = split_story[1][2:] # remove "= "
if not is_q1:
assert "Press (3) to share via NFC" in story

View File

@ -2,13 +2,13 @@
#
# Test the address explorer.
#
# Only single-sig here. Multisig cases are elsewhere.
# Only single-sig here. Multisig cases are in test_multisig.py.
#
import pytest, time, io, csv, bech32
from ckcc_protocol.constants import *
from bip32 import BIP32Node
from base58 import decode_base58_checksum
from helpers import detruncate_address, hash160
from helpers import detruncate_address, hash160, addr_from_display_format
from charcodes import KEY_QR, KEY_LEFT, KEY_RIGHT
from constants import MAX_BIP32_IDX
@ -53,7 +53,7 @@ def parse_display_screen(cap_story, is_mark3):
d = dict()
for path_raw, addr, empty in zip(*[iter(raw_addrs)]*3):
path = path_raw.split(" =>")[0]
d[path] = addr
d[path] = addr_from_display_format(addr)
assert len(d) == n
return d
return doit
@ -374,9 +374,10 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
@pytest.mark.parametrize('which_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR])
def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_address_explorer,
need_keypress, cap_menu, parse_display_screen, validate_address,
cap_screen_qr, qr_quality_check, nfc_read_text, get_setting,
verify_qr_address, qr_quality_check, nfc_read_text, get_setting,
press_select, press_cancel, is_q1, press_nfc, cap_story,
generate_addresses_file, settings_set, set_addr_exp_start_idx):
generate_addresses_file, settings_set, set_addr_exp_start_idx,
sign_msg_from_address):
path, start_idx = path_sidx
settings_set('aei', True if start_idx else False)
@ -436,8 +437,8 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
time.sleep(.5) # .2 not enuf
m = cap_menu()
assert m[0] == 'Classic P2PKH'
assert m[1] == 'Segwit P2WPKH'
assert m[1] == 'Classic P2PKH'
assert m[0] == 'Segwit P2WPKH'
assert m[2] == 'Taproot P2TR'
assert m[3] == 'P2SH-Segwit'
@ -468,16 +469,12 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
assert 'Showing single addr' in body
assert path in body
addr = body.split("\n")[3]
addr = addr_from_display_format(body.split("\n")[3])
addr_vs_path(addr, path, addr_fmt=which_fmt)
need_keypress(KEY_QR if is_q1 else '4')
qr = cap_screen_qr().decode('ascii')
if which_fmt in (AF_P2WPKH, AF_P2TR):
assert qr == addr.upper()
else:
assert qr == addr
verify_qr_address(which_fmt, addr)
if get_setting('nfc', 0):
# this is actually testing NFC export in qr code menu
@ -500,6 +497,19 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
f_path, f_addr = next(addr_gen)
assert f_path == path
assert f_addr == addr
press_select() # file written
# msg sign
time.sleep(.1)
title, body = cap_story()
if which_fmt == AF_P2TR:
assert "Press (0) to sign message with this key" not in body
else:
assert "Press (0) to sign message with this key" in body
need_keypress('0')
msg = "COLDCARD the rock solid HWW"
sign_msg_from_address(msg, addr, path, which_fmt, "sd", True)
press_cancel()
else:
n = 10
if (start_idx + n) > MAX_BIP32_IDX:
@ -523,9 +533,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
qr_addr_list = []
need_keypress(KEY_QR if is_q1 else '4')
for i in range(n):
qr = cap_screen_qr().decode('ascii')
if which_fmt in (AF_P2WPKH, AF_P2TR):
qr = qr.lower()
qr = verify_qr_address(which_fmt)
qr_addr_list.append(qr)
need_keypress(KEY_RIGHT if is_q1 else "9")
time.sleep(.5)

View File

@ -2,23 +2,104 @@
#
# Testing backups.
#
import pytest, time, json, os, shutil
import pytest, time, json, os, shutil, re
from constants import simulator_fixed_words, simulator_fixed_tprv
from charcodes import KEY_QR
from bip32 import BIP32Node
from mnemonic import Mnemonic
@pytest.fixture
def override_bkpw(goto_home, pick_menu_item, cap_story, need_keypress, seed_story_to_words,
cap_menu, press_select, press_cancel, enter_complex, is_q1):
def purge_current(exit=False):
time.sleep(.1)
title, story = cap_story()
if "(1) to forget current" in story:
need_keypress("1")
time.sleep(.1)
title, story = cap_story()
assert "Delete current stored password?" in story
press_select()
time.sleep(.1)
title, story = cap_story()
assert "(1) to forget current" not in story
if exit:
press_cancel()
def doit(password=None, old_password=None):
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Danger Zone")
pick_menu_item("I Am Developer.")
pick_menu_item("BKPW Override")
time.sleep(.1)
title, story = cap_story()
current_bkpw = None
if "(2) to show current active backup password" in story:
need_keypress("2")
time.sleep(.1)
title, story = cap_story()
assert 'Anyone with knowledge of the password will be able to decrypt your backups.' in story
press_select()
time.sleep(.1)
title, current_bkpw = cap_story()
current_bkpw = current_bkpw.strip()
press_select()
if old_password:
assert current_bkpw == old_password, "old_password mismatch"
if password is None:
# purge current bkpw
purge_current(exit=True)
return
# purge what was there from before
purge_current()
need_keypress("0")
enter_complex(password, apply=False, b39pass=False)
time.sleep(.1)
title, story = cap_story()
assert "(2) to show current active backup password" in story
need_keypress("2")
press_select() # are you sure?
time.sleep(.1)
title, story = cap_story()
new_current_bkpw = story.strip()
press_select()
time.sleep(.1)
title, story = cap_story()
if ((3*" ") in password) and not is_q1:
assert password.replace(" ", " ") == new_current_bkpw
else:
assert new_current_bkpw == password
assert "(1) to forget current password" in story
assert "(0) to change" in story
return doit
@pytest.fixture
def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
cap_story, need_keypress, cap_screen_qr, pass_word_quiz,
get_setting, seed_story_to_words, press_cancel, is_q1,
press_select, is_headless):
def doit(reuse_pw=False, save_pw=False, st=None, ct=False):
def doit(reuse_pw=None, save_pw=False, st=None, ct=False):
# st -> seed type
# ct -> cleartext backup
if reuse_pw:
settings_set('bkpw', ' '.join('zoo' for _ in range(12)))
if isinstance(reuse_pw, list):
assert len(reuse_pw) == 12
else:
assert reuse_pw is True # default
reuse_pw = ['zoo' for _ in range(12)]
settings_set('bkpw', ' '.join(reuse_pw))
else:
settings_remove('bkpw')
@ -55,13 +136,10 @@ def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
return # nothing more to be done
if reuse_pw:
assert ' 1: zoo' in body
assert '12: zoo' in body
assert (' 1: %s' % reuse_pw[0]) in body
assert ('12: %s' % reuse_pw[-1]) in body
press_select()
words = ['zoo'] * 12
time.sleep(0.1)
title, body = cap_story()
else:
assert title == 'NO-TITLE'
assert 'Record this' in body
@ -102,7 +180,7 @@ def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
@pytest.mark.qrcode
@pytest.mark.parametrize('multisig', [False, 'multisig'])
@pytest.mark.parametrize('st', ["b39pass", "eph", None])
@pytest.mark.parametrize('reuse_pw', [False, True])
@pytest.mark.parametrize('reuse_pw', [True, False])
@pytest.mark.parametrize('save_pw', [False, True])
@pytest.mark.parametrize('seedvault', [False, True])
@pytest.mark.parametrize('pass_way', ["qr", None])
@ -113,7 +191,7 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
generate_ephemeral_words, set_bip39_pw, verify_backup_file,
check_and_decrypt_backup, restore_backup_cs, clear_ms, seedvault,
restore_main_seed, import_ephemeral_xprv, backup_system,
press_cancel, sim_exec, pass_way):
press_cancel, sim_exec, pass_way, garbage_collector):
# Make an encrypted 7z backup, verify it, and even restore it!
clear_ms()
reset_seed_words()
@ -147,6 +225,10 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
time.sleep(.1)
assert len(get_setting('multisig')) == 1
if not reuse_pw:
# drop saved bkpw before we get to ephemeral settings
settings_remove("bkpw")
if st == "b39pass":
xfp_pass = set_bip39_pw("coinkite", reset=False, seed_vault=seedvault)
assert not get_setting('multisig', None)
@ -193,9 +275,12 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
print("filename %d: %s" % (copy, fn))
files.append(fn)
garbage_collector.append(microsd_path(fn))
# write extra copy.
need_keypress('2')
if not copy:
need_keypress('2')
time.sleep(.01)
bk_a = open_microsd(files[0]).read()
@ -438,7 +523,6 @@ def test_seed_vault_backup(settings_set, reset_seed_words, generate_ephemeral_wo
assert "Press (1) to save" in body
press_cancel()
time.sleep(.01)
assert get_setting('bkpw', 'xxx') == 'xxx'
title, story = cap_story()
assert "Backup file written:" in story
fn = story.split("\n\n")[1]
@ -513,4 +597,40 @@ def test_clone_start(reset_seed_words, pick_menu_item, cap_story, goto_home):
# TODO check file made is a good backup, with correct password
def test_bkpw_override(reset_seed_words, override_bkpw, goto_home, pick_menu_item,
cap_story, press_select, garbage_collector, microsd_path):
reset_seed_words() # clean slate
old_pw = None
test_cases = [
" ".join(12 * ["elevator"]),
" ".join(12 * ["fever"]),
32 * "a",
(16 * "0") + " " + (16 *"1"),
64 * "Q",
(26 * "?") + "!@#$%^&*()",
]
for pw in test_cases:
override_bkpw(pw, old_pw)
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Backup")
pick_menu_item("Backup System")
time.sleep(1)
title, story = cap_story()
split_pw = pw.split(" ")
if len(split_pw) == 12:
assert (' 1: %s' % split_pw[0]) in story
assert ('12: %s' % split_pw[-1]) in story
else:
# not words of len 12
assert ("%s...%s" % (pw[0], pw[-1])) in story
press_select()
time.sleep(1)
title, story = cap_story()
assert "Backup file written" in story
garbage_collector.append(microsd_path(story.split("\n\n")[1]))
press_select()
# EOF

View File

@ -373,4 +373,26 @@ def test_psbt_static(file, goto_home, cap_story, scan_a_qr, press_select,
assert res["complete"] is True
assert rb.hex() == res["hex"]
def test_verify_signed_msg(goto_home, need_keypress, scan_a_qr, cap_story):
goto_home()
need_keypress(KEY_QR)
data = """\n\n\n \t \n-----BEGIN BITCOIN SIGNED MESSAGE-----
5b9e372262952ed399dcdd4f5f08458a6d2811f120cddcb4267099f68f60207c addresses.csv
-----BEGIN BITCOIN SIGNATURE-----
tb1qupyd58ndsh7lut0et0vtrq432jvu9jtdyws9n9
KDOloGMDU3fv+Y3NRSe17SoO4uSKo9IUU2+baJ/pqaHZBuvmW6j5nnv/N4M5BCVawiUig/qzExZpFsA7ZKzlUmU=
-----END BITCOIN SIGNATURE-----\n\n\n\n"""
actual_vers, parts = split_qrs(data, 'U', max_version=20)
for p in parts:
scan_a_qr(p)
time.sleep(4.0 / len(parts)) # just so we can watch
title, story = cap_story()
assert "Good signature by address" in story
# EOF

View File

@ -194,4 +194,25 @@ def test_wif(data, try_decode):
assert compressed == tcompressed
assert testnet == ttestnet
@pytest.mark.parametrize('data', [
'{"msg": "coinkite"}',
'{"msg": "coink\n\n\tite", "subpath": "m/99h"}',
'{"msg": "coinkite", "subpath": "m/96420h", "addr_fmt": "p2wpkh"}',
])
def test_json_msg_sign(data, try_decode):
ft, vals = try_decode(data)
assert ft == "smsg"
assert vals[0] == data
@pytest.mark.parametrize('data', [
"-----BEGIN BITCOIN SIGNED MESSAGE-----\ncoinkite\n-----BEGIN BITCOIN SIGNATURE-----\nmtHSVByP9EYZmB26jASDdPVm19gvpecb5R\nH3c6imctVKRRYC1zOBAitdb/PuoQ9j0xaR6qKXH5dQECZH5OuvvE7aoL6j/WOaR/CFq/+SvIZPAzIhvQYBizBUc=\n-----END BITCOIN SIGNATURE-----",
"\n\n-----BEGIN BITCOIN SIGNED MESSAGE-----\ncoinkite\n-----BEGIN BITCOIN SIGNATURE-----\nmtHSVByP9EYZmB26jASDdPVm19gvpecb5R\nH3c6imctVKRRYC1zOBAitdb/PuoQ9j0xaR6qKXH5dQECZH5OuvvE7aoL6j/WOaR/CFq/+SvIZPAzIhvQYBizBUc=\n-----END BITCOIN SIGNATURE-----",
"\n\n\t-----BEGIN BITCOIN SIGNED MESSAGE-----\ncoinkite\n-----BEGIN BITCOIN SIGNATURE-----\nmtHSVByP9EYZmB26jASDdPVm19gvpecb5R\nH3c6imctVKRRYC1zOBAitdb/PuoQ9j0xaR6qKXH5dQECZH5OuvvE7aoL6j/WOaR/CFq/+SvIZPAzIhvQYBizBUc=\n-----END BITCOIN SIGNATURE-----",
])
def test_json_msg_verify(data, try_decode):
ft, vals = try_decode(data)
assert ft == "vmsg"
assert vals[0] == data
# EOF

View File

@ -1385,10 +1385,13 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story,
need_keypress, word_menu_entry, settings_set,
confirm_tmp_seed, cap_menu, microsd_path,
restore_main_seed, get_identity_story, press_select,
press_cancel):
press_cancel, settings_remove):
reset_seed_words()
# disable seed vault
settings_remove("seedvault")
settings_remove("seeds")
goto_eph_seed_menu()
ephemeral_seed_disabled()
@ -1405,6 +1408,7 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story,
title, story = cap_story()
assert "FAILED" == title
assert 'Cannot use master seed as temporary.' in story
assert 'tested recovery of your master seed' in story
press_cancel()
# go to ephemeral seed and then try to create new ephemeral seed from master
@ -1433,6 +1437,7 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story,
title, story = cap_story()
assert "FAILED" == title
assert 'Cannot use master seed as temporary.' in story
assert 'tested recovery of your master seed' in story
press_cancel()
# now import same seed but represented as master extended key
@ -1482,8 +1487,13 @@ def test_home_menu_xfp(goto_home, pick_menu_item, press_select, cap_story, cap_m
time.sleep(0.1)
need_keypress("6") # skip words
press_select()
press_select()
time.sleep(.3)
time.sleep(.1)
_, story = cap_story()
if "Press (1) to store temporary seed" in story:
# seed vault enabled
press_select() # do not save
press_select() # new tmp seed
time.sleep(.2)
m = cap_menu()
assert m[1] == "Ready To Sign"
assert m[0] == "[" + xfp2str(settings_get("xfp")) + "]"
@ -1509,9 +1519,11 @@ def test_home_menu_xfp(goto_home, pick_menu_item, press_select, cap_story, cap_m
def test_seed_vault_enable_on_tmp(generate_ephemeral_words, reset_seed_words,
goto_eph_seed_menu, ephemeral_seed_disabled,
verify_ephemeral_secret_ui, goto_home, cap_menu,
restore_main_seed, pick_menu_item, settings_set):
settings_set("seedvault", None) # disable seed vault
restore_main_seed, pick_menu_item, settings_remove):
reset_seed_words()
# disable seed vault
settings_remove("seedvault")
settings_remove("seeds")
goto_eph_seed_menu()
ephemeral_seed_disabled()
e_seed_words = generate_ephemeral_words(num_words=12, dice=False,

View File

@ -547,15 +547,15 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi
@pytest.mark.qrcode
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
@pytest.mark.parametrize('acct_num', [ None, 0, 99, 8989])
@pytest.mark.parametrize('use_nfc', [False, True])
def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home,
def test_export_xpub(chain, acct_num, dev, cap_menu, pick_menu_item, goto_home,
cap_story, need_keypress, enter_number, cap_screen_qr,
use_mainnet, nfc_read_text, is_q1, press_select, press_cancel,
settings_set, nfc_read_text, is_q1, press_select, press_cancel,
press_nfc, expect_acctnum_captured):
# XPUB's via QR
use_mainnet()
settings_set("chain", chain)
chain_num = 0 if chain == "BTC" else 1
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Export Wallet')
@ -565,13 +565,13 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
for m in top_items:
is_xfp = False
if '-84' in m:
expect = "m/84h/0h/{acct}h"
elif '86' in m and 'P2TR' in m:
expect = "m/86h/0h/{acct}h"
expect = f"m/84h/{chain_num}h/{{acct}}h"
elif '86' in m:
expect = f"m/86h/{chain_num}h/{{acct}}h"
elif '-44' in m:
expect = "m/44h/0h/{acct}h"
expect = f"m/44h/{chain_num}h/{{acct}}h"
elif '49' in m:
expect = "m/49h/0h/{acct}h"
expect = f"m/49h/{chain_num}h/{{acct}}h"
elif 'Master' in m:
expect = "m"
elif 'XFP' in m:
@ -581,17 +581,21 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
time.sleep(0.3)
if is_xfp:
got = cap_screen_qr().decode('ascii')
if use_nfc:
press_nfc()
assert got == xfp2str(simulator_fixed_xfp).upper()
press_cancel()
time.sleep(.1)
press_nfc()
time.sleep(.2)
nfc_got = nfc_read_text()
time.sleep(.2)
assert nfc_got == got == xfp2str(simulator_fixed_xfp).upper()
press_cancel() # cancel animation
press_cancel() # cancel QR
continue
time.sleep(0.3)
title, story = cap_story()
assert expect in story
assert expect.format(acct=0) in story
if 'acct' in expect:
if expect != "m":
assert "Press (1) to select account" in story
if acct_num is not None:
need_keypress('1')
@ -601,24 +605,52 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
expect = expect.format(acct=acct_num)
title, story = cap_story()
assert expect in story
assert "Press (1) to select account" not in story
assert "Press (1) to select account" in story
expect = expect.format(acct=0)
if not use_nfc:
press_select()
got_pub = cap_screen_qr().decode('ascii')
else:
if f'Press {KEY_NFC if is_q1 else "(3)"}' not in story:
raise pytest.skip("NFC disabled")
expect = expect.format(acct=0)
press_select()
got_pub = cap_screen_qr().decode('ascii')
if f'Press {KEY_NFC if is_q1 else "(3)"}' in story:
assert 'NFC' in story
press_nfc()
time.sleep(0.2)
got_pub = nfc_read_text()
got_nfc_pub = nfc_read_text()
time.sleep(0.1)
#press_select()
press_cancel() # cancel animation
press_cancel() # cancel QR
assert got_nfc_pub == got_pub
if got_pub[0] not in 'xt':
got_pub,*_ = slip132undo(got_pub)
time.sleep(.1)
_, story = cap_story()
assert got_pub[0] in 'xt'
if "Press (2)" in story:
if chain == "BTC":
assert f"{'z' if expect[:5] == 'm/84h' else 'y'}pub (SLIP-132)" in story
else:
assert f"{'v' if expect[:5] == 'm/84h' else 'u'}pub (SLIP-132)" in story
need_keypress("2")
time.sleep(.1)
_, story = cap_story()
assert ("%spub (BIP-32)" % ("x" if chain == "BTC" else "t")) in story
assert "Press (2)" in story
press_select()
got_slip_pub = cap_screen_qr().decode('ascii')
got_unslip, *_ = slip132undo(got_slip_pub)
assert got_unslip == got_pub
if f'Press {KEY_NFC if is_q1 else "(3)"}' in story:
assert 'NFC' in story
press_nfc()
time.sleep(0.2)
got_nfc_slip_pub = nfc_read_text()
time.sleep(0.1)
press_cancel() # cancel animation
assert got_slip_pub == got_nfc_slip_pub
press_cancel() # cancel QR
expect_acctnum_captured(acct_num)
@ -628,7 +660,6 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
if expect != 'm':
wallet = wallet.subkey_for_path(expect[2:].replace('h', "'"))
assert got.sec() == wallet.sec()
press_cancel()
@pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])

View File

@ -769,7 +769,7 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
use_regtest, get_cc_key, import_miniscript,
bitcoin_core_signer, import_duplicate, press_select,
virtdisk_path, garbage_collector):
def doit(M, N, script_type, internal_key=None, cc_account=0, funded=True, r=None,
def doit(M, N, script_type, internal_key=None, cc_account=0, funded=True,
tapscript_threshold=False, add_own_pk=False, same_account=False, way="sd"):
use_regtest()
@ -786,29 +786,14 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
wallet_name=f"watch_only_{script_type}_{M}of{N}", disable_private_keys=True,
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
)
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
pick_menu_item('Export XPUB')
time.sleep(0.5)
title, story = cap_story()
assert "extended public keys (XPUB) you would need to join a multisig wallet" in story
press_select()
need_keypress(str(cc_account)) # account
press_select()
xpub_obj = load_export(way, label="Multisig XPUB", is_json=True, sig_check=False)
template = xpub_obj[script_type +"_desc"]
acct_deriv = xpub_obj[script_type + '_deriv']
me_pth = f"m/48h/1h/{cc_account}h/3h"
me = get_cc_key(me_pth)
ik = internal_key or ranged_unspendable_internal_key()
if tapscript_threshold:
me = f"[{xpub_obj['xfp']}/{acct_deriv.replace('m/','')}]{xpub_obj[script_type]}/<0;1>/*"
signers_xp = [me] + bitcoind_signers_xpubs
assert len(signers_xp) == N
desc = f"tr({H},%s)"
if internal_key:
desc = desc.replace(H, internal_key)
elif r:
desc = desc.replace(H, f"r={r}")
desc = f"tr({ik},%s)"
scripts = []
for c in itertools.combinations(signers_xp, M):
@ -826,7 +811,7 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
if add_own_pk:
if len(scripts) < 8:
if same_account:
cc_key = get_cc_key("m/86h/1h/0h", subderiv="/<2;3>/*")
cc_key = get_cc_key(me_pth, subderiv="/<2;3>/*")
else:
cc_key = get_cc_key("m/86h/1h/1000h")
cc_pk_leaf = f"pk({cc_key})"
@ -842,22 +827,17 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
else:
if add_own_pk:
if same_account:
ss = [get_cc_key("m/86h/1h/0h", subderiv="/<4;5>/*")] + bitcoind_signers_xpubs
cc_key = get_cc_key("m/86h/1h/0h", subderiv="/<6;7>/*")
ss = [get_cc_key(me_pth, subderiv="/<4;5>/*")] + bitcoind_signers_xpubs
cc_key = get_cc_key(me_pth, subderiv="/<6;7>/*")
else:
ss = [get_cc_key("m/86h/1h/0h")] + bitcoind_signers_xpubs
cc_key = get_cc_key("m/86h/1h/1000h")
tmplt = f"sortedmulti_a({M},{','.join(ss)})"
cc_pk_leaf = f"pk({cc_key})"
desc = f"tr({H},{{{tmplt},{cc_pk_leaf}}})"
desc = f"tr({ik},{{{tmplt},{cc_pk_leaf}}})"
else:
desc = template.replace("M", str(M), 1).replace("...", ",".join(bitcoind_signers_xpubs))
if internal_key:
desc = desc.replace(H, internal_key)
elif r:
desc = desc.replace(H, f"r={r}")
desc = f"tr({ik},sortedmulti_a({M},{me},{','.join(bitcoind_signers_xpubs)}))"
name = "minisc"
fname = None
@ -889,13 +869,7 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
assert "P2SH-P2WSH" in story
# assert "Derivation:\n Varies (2)" in story
press_select() # approve multisig import
if r == "@":
# unspendable key is generated randomly
# descriptors will differ
with pytest.raises(AssertionError):
import_duplicate(fname, way=way, data=data)
else:
import_duplicate(fname, way=way, data=data)
import_duplicate(fname, way=way, data=data)
goto_home()
pick_menu_item('Settings')
pick_menu_item('Miniscript')
@ -915,20 +889,6 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
assert res[0]["success"]
assert res[1]["success"]
if r and r != "@":
from pysecp256k1.extrakeys import keypair_create, keypair_xonly_pub, xonly_pubkey_parse
from pysecp256k1.extrakeys import xonly_pubkey_tweak_add, xonly_pubkey_serialize, xonly_pubkey_from_pubkey
H_xo = xonly_pubkey_parse(bytes.fromhex(H))
r_bytes = bytes.fromhex(r)
kp = keypair_create(r_bytes)
kp_xo, kp_parity = keypair_xonly_pub(kp)
pk = xonly_pubkey_tweak_add(H_xo, xonly_pubkey_serialize(kp_xo))
xo, xo_parity = xonly_pubkey_from_pubkey(pk)
internal_key_bytes = xonly_pubkey_serialize(xo)
internal_key_hex = internal_key_bytes.hex()
assert internal_key_hex in core_desc_object[0]["desc"]
assert internal_key_hex in core_desc_object[1]["desc"]
if funded:
if script_type == "p2wsh":
addr_type = "bech32"
@ -961,7 +921,7 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export,
@pytest.mark.parametrize("add_pk", [True, False])
@pytest.mark.parametrize("same_acct", [None, True, False])
@pytest.mark.parametrize("way", ["qr", "sd"])
@pytest.mark.parametrize("M_N", [(3,4),(4,5),(5,6)])
@pytest.mark.parametrize("M_N", [(3,4),(5,6)])
def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, pick_menu_item,
cap_menu, cap_story, microsd_path, use_regtest, bitcoind, microsd_wipe,
load_export, bitcoind_miniscript, add_pk, same_acct, get_cc_key,
@ -1032,7 +992,7 @@ def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, pick_menu_item,
@pytest.mark.parametrize("add_pk", [True, False])
@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5)])
@pytest.mark.parametrize('way', ["qr", "sd", "vdisk", "nfc"])
@pytest.mark.parametrize('internal_type', ["unspend(", "xpub", "static"])
@pytest.mark.parametrize('internal_type', ["unspend(", "xpub"])
def test_bitcoind_tapscript_address(M_N, clear_miniscript, bitcoind_miniscript,
use_regtest, way, csa, address_explorer_check,
add_pk, internal_type, skip_if_useless_way):
@ -1054,13 +1014,11 @@ def test_bitcoind_tapscript_address(M_N, clear_miniscript, bitcoind_miniscript,
@pytest.mark.bitcoind
@pytest.mark.parametrize("cc_first", [True, False])
@pytest.mark.parametrize("m_n", [(2,2), (3, 5), (32, 32)])
@pytest.mark.parametrize("m_n", [(2,3), (32, 32)])
@pytest.mark.parametrize("way", ["qr", "sd"])
@pytest.mark.parametrize("internal_key_spendable", [
True,
False,
"77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76",
"@",
"tpubD6NzVbkrYhZ4WhUnV3cPSoRWGf9AUdG2dvNpsXPiYzuTnxzAxemnbajrATDBWhaAVreZSzoGSe3YbbkY2K267tK3TrRmNiLH2pRBpo8yaWm/<2;3>/*",
"unspend(c72231504cf8c1bbefa55974db4e0cdac781049a9a81a87e7ff5beeb45b34d3d)/<0;1>/*"
])
@ -1073,19 +1031,14 @@ def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest,
clear_miniscript()
microsd_wipe()
internal_key = None
r = None
if internal_key_spendable is True:
internal_key = get_cc_key("86h/0h/3h")
elif internal_key_spendable == "@":
r = "@"
elif isinstance(internal_key_spendable, str):
if len(internal_key_spendable) == 64:
r = internal_key_spendable
else:
internal_key = internal_key_spendable
internal_key = internal_key_spendable
tapscript_wo, bitcoind_signers = bitcoind_miniscript(
M, N, "p2tr", internal_key=internal_key, r=r,
M, N, "p2tr", internal_key=internal_key,
way=way
)
@ -1274,11 +1227,11 @@ def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bi
@pytest.mark.parametrize("desc", [
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})#tpm3afjn",
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)}})",
"tr(unspend(61350cde0f20e0268d0f33c22967863d9ebcbc3f448b78c9e83810d2152692e0)/<0;1>/*,{{sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})",
"tr(unspend(af042dea4fdb855b7b66732ce8512829d95bbf4963a7b28279d5a0b5b48e5bea)/<0;1>/*,{sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)}})",
"tr(tpubD6NzVbkrYhZ4XB7hZjurMYsPsgNY32QYGZ8YFVU7cy1VBRNoYpKAVuUfqfUFss6BooXRrCeYAdK9av2yFnqWXZaUMJuZdpE9Kuh6gubCVHu/<0;1>/*,{sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)}})",
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)})",
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},or_d(pk([0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*),and_v(v:pkh([30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),older(500)))})",
"tr(unspend(f19573a10866ee9881769e24464f9a0e989c2cb8e585db385934130462abed90)/<0;1>/*,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)})",
"tr(unspend(dfed64ff493dca2ab09eadefaa0c88be8404908fa6eff869ff71c0d359d086b9)/<2;3>/*,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},or_d(pk([0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*),and_v(v:pkh([30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),older(500)))})",
"tr(unspend(b320077905d0954b01a8a328ea08c0ac3b4b066d1240f47a1b2c58651dcda4eb)/<0;1>/*,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},or_d(pk([0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*),and_v(v:pkh([30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),older(500)))})",
])
def test_tapscript_import_export(clear_miniscript, pick_menu_item, cap_story,
@ -1543,7 +1496,7 @@ CHANGE_BASED_DESCS = [
")"
"))#a4nfkskx"
),
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{or_d(pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*),older(5))),or_i(and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2147483646;2147483647>/*),older(10)),or_d(multi_a(3,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<100;101>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<26;27>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<4;5>/*),and_v(v:thresh(2,pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<20;21>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<104;105>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<22;23>/*)),older(5))))})#z5x7409w",
"tr(tpubD6NzVbkrYhZ4WhUnV3cPSoRWGf9AUdG2dvNpsXPiYzuTnxzAxemnbajrATDBWhaAVreZSzoGSe3YbbkY2K267tK3TrRmNiLH2pRBpo8yaWm/<2;3>/*,{or_d(pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*),older(5))),or_i(and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2147483646;2147483647>/*),older(10)),or_d(multi_a(3,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<100;101>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<26;27>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<4;5>/*),and_v(v:thresh(2,pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<20;21>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<104;105>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<22;23>/*)),older(5))))})",
"tr([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<66;67>/*,{or_d(pk([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*),and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*),older(5))),or_i(and_v(v:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2147483646;2147483647>/*),older(10)),or_d(multi_a(3,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<100;101>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<26;27>/*,[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<4;5>/*),and_v(v:thresh(2,pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<20;21>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<104;105>/*),a:pkh([0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<22;23>/*)),older(5))))})#qqcy9jlr",
]
@ -1708,9 +1661,9 @@ def test_same_key_account_based_multisig(goto_home, pick_menu_item, cap_story,
@pytest.mark.parametrize("desc", [
"wsh(or_d(pk(@A),and_v(v:pkh(@A),older(5))))",
"tr(%s,multi_a(2,@A,@A))" % H,
"tr(%s,{sortedmulti_a(2,@A,@A),pk(@A)})" % H,
"tr(%s,or_d(pk(@A),and_v(v:pkh(@A),older(5))))" % H,
"tr(@ik,multi_a(2,@A,@A))",
"tr(@ik,{sortedmulti_a(2,@A,@A),pk(@A)})",
"tr(@ik,or_d(pk(@A),and_v(v:pkh(@A),older(5))))",
])
def test_insane_miniscript(get_cc_key, pick_menu_item, cap_story,
microsd_path, desc, import_miniscript,
@ -1718,6 +1671,7 @@ def test_insane_miniscript(get_cc_key, pick_menu_item, cap_story,
cc_key = get_cc_key("84h/0h/0h")
desc = desc.replace("@A", cc_key)
desc = desc.replace("@ik", ranged_unspendable_internal_key())
fname = "insane.txt"
fpath = microsd_path(fname)
with open(fpath, "w") as f:
@ -1737,7 +1691,7 @@ def test_tapscript_depth(get_cc_key, pick_menu_item, cap_story,
scripts.append(f"pk({k})")
tree = TREE[leaf_num] % tuple(scripts)
desc = f"tr({H},{tree})"
desc = f"tr({ranged_unspendable_internal_key()},{tree})"
fname = "9leafs.txt"
fpath = microsd_path(fname)
with open(fpath, "w") as f:
@ -1752,7 +1706,7 @@ def test_tapscript_depth(get_cc_key, pick_menu_item, cap_story,
@pytest.mark.parametrize("same_acct", [True, False])
@pytest.mark.parametrize("recovery", [True, False])
@pytest.mark.parametrize("leaf2_mine", [True, False])
@pytest.mark.parametrize("internal_type", ["unspend(", "xpub", "static"])
@pytest.mark.parametrize("internal_type", ["unspend(", "xpub"])
@pytest.mark.parametrize("minisc", [
"or_d(pk(@A),and_v(v:pkh(@B),locktime(N)))",
@ -1816,10 +1770,10 @@ def test_minitapscript(leaf2_mine, recovery, minisc, clear_miniscript, goto_home
if "@C" in minisc:
minisc = minisc.replace("@C", core_keys[1])
ik = H
if internal_type == "unspend(":
ik = f"unspend({os.urandom(32).hex()})/<2;3>/*"
elif internal_type == "xpub":
else:
assert internal_type == "xpub"
ik = ranged_unspendable_internal_key(os.urandom(32))
if leaf2_mine:
@ -1932,7 +1886,7 @@ def test_minitapscript(leaf2_mine, recovery, minisc, clear_miniscript, goto_home
address_explorer_check("sd", "bech32m", wo, "minitapscript")
@pytest.mark.parametrize("desc", [
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})",
"tr(tpubD6NzVbkrYhZ4WhUnV3cPSoRWGf9AUdG2dvNpsXPiYzuTnxzAxemnbajrATDBWhaAVreZSzoGSe3YbbkY2K267tK3TrRmNiLH2pRBpo8yaWm/<2;3>/*,{{sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})",
"wsh(sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*))",
"sh(wsh(or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:multi_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),older(500)))))",
])
@ -1982,7 +1936,7 @@ def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, ca
if addr_fmt == "bech32":
desc = f"wsh({minsc})"
else:
desc = f"tr({H},{minsc})"
desc = f"tr({ranged_unspendable_internal_key()},{minsc})"
name = "d_wrapper"
fname = f"{name}.txt"
@ -2109,7 +2063,7 @@ def test_chain_switching(use_mainnet, use_regtest, settings_get, settings_set,
x = "wsh(or_d(pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),and_v(v:pkh([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),older(100))))"
z = "wsh(or_d(pk([0f056943/48'/0'/0'/3']xpub6FQgdFZAHcAeDMVe9KxWoLMxziCjscCExzuKJhRSjM71CA9dUDZEGNgPe4S2SsRumCBXeaTBZ5nKz2cMDiK4UEbGkFXNipHLkm46inpjE9D/0/*),and_v(v:pkh([0f056943/48'/0'/0'/2']xpub6FQgdFZAHcAeAhQX2VvQ42CW2fDdKDhgwzhzXuUhWb4yfArmaZXkLbGS9W1UcgHwNxVESCS1b8BK8tgNYEF8cgmc9zkmsE45QSEvbwdp6Kr/0/*),older(100))))"
y = f"tr({H},or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),after(800000))))"
y = f"tr({ranged_unspendable_internal_key()},or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),after(800000))))"
fname_btc = "BTC.txt"
fname_xtn = "XTN.txt"
@ -2201,7 +2155,7 @@ def test_import_same_policy_same_keys_diff_order(taproot_ikspendable, minisc,
ik = get_cc_key("84h/1h/100h", subderiv="/0/*")
desc = f"tr({ik},{minisc})"
else:
desc = f"tr({H},{minisc})"
desc = f"tr({ranged_unspendable_internal_key()},{minisc})"
else:
desc = f"wsh({minisc})"
@ -2251,7 +2205,7 @@ def test_import_miniscript_usb_json(use_regtest, cs, way, cap_menu,
virtdisk_path, import_miniscript, goto_home,
press_select):
name = "my_minisc"
minsc = f"tr({H},or_d(multi_a(2,@A,@C),and_v(v:pkh(@B),after(100))))"
minsc = f"tr({ranged_unspendable_internal_key()},or_d(multi_a(2,@A,@C),and_v(v:pkh(@B),after(100))))"
use_regtest()
clear_miniscript()
@ -2332,7 +2286,7 @@ def test_unique_name(clear_miniscript, use_regtest, offer_minsc_import,
name = "my_name"
x = "wsh(or_d(pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),and_v(v:pkh([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),older(100))))"
y = f"tr({H},or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),after(800000))))"
y = f"tr({ranged_unspendable_internal_key()},or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:pk([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),after(800000))))"
xd = json.dumps({"name": name, "desc": x})
title, story = offer_minsc_import(xd)
@ -2769,11 +2723,13 @@ def test_expanding_multisig(tmplt, clear_miniscript, goto_home, pick_menu_item,
address_explorer_check("sd", af, wo, wname)
@pytest.mark.parametrize("blinded", [True, False])
def test_big_boy(use_regtest, clear_miniscript, bitcoin_core_signer, get_cc_key, microsd_path,
garbage_collector, pick_menu_item, bitcoind, import_miniscript, press_select,
cap_story, cap_menu, load_export, start_sign, end_sign):
cap_story, cap_menu, load_export, start_sign, end_sign, blinded):
# keys (@0,@4,@5) are more important (primary) than keys (@1,@2,@3) (secondary)
# currently requires to tweak MAX_TR_SIGNERS = 33
# with blinded=True, all co-signer keys are blinded (have no key origin info)
tmplt = (
"tr("
"tpubD6NzVbkrYhZ4XgXS51CV3bhoP5dJeQqPhEyhKPDXBgEs64VdSyAfku99gtDXQzY6HEXY5Dqdw8Qud1fYiyewDmYjKe9gGJeDx7x936ur4Ju/<0;1>/*," # unspendable
@ -2797,6 +2753,10 @@ def test_big_boy(use_regtest, clear_miniscript, bitcoin_core_signer, get_cc_key,
for i in range(1, 6):
csigner, ckey = bitcoin_core_signer(f"co-signer-{i}")
ckey = ckey.replace("/0/*", "")
if blinded:
ckey = ckey.split("]")[-1]
csigner.keypoolrefill(20)
cosigners.append(csigner)
desc = desc.replace(f"@{i}", ckey)
@ -3009,3 +2969,138 @@ def test_single_key_miniscript(af, settings_set, clear_miniscript, goto_home, ge
unspent = wo.listunspent()
assert len(unspent) == 1
@pytest.mark.parametrize("tmplt", [
"wsh(or_d(pk(@0),and_v(v:pkh(@1),older(100))))",
f"tr({ranged_unspendable_internal_key()},or_d(pk(@0),and_v(v:pk(@1),older(100))))"
])
@pytest.mark.parametrize("cc_sign", [False, True])
@pytest.mark.parametrize("has_orig", [False, True])
def test_originless_keys(tmplt, offer_minsc_import, get_cc_key, bitcoin_core_signer, bitcoind,
pick_menu_item, load_export, goto_home, cap_menu, clear_miniscript,
use_regtest, press_select, start_sign, end_sign, cap_story, cc_sign,
has_orig, address_explorer_check):
# can be both:
# a.) just ranged xpub without origin info -> xpub1/<0;1>/*
# b.) ranged xpub with its fp -> [xpub1_fp]xpub1/<0;1>/*
sequence = 100
use_regtest()
clear_miniscript()
af = "bech32m" if "tr(" in tmplt else "bech32"
name = "originless"
cc_key = get_cc_key("m/84h/1h/0h")
cs, ck = bitcoin_core_signer(name+"_signer")
originless_ck = ck.split("]")[-1]
n = BIP32Node.from_hwif(originless_ck.split("/")[0]) # just extended key
fp_str = "[" + n.fingerprint().hex() + "]"
if has_orig:
originless_ck = fp_str + originless_ck
desc = tmplt.replace("@0", cc_key)
desc = desc.replace("@1", originless_ck)
to_import = {"desc": desc, "name": name}
offer_minsc_import(json.dumps(to_import))
press_select()
wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True,
passphrase=None, avoid_reuse=False, descriptors=True)
goto_home()
pick_menu_item("Settings")
pick_menu_item("Miniscript")
menu = cap_menu()
assert menu[0] == name
pick_menu_item(menu[0]) # pick imported descriptor miniscript wallet
pick_menu_item("Descriptors")
pick_menu_item("Bitcoin Core")
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
text = text.replace("importdescriptors ", "").strip()
# remove junk
r1 = text.find("[")
r2 = text.find("]", -1, 0)
text = text[r1: r2]
core_desc_object = json.loads(text)
res = wo.importdescriptors(core_desc_object)
for obj in res:
assert obj["success"]
# fund wallet
addr = wo.getnewaddress("", af)
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
unspent = wo.listunspent()
assert len(unspent) == 1
if cc_sign:
inputs = []
else:
inputs = [{"txid": unspent[0]["txid"], "vout": unspent[0]["vout"], "sequence": sequence}]
# split to 10 utxos
dest_addrs = [wo.getnewaddress(f"a{i}", af) for i in range(10)]
psbt_resp = wo.walletcreatefundedpsbt(
inputs,
[{a: 4} for a in dest_addrs] + [{bitcoind.supply_wallet.getnewaddress(): 5}],
0,
{"fee_rate": 3, "change_type": af, "subtractFeeFromOutputs": [0]},
)
psbt = psbt_resp.get("psbt")
if cc_sign:
start_sign(base64.b64decode(psbt))
time.sleep(.1)
title, story = cap_story()
assert title == "OK TO SEND?"
assert "Consolidating" not in story
final_psbt = end_sign(True)
final_psbt = base64.b64encode(final_psbt).decode()
else:
final_psbt_o = cs.walletprocesspsbt(psbt, True, "DEFAULT" if af == "bech32m" else "ALL")
final_psbt = final_psbt_o["psbt"]
assert psbt != final_psbt
res = wo.finalizepsbt(final_psbt)
assert res["complete"]
tx_hex = res["hex"]
res = wo.testmempoolaccept([tx_hex])
if not cc_sign:
# timelocked
assert not res[0]["allowed"]
assert res[0]["reject-reason"] == 'non-BIP68-final'
# mines some blocks to release the lock
bitcoind.supply_wallet.generatetoaddress(sequence, bitcoind.supply_wallet.getnewaddress())
res = wo.testmempoolaccept([tx_hex])
assert res[0]["allowed"]
res = wo.sendrawtransaction(tx_hex)
assert len(res) == 64 # tx id
# check addresses
address_explorer_check("sd", af, wo, name)
@pytest.mark.parametrize("internal_key", [
H,
"r=@",
"r=dfed64ff493dca2ab09eadefaa0c88be8404908fa6eff869ff71c0d359d086b9",
"f19573a10866ee9881769e24464f9a0e989c2cb8e585db385934130462abed90"
])
def test_static_internal_key(internal_key, clear_miniscript, microsd_path, pick_menu_item,
cap_story, import_miniscript, garbage_collector):
clear_miniscript()
desc = "tr(@ik,{{sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})"
desc = desc.replace("@ik", internal_key)
fname = "imdesc.txt"
fpath = microsd_path(fname)
with open(microsd_path(fname), "w") as f:
f.write(desc)
garbage_collector.append(fpath)
title, story = import_miniscript(fname)
assert "Failed to import" in story
assert "only extended keys allowed" in story

View File

@ -2,13 +2,40 @@
#
# Message signing.
#
import pytest, time, os, itertools, hashlib
import pytest, time, os, itertools, hashlib, json
from bip32 import BIP32Node
from msg import verify_message, RFC_SIGNATURE_TEMPLATE, sign_message, parse_signed_message
from base64 import b64encode, b64decode
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused
from ckcc_protocol.constants import *
from constants import addr_fmt_names, msg_sign_unmap_addr_fmt
from charcodes import KEY_QR, KEY_NFC
from helpers import addr_from_display_format
def addr_fmt_from_subpath(subpath):
if not subpath:
af = AF_CLASSIC
elif subpath[:4] == "m/84":
af = AF_P2WPKH
elif subpath[:4] == "m/49":
af = AF_P2WPKH_P2SH
else:
af = AF_CLASSIC
return af
def default_derivation_by_af(addr_fmt, testnet=True):
b44ct = "1" if testnet else "0"
if addr_fmt == AF_CLASSIC:
path = "m/44h/{chain}h/0h/0/0"
elif addr_fmt == AF_P2WPKH_P2SH:
path = "m/49h/{chain}h/0h/0/0"
elif addr_fmt == AF_P2WPKH:
path = "m/84h/{chain}h/0h/0/0"
else:
assert False, "unsupported address format"
return path.format(chain=b44ct)
@pytest.mark.parametrize('msg', [ 'aZ', 'hello', 'abc def eght', "x"*140, 'a'*240])
@ -54,6 +81,200 @@ def test_sign_msg_refused(dev, press_cancel):
done = dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
@pytest.fixture
def verify_msg_sign_story():
def doit(story, msg, subpath=None, addr_fmt=None, testnet=True, addr=None):
assert story.startswith('Ok to sign this?')
assert msg in story
assert 'Using the key associated' in story
if addr:
assert addr == addr_from_display_format(story.split("\n\n")[2].split("\n")[-1])
if not subpath:
assert 'm =>' not in story
subpath = default_derivation_by_af(addr_fmt or AF_CLASSIC, testnet)
else:
subpath = subpath.lower().replace("'", "h")
assert ('%s =>' % subpath) in story
return subpath
return doit
@pytest.fixture
def msg_sign_export(cap_story, press_nfc, nfc_read_text, press_select, press_cancel,
readback_bbqr, cap_screen_qr, need_keypress, microsd_path,
virtdisk_path, is_q1, OK):
def doit(way, qr_only=False):
time.sleep(.1)
title, story = cap_story()
if way == "sd":
if "Press (1) to save Signed Msg" in story:
need_keypress("1")
elif way == "nfc":
if f"press {KEY_NFC if is_q1 else '(3)'} to share via NFC" not in story:
pytest.xfail("NFC disabled")
else:
press_nfc()
time.sleep(0.2)
signed_msg = nfc_read_text()
time.sleep(0.3)
press_cancel()
time.sleep(.1)
title, story = cap_story()
assert f"Press {OK} to share again" in story
press_cancel()
elif way == "qr":
if not is_q1:
pytest.xfail("QR disabled")
if not qr_only:
need_keypress(KEY_QR)
time.sleep(.1)
title, story = cap_story()
assert "Press ENTER to export signature QR only" in story
assert "(0) to export full RFC template" in story
press_select()
time.sleep(.1)
sig_only = cap_screen_qr().decode('ascii')
press_select()
time.sleep(.1)
need_keypress("0")
time.sleep(.1)
file_type, signed_msg = readback_bbqr()
signed_msg = signed_msg.decode()
assert file_type == "U"
assert sig_only in signed_msg
press_select()
press_cancel()
else:
# virtual disk
if "press (2) to save to Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way in ("sd", "vdisk"):
path_f = microsd_path if way == "sd" else virtdisk_path
time.sleep(.1)
title, story = cap_story()
fname = story.split("\n\n")[-1]
with open(path_f(fname), "r") as f:
signed_msg = f.read()
return signed_msg
return doit
@pytest.fixture
def sign_msg_from_text(pick_menu_item, enter_number, press_select,
cap_story, need_keypress, settings_set, is_q1,
addr_vs_path, bitcoind, msg_sign_export,
verify_msg_sign_story, OK):
# used when signing note/passwords misc content
# used after simple text QR scan
# expects to start at menu which offers different single sig address formats
def doit(msg, addr_fmt, acct, change, idx, way, chain="XTN", qr_only=False):
settings_set("chain", chain)
path = "m"
# pick address format from menu
if addr_fmt == AF_CLASSIC:
path += "/44h"
af_label = "Classic P2PKH"
elif addr_fmt == AF_P2WPKH:
path += "/84h"
af_label = "Segwit P2WPKH"
else:
path += "/49h"
af_label = "P2SH-Segwit"
pick_menu_item(af_label)
# chain - no user input - depends on current active settings
if chain == "BTC":
path += "/0h"
else:
path += "/1h"
# pick account
if acct is None:
path += "/0h"
press_select()
else:
path += ("/%dh" % acct)
enter_number(acct)
time.sleep(.1)
title, story = cap_story()
assert title == "Change?"
assert "Press (0) to use internal/change address" in story
assert f"{OK} to use external/receive address" in story
if change:
path += "/1"
need_keypress("0")
else:
path += "/0"
press_select()
# index num
if idx is None:
path += "/0"
press_select()
else:
path += ("/%d" % idx)
enter_number(idx)
time.sleep(.1)
title, story = cap_story()
path = verify_msg_sign_story(story, msg, path, addr_fmt, testnet=True if chain == "XTN" else False)
press_select()
signed_msg = msg_sign_export(way, qr_only)
ret_msg, addr, sig = parse_signed_message(signed_msg)
addr_vs_path(addr, path, addr_fmt, chain=chain)
assert verify_message(addr, sig, ret_msg) is True
if addr_fmt == AF_CLASSIC and chain == "XTN":
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
assert res is True
return doit
@pytest.fixture
def sign_msg_from_address(need_keypress, scan_a_qr, press_select, enter_complex, cap_story,
addr_vs_path, verify_msg_sign_story, msg_sign_export):
def doit(msg, addr, subpath, addr_fmt, way=None, testnet=True):
if way == 'qr':
# scan text via QR
need_keypress(KEY_QR)
scan_a_qr(msg)
time.sleep(1)
press_select()
else:
enter_complex(msg, b39pass=False)
time.sleep(.1)
title, story = cap_story()
verify_msg_sign_story(story, msg, subpath, addr_fmt, testnet, addr)
press_select()
time.sleep(.1)
signed_msg = msg_sign_export(way)
ret_msg, addr, sig = parse_signed_message(signed_msg)
addr_vs_path(addr, subpath, addr_fmt, chain="XTN" if testnet else "BTC")
return doit
@pytest.mark.parametrize('path,expect', [
('1/1hard/2', 'invalid characters'),
('m/m/m/1/1hard/2', 'invalid characters'),
@ -78,24 +299,36 @@ def test_bad_paths(dev, path, expect):
@pytest.fixture
def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
press_select, microsd_path):
press_select, microsd_path, verify_msg_sign_story):
# sign a file on the microSD card
def doit(msg, subpath=None, addr_fmt=None, expect_fail=False):
fname = 't-msgsign.txt'
def doit(msg, subpath="", addr_fmt=None, expect_fail=False, testnet=True,
use_json=False):
suffix = "json" if use_json else "txt"
fname = f't-msgsign.{suffix}'
result_fname = 't-msgsign-signed.txt'
# cleanup
try: os.unlink(microsd_path(result_fname))
except OSError: pass
with open_microsd(fname, 'wt') as sd:
sd.write(msg + '\n')
if subpath is not None:
sd.write(subpath + '\n')
if addr_fmt is not None:
sd.write(addr_fmt_names[addr_fmt] + '\n')
if use_json:
res = {"msg": msg}
if subpath:
res["subpath"] = subpath
if addr_fmt is not None:
res["addr_fmt"] = addr_fmt_names[addr_fmt]
sd.write(json.dumps(res))
else:
sd.write(msg + '\n')
if subpath or addr_fmt:
sd.write((subpath or "") + '\n')
if addr_fmt is not None:
sd.write(addr_fmt_names[addr_fmt])
goto_home()
pick_menu_item('Advanced/Tools')
@ -115,17 +348,8 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
assert not story.startswith('Ok to sign this?')
return story
assert story.startswith('Ok to sign this?')
assert msg in story
assert 'Using the key associated' in story
if not subpath:
assert 'm =>' in story
else:
x_subpath = subpath.lower().replace("'", "h")
assert ('%s =>' % x_subpath) in story
press_select()
verify_msg_sign_story(story, msg, subpath, addr_fmt, testnet)
press_select() # confirm msg sign
# wait for it to finish
for r in range(10):
@ -135,26 +359,23 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
else:
assert False, 'timed out'
lines = [i.strip() for i in open_microsd(result_fname, 'rt').readlines()]
with open_microsd(result_fname, 'rt') as f:
res = f.read()
assert lines[0] == '-----BEGIN BITCOIN SIGNED MESSAGE-----'
assert lines[1:-4] == [msg]
assert lines[-4] == '-----BEGIN BITCOIN SIGNATURE-----'
addr = lines[-3]
sig = lines[-2]
assert lines[-1] == '-----END BITCOIN SIGNATURE-----'
return sig, addr
ret_msg, addr, sig = parse_signed_message(res)
assert ret_msg == msg
return sig, addr, msg
return doit
@pytest.mark.parametrize('msg', [ 'ab', 'hello', 'abc def eght', "x"*140, 'a'*240])
@pytest.mark.bitcoind # only for testnet and p2pkh
@pytest.mark.parametrize("use_json", [True, False])
@pytest.mark.parametrize('msg', [ 'ab', 'abc def eght', 'a'*240])
@pytest.mark.parametrize('path', [
"m/84'/0'/22'",
None,
'm',
"m/1/2",
"m/1'/100'",
'm/23h/22h',
])
@pytest.mark.parametrize('addr_fmt', [
@ -163,29 +384,53 @@ def sign_on_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
AF_CLASSIC,
AF_P2WPKH_P2SH,
])
def test_sign_msg_microsd_good(sign_on_microsd, msg, path, addr_vs_path, addr_fmt):
if (path is None) and (addr_fmt is not None):
# must give path if addr fmt is to be specified
return
@pytest.mark.parametrize("testnet", [True, False])
def test_sign_msg_microsd_good(sign_on_microsd, msg, path, addr_vs_path,
addr_fmt, testnet, settings_set, bitcoind,
use_json):
settings_set("chain", "XTN" if testnet else "BTC")
# cases we expect to work
sig, addr = sign_on_microsd(msg, path, addr_fmt)
sig, addr, ret_msg = sign_on_microsd(msg, path, addr_fmt, testnet=testnet,
use_json=use_json)
assert msg == ret_msg
raw = b64decode(sig)
assert 40 <= len(raw) <= 65
if path is None:
path = 'm'
if addr_fmt is None:
addr_fmt = addr_fmt_from_subpath(path)
if not path:
path = default_derivation_by_af(addr_fmt, testnet=testnet)
# check expected addr was used
addr_vs_path(addr, path, addr_fmt)
addr_vs_path(addr, path, addr_fmt, chain="XTN" if testnet else "BTC")
assert verify_message(addr, sig, msg) is True
if addr_fmt == AF_CLASSIC and testnet:
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
assert res is True
@pytest.fixture
def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story):
def doit(body, expect_fail=True):
def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story, press_select,
nfc_read_text, addr_vs_path, press_cancel, OK, verify_msg_sign_story):
def doit(msg, subpath=None, addr_fmt=None, expect_fail=False, use_json=False,
testnet=True):
if use_json:
res = {"msg": msg}
if subpath:
res["subpath"] = subpath
if addr_fmt is not None:
res["addr_fmt"] = addr_fmt_names[addr_fmt]
body = json.dumps(res)
else:
body = msg + "\n"
if subpath or addr_fmt:
body += ((subpath or "") + '\n')
if addr_fmt is not None:
body += addr_fmt_names[addr_fmt]
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('NFC Tools')
@ -194,48 +439,115 @@ def sign_using_nfc(goto_home, pick_menu_item, nfc_write_text, cap_story):
time.sleep(0.5)
if expect_fail:
return cap_story()
raise NotImplementedError
if not addr_fmt:
addr_fmt = addr_fmt_from_subpath(subpath)
if not subpath:
subpath = default_derivation_by_af(addr_fmt, testnet=testnet)
_, story = cap_story()
subpath = verify_msg_sign_story(story, msg, subpath, addr_fmt, testnet)
press_select()
signed_msg = nfc_read_text()
if "BITCOIN SIGNED MESSAGE" not in signed_msg:
# missed it? again
signed_msg = nfc_read_text()
press_select() # exit NFC animation
pmsg, addr, sig = parse_signed_message(signed_msg)
assert pmsg == msg
addr_vs_path(addr, subpath, addr_fmt, chain="XTN" if testnet else "BTC")
assert verify_message(addr, sig, msg) is True
time.sleep(0.5)
_, story = cap_story()
assert f"Press {OK} to share again" in story
press_select()
signed_msg_again = nfc_read_text()
assert signed_msg == signed_msg_again
press_cancel() # exit NFC animation
press_cancel() # do not want to share again
return sig, addr, msg
return doit
@pytest.mark.parametrize('msg,concern,no_file', [
('', 'too short', 0), # zero length not supported
('a'*1000, 'too long', 1), # too big, won't even be offered as a file
('a'*300, 'too long', 0), # too big
('a'*241, 'too long', 0), # too big
('hello%20sworld'%'', 'many spaces', 0), # spaces
('hello%10sworld'%'', 'many spaces', 0), # spaces
('hello%5sworld'%'', 'many spaces', 0), # spaces
('test\ttest', "must be ascii printable", 0),
('testêtest', "must be ascii printable", 0),
])
@pytest.mark.parametrize('transport', ['sd', 'usb', 'nfc'])
def test_sign_msg_fails(dev, sign_on_microsd, msg, concern, no_file, transport, sign_using_nfc, path='m/12/34'):
@pytest.mark.bitcoind
@pytest.mark.parametrize("way", ["nfc", "sd"])
@pytest.mark.parametrize("msg", ['test\ttest', "\n\n\tmsg\n\n\tsigning"])
def test_sign_msg_with_ascii_non_printable_chars(msg, way, sign_on_microsd, addr_vs_path,
settings_set, bitcoind, sign_using_nfc):
# only works with the JSON format
settings_set("chain", "XTN")
if way == "sd":
sig, addr, ret_msg = sign_on_microsd(msg, "", None, use_json=True)
else:
sig, addr, ret_msg = sign_using_nfc(msg, "", None, use_json=True)
assert ret_msg == msg
raw = b64decode(sig)
assert 40 <= len(raw) <= 65
addr_fmt = AF_CLASSIC
path = default_derivation_by_af(addr_fmt, testnet=True)
# check expected addr was used
addr_vs_path(addr, path, addr_fmt)
assert verify_message(addr, sig, msg) is True
res = bitcoind.rpc.verifymessage(addr, sig, msg)
assert res is True
@pytest.mark.parametrize('msg,subpath,addr_fmt,concern,no_file,no_json', [
('', "m", AF_CLASSIC, 'too short', 0, 0), # zero length not supported
('a'*1000, "m/1", AF_P2WPKH,'too long', 1, 0), # too big, won't even be offered as a file
('a'*241, "m/400", AF_P2WPKH_P2SH, 'too long', 0, 0), # too big
('hello%20sworld'%'', "m", AF_CLASSIC, 'many spaces', 0, 0), # spaces
('hello%10sworld'%'', "m/1h/3h", AF_P2WPKH_P2SH, 'many spaces', 0, 0), # spaces
('hello%5sworld'%'', "m", AF_CLASSIC, 'many spaces', 0, 0), # spaces
("coinkite", "m", AF_P2WSH, "Unsupported address format", 0, 0), # invalid address format
("coinkite", "m", AF_P2WSH_P2SH, "Unsupported address format", 0, 0), # invalid address format
("coinkite", " m", AF_P2TR, "Unsupported address format", 0, 0), # invalid address format
("coinkite", "m/0/0/0/0/0/0/0/0/0/0/0/0/0", AF_CLASSIC, "too deep", 0, 0), # invalid path
("coinkite", "m/0/0/0/0/0/q/0/0/0", AF_P2WPKH, "invalid characters in path", 0, 0), # invalid path
("coinkite ", "m", AF_CLASSIC, "trailing space(s)", 0, 0), # invalid msg - trailing space
(" coinkite", "m", AF_P2WPKH_P2SH, "leading space(s)", 0, 0), # invalid msg - leading space
('testêtest', "m", AF_P2WPKH, "must be ascii", 0, 0),
# below works only with the JSON format
('test\ttest', "m", AF_CLASSIC, "must be ascii printable", 0, 1),
])
@pytest.mark.parametrize("use_json", [True, False])
@pytest.mark.parametrize('transport', ['sd', 'usb', 'nfc'])
def test_sign_msg_fails(dev, sign_on_microsd, msg, subpath, addr_fmt, concern,
no_file, no_json, transport, sign_using_nfc, use_json):
if use_json and no_json:
# special cases with ascii non printable characters - can be present in json
raise pytest.skip("json can contain ASCII non-printable in msg")
if transport == 'usb':
with pytest.raises(CCProtoError) as ee:
try:
encoded_msg = msg.encode('ascii')
except UnicodeEncodeError:
encoded_msg = msg.encode()
dev.send_recv(CCProtocolPacker.sign_message(encoded_msg, path), timeout=None)
dev.send_recv(CCProtocolPacker.sign_message(encoded_msg, subpath, addr_fmt), timeout=None)
story = ee.value.args[0]
elif transport == 'sd':
try:
story = sign_on_microsd(msg, path, expect_fail=True)
story = sign_on_microsd(msg, subpath, addr_fmt, expect_fail=True, use_json=use_json)
assert story.startswith('Problem: ')
except AssertionError as e:
if no_file:
assert ("No suitable files found" in str(e)) or story == 'NO-FILE'
return
elif transport == 'nfc':
title, story = sign_using_nfc(msg, expect_fail=True)
title, story = sign_using_nfc(msg, subpath, addr_fmt, expect_fail=True, use_json=use_json)
assert title == 'ERROR' or "Problem" in story
else:
raise ValueError(transport)
assert concern in story
@pytest.mark.parametrize('msg,num_iter,expect', [
('Test2', 1, 'IHra0jSywF1TjIJ5uf7IDECae438cr4o3VmG6Ri7hYlDL+pUEXyUfwLwpiAfUQVqQFLgs6OaX0KsoydpuwRI71o='),
('Test', 2, 'IDgMx1ljPhLHlKUOwnO/jBIgK+K8n8mvDUDROzTgU8gOaPDMs+eYXJpNXXINUx5WpeV605p5uO6B3TzBVcvs478='),
@ -280,32 +592,16 @@ def test_low_R_cases(msg, num_iter, expect, dev, set_seed_words, use_mainnet,
assert sig == expect
@pytest.mark.parametrize("body", [
"coinkite\nm\np2wsh", # invalid address format
"coinkite\nm\np2sh-p2wsh", # invalid address format
"coinkite\nm\np2tr", # invalid address format
"coinkite\nm/0/0/0/0/0/0/0/0/0/0/0/0/0\np2pkh", # invalid path
"coinkite\nm/0/0/0/0/0/q/0/0/0\np2pkh", # invalid path
"coinkite yes!\nm\np2pkh", # invalid msg - too many spaces
"c\nm\np2pkh", # invalid msg - too short
"coinkite \nm\np2pkh", # invalid msg - trailing space
" coinkite\nm\np2pkh", # invalid msg - leading space
])
def test_nfc_msg_signing_invalid(body, goto_home, pick_menu_item, nfc_write_text, cap_story):
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('NFC Tools')
pick_menu_item('Sign Message')
nfc_write_text(body)
time.sleep(0.5)
title, story = cap_story()
assert title == 'ERROR' or "Problem" in story
@pytest.mark.parametrize("msg", ["coinkite", "Coldcard Signing Device!", 200 * "a"])
@pytest.mark.parametrize("path", ["", "m/84'/0'/0'/300/0", "m/800h/0h", "m/0/0/0/0/1/1/1"])
@pytest.mark.parametrize("str_addr_fmt", ["p2pkh", "", "p2wpkh", "p2wpkh-p2sh", "p2sh-p2wpkh"])
def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_menu_item,
goto_home, cap_story, press_select, press_cancel, addr_vs_path, OK):
@pytest.mark.bitcoind # only for testnet and p2pkh
@pytest.mark.parametrize("testnet", [True, False])
@pytest.mark.parametrize("use_json", [True, False])
@pytest.mark.parametrize("msg", ["Coldcard Signing Device!", 200 * "a"])
@pytest.mark.parametrize("path", ["", "m/84h/0h/0h/300/0", "m/0/0/0/0/1/1/1"])
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, None, AF_P2WPKH, AF_P2WPKH_P2SH])
def test_nfc_msg_signing(msg, path, addr_fmt, testnet, settings_set, bitcoind, use_json,
sign_using_nfc, goto_home):
settings_set("chain", "XTN" if testnet else "BTC")
for _ in range(5):
# need to wait for ApproveMessageSign to be popped from ux stack
@ -315,43 +611,14 @@ def test_nfc_msg_signing(msg, path, str_addr_fmt, nfc_write_text, nfc_read_text,
except:
time.sleep(0.5)
pick_menu_item('Advanced/Tools')
pick_menu_item('NFC Tools')
pick_menu_item('Sign Message')
if str_addr_fmt != "":
addr_fmt = msg_sign_unmap_addr_fmt[str_addr_fmt]
body = "\n".join([msg, path, str_addr_fmt])
else:
addr_fmt = AF_CLASSIC
body = "\n".join([msg, path])
nfc_write_text(body)
time.sleep(0.5)
_, story = cap_story()
assert "Ok to sign this?" in story
assert msg in story
assert path.replace("'", "h") in story
press_select()
signed_msg = nfc_read_text()
if "BITCOIN SIGNED MESSAGE" not in signed_msg:
# missed it? again
signed_msg = nfc_read_text()
press_select() # exit NFC animation
pmsg, addr, sig = parse_signed_message(signed_msg)
assert pmsg == msg
addr_vs_path(addr, path, addr_fmt)
assert verify_message(addr, sig, msg) is True
time.sleep(0.5)
_, story = cap_story()
assert f"Press {OK} to share again" in story
press_select()
signed_msg_again = nfc_read_text()
assert signed_msg == signed_msg_again
press_cancel() # exit NFC animation
press_cancel() # do not want to share again
addr, sig, ret_msg = sign_using_nfc(msg, path, addr_fmt, testnet=testnet, use_json=use_json)
assert msg == ret_msg
if addr_fmt == AF_CLASSIC and testnet:
res = bitcoind.rpc.verifymessage(sig, addr, ret_msg)
assert res is True
@pytest.fixture
def verify_armored_signature(pick_menu_item, nfc_write_text, press_select,
def verify_armored_signature(pick_menu_item, nfc_write_text,
cap_story, goto_home):
def doit(way, fname=None, signed_msg=None):
goto_home()
@ -383,7 +650,8 @@ def test_verify_signature_file(way, addr_fmt, path, msg, sign_on_microsd, goto_h
cap_story, bitcoind, microsd_path, nfc_write_text,
verify_armored_signature, chain, settings_set):
settings_set("chain", chain)
sig, addr = sign_on_microsd(msg, path, msg_sign_unmap_addr_fmt[addr_fmt])
sig, addr, ret_msg = sign_on_microsd(msg, path, msg_sign_unmap_addr_fmt[addr_fmt])
assert ret_msg == msg
fname = 't-msgsign-signed.txt'
should = RFC_SIGNATURE_TEMPLATE.format(addr=addr, sig=sig, msg=msg)
with open(microsd_path(fname), "r") as f:
@ -392,7 +660,7 @@ def test_verify_signature_file(way, addr_fmt, path, msg, sign_on_microsd, goto_h
title, story = verify_armored_signature(way, fname, should)
assert title == "CORRECT"
assert "Good signature" in story
assert addr in story
assert addr == addr_from_display_format(story.split("\n")[-1])
if (addr_fmt == "p2pkh") and (chain != "BTC"):
res = bitcoind.rpc.verifymessage(addr, sig, msg)
assert res is True
@ -671,4 +939,111 @@ def test_verify_signature_file_truncated(way, microsd_path, cap_story, verify_ar
assert "Armor text MUST be surrounded by exactly five (5) dashes" in story
assert "auth.py" in story
@pytest.mark.parametrize("msg", ["this is the message to sign", "this is meessage to sign\n with newline", "a"*200])
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, AF_P2WPKH])
@pytest.mark.parametrize("acct", [None, 5555])
def test_sign_scanned_text(msg, addr_fmt, acct, goto_home, need_keypress, scan_a_qr,
sign_msg_from_text, cap_story, skip_if_useless_way):
skip_if_useless_way("qr")
goto_home()
need_keypress(KEY_QR)
scan_a_qr(msg)
time.sleep(1)
title, story = cap_story()
assert title == "Simple Text"
assert "Press (0) to sign the text" in story
need_keypress("0")
sign_msg_from_text(msg, addr_fmt, acct, False, 999, "qr", "XTN", True)
@pytest.mark.parametrize("data", [
{"msg": "msg to be signed via QR"},
{"msg": "msg with some\n\t\n control characters", "addr_fmt": "p2sh-p2wpkh"},
{"msg": 100*"CC", "addr_fmt": "p2wpkh", "subpath": "m/900h/0"},
{"msg": "This is my address! @twiiter_nick", "subpath": "m/84h/1h/0h/0/0"},
{"msg": "This is my address! @twiiter_nick", "subpath": "m/49'/0'/5'/1/100"},
])
@pytest.mark.parametrize("way", ["sd", "nfc", "qr"])
def test_sign_scanned_json(data, way, goto_home, need_keypress, scan_a_qr,
cap_story, msg_sign_export, press_select,
addr_vs_path, bitcoind, skip_if_useless_way,
verify_msg_sign_story):
skip_if_useless_way(way)
goto_home()
af = data.get("addr_fmt", None)
if not af:
addr_fmt = addr_fmt_from_subpath(data.get("subpath", None))
else:
addr_fmt = msg_sign_unmap_addr_fmt[af]
need_keypress(KEY_QR)
scan_a_qr(json.dumps(data))
time.sleep(1)
title, story = cap_story()
subpath = verify_msg_sign_story(story, data["msg"], data.get("subpath", None), addr_fmt)
press_select()
signed_msg = msg_sign_export(way)
ret_msg, addr, sig = parse_signed_message(signed_msg)
assert ret_msg == data["msg"]
# check expected addr was used
addr_vs_path(addr, subpath, addr_fmt)
assert verify_message(addr, sig, ret_msg) is True
if addr_fmt == AF_CLASSIC:
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
assert res is True
@pytest.mark.bitcoind
@pytest.mark.parametrize("msg", ["an an an an an an an an", 240*"a"])
@pytest.mark.parametrize("path", ["m/84h/0", "m/44h/0", "m/49h/0", "m"])
def test_sparrow_qr_sign_msg(msg, path, skip_if_useless_way, need_keypress, scan_a_qr, cap_story,
verify_msg_sign_story, press_select, msg_sign_export, addr_vs_path,
bitcoind):
skip_if_useless_way("qr")
tmplt = "signmessage %s ascii:%s"
data = tmplt % (path, msg)
addr_fmt = addr_fmt_from_subpath(path)
need_keypress(KEY_QR)
scan_a_qr(data)
time.sleep(1)
title, story = cap_story()
subpath = verify_msg_sign_story(story, msg, path, addr_fmt)
press_select()
signed_msg = msg_sign_export("qr")
ret_msg, addr, sig = parse_signed_message(signed_msg)
assert ret_msg == msg
# check expected addr was used
addr_vs_path(addr, subpath, addr_fmt)
assert verify_message(addr, sig, ret_msg) is True
if addr_fmt == AF_CLASSIC:
res = bitcoind.rpc.verifymessage(addr, sig, ret_msg)
assert res is True
@pytest.mark.parametrize("msg", [(50*"a")+"\n\n"+(100*"b"), "Balance replenish 564565456254"])
def test_verify_scanned_signed_msg(msg, scan_a_qr, need_keypress, goto_home, cap_story,
skip_if_useless_way):
skip_if_useless_way("qr")
wallet = BIP32Node.from_master_secret(os.urandom(32))
addr = wallet.address()
sk = bytes(wallet.node.private_key)
sig = sign_message(sk, msg.encode())
armored = RFC_SIGNATURE_TEMPLATE.format(addr=addr, sig=sig, msg=msg)
goto_home()
need_keypress(KEY_QR)
scan_a_qr(armored)
time.sleep(1)
title, story = cap_story()
assert title == "CORRECT"
assert "Good signature by address" in story
assert addr == addr_from_display_format(story.split("\n")[-1])
# EOF

View File

@ -12,7 +12,7 @@ from ckcc.protocol import CCProtocolPacker, MAX_TXN_LEN
from pprint import pprint
from base64 import b64encode, b64decode
from base58 import encode_base58_checksum
from helpers import B2A, fake_dest_addr, xfp2str, detruncate_address
from helpers import B2A, fake_dest_addr, xfp2str, addr_from_display_format
from helpers import path_to_str, str_to_path, slip132undo, swab32, hash160
from struct import unpack, pack
from constants import *
@ -99,6 +99,19 @@ def make_multisig(dev, sim_execfile):
# - but can provide str format for deriviation, use {idx} for cosigner idx
def doit(M, N, unique=0, deriv=None, dev_key=False, chain="XTN"):
def _derive(master, origin_der, idx):
if origin_der == "m":
return master
d = origin_der.format(idx=idx) if origin_der else "m/45h"
try:
child = master.subkey_for_path(d)
except IndexError:
# some test cases are using bogus paths
child = master
return child
keys = []
for i in range(N-1):
@ -106,16 +119,7 @@ def make_multisig(dev, sim_execfile):
xfp = unpack("<I", pk.fingerprint())[0]
if not deriv:
sub = pk.subkey_for_path("m/45h")
else:
path = deriv.format(idx=i)
try:
sub = pk.subkey_for_path(path)
except IndexError:
# some test cases are using bogus paths
sub = pk
sub = _derive(pk, deriv, i)
keys.append((xfp, pk, sub))
if dev_key:
@ -127,17 +131,9 @@ def make_multisig(dev, sim_execfile):
pk = BIP32Node.from_wallet_key(simulator_fixed_tprv if chain == "XTN" else simulator_fixed_xprv)
xfp = simulator_fixed_xfp
if not deriv:
sub = pk.subkey_for_path("m/45h")
else:
path = deriv.format(idx=N-1)
try:
sub = pk.subkey_for_path(path)
except IndexError:
# some test cases are using bogus paths
sub = pk
dev_sim = _derive(pk, deriv, N-1)
keys.append((xfp, pk, sub))
keys.append((xfp, pk, dev_sim))
return keys
@ -519,7 +515,7 @@ def test_ms_show_addr(dev, cap_story, press_select, addr_vs_path, bitcoind_p2sh,
#print(story)
if not has_ms_checks:
assert got_addr in story
assert got_addr == addr_from_display_format(story.split("\n\n")[0])
assert all((xfp2str(xfp) in story) for xfp,_,_ in keys)
if bip45:
for i in range(len(keys)):
@ -1266,7 +1262,7 @@ def make_myself_wallet(dev, set_bip39_pw, offer_ms_import, press_select, clear_m
title, story = offer_ms_import(config)
#print(story)
# dont care if update or create; accept it.
# don't care if update or create; accept it.
time.sleep(.1)
press_select()
@ -2024,7 +2020,7 @@ def test_ms_import_nopath(N, xderiv, make_multisig, clear_ms, offer_ms_import):
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
def test_ms_import_many_derivs(M, N, way, make_multisig, clear_ms, offer_ms_import, press_select,
pick_menu_item, cap_story, microsd_path, virtdisk_path, nfc_read_text,
goto_home, load_export, is_q1):
goto_home, load_export, is_q1, need_keypress):
# try config file with different derivation paths given, including None
# - also check we can convert those into Electrum wallets
@ -2047,6 +2043,14 @@ def test_ms_import_many_derivs(M, N, way, make_multisig, clear_ms, offer_ms_impo
sk.node.depth = dp.count('/')
config += '%s: %s\n' % (xfp2str(xfp), sk.hwif(as_private=False))
# need to disable checks for root paths with wrong xfp
goto_home()
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
pick_menu_item("Skip Checks?")
need_keypress("4")
pick_menu_item("Skip Checks")
title, story = offer_ms_import(config)
assert f'Policy: {M} of {N}\n' in story
assert f'P2SH-P2WSH' in story
@ -2121,7 +2125,8 @@ def test_danger_warning(request, descriptor, clear_ms, import_ms_wallet, cap_sto
def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
need_keypress, goto_home, pick_menu_item, cap_story,
import_ms_wallet, make_multisig, settings_set,
enter_number, set_addr_exp_start_idx, desc):
enter_number, set_addr_exp_start_idx, desc,
cap_screen_qr, press_cancel, press_right):
clear_ms()
M, N = M_N
wal_name = f"ax{M}-{N}-{addr_fmt}"
@ -2183,7 +2188,7 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
for ln in story.split('\n'):
if '=>' not in ln: continue
path,chk,addr = ln.split()
path,chk,addr = ln.split(" ", 2)
assert chk == '=>'
assert '/' in path
path = path.replace("[", "").replace("]", "")
@ -2195,6 +2200,19 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
else:
assert len(maps) == (MAX_BIP32_IDX - start_idx) + 1
need_keypress(KEY_QR)
qr_addrs = []
for i in range(10):
addr_qr = cap_screen_qr().decode()
if addr_fmt == AF_P2WSH:
# segwit addresses are case insensitive
addr_qr = addr_qr.lower()
qr_addrs.append(addr_qr)
press_right()
time.sleep(.2)
press_cancel()
c = 0
for idx, (subpath, addr) in enumerate(maps, start=start_idx):
chng_idx = 1 if change else 0
path_mapper = lambda co_idx: str_to_path(derivs[co_idx]) + [chng_idx, idx]
@ -2206,9 +2224,9 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
assert int(subpath.split('/')[-2]) == chng_idx
#print('../0/%s => \n %s' % (idx, B2A(script)))
start, end = detruncate_address(addr)
assert expect.startswith(start)
assert expect.endswith(end)
addr = addr_from_display_format(addr)
assert addr == expect == qr_addrs[c]
c += 1
def test_dup_ms_wallet_bug(goto_home, pick_menu_item, press_select, import_ms_wallet,
@ -2288,11 +2306,12 @@ def test_bitcoind_ms_address(change, M_N, addr_fmt, clear_ms, goto_home, need_ke
pick_menu_item, cap_menu, cap_story, make_multisig, import_ms_wallet,
microsd_path, bitcoind_d_wallet_w_sk, use_regtest, load_export, way,
is_q1, press_select, start_idx, settings_set, set_addr_exp_start_idx,
desc):
desc, garbage_collector, virtdisk_path):
use_regtest()
clear_ms()
bitcoind = bitcoind_d_wallet_w_sk
M, N = M_N
path_f = microsd_path if way == "sd" else virtdisk_path
# whether to import as descriptor or old school to CC
descriptor = random.choice([True, False])
bip67 = True
@ -2343,7 +2362,12 @@ def test_bitcoind_ms_address(change, M_N, addr_fmt, clear_ms, goto_home, need_ke
assert "change addresses." not in story
assert "(0)" not in story
contents = load_export(way, label="Address summary", is_json=False, sig_check=False)
if way != "nfc":
contents, exp_fname = load_export(way, label="Address summary", is_json=False,
sig_check=False, ret_fname=True)
garbage_collector.append(path_f(exp_fname))
else:
contents = load_export(way, label="Address summary", is_json=False, sig_check=False)
addr_cont = contents.strip()
goto_home()
pick_menu_item('Settings')
@ -2351,7 +2375,12 @@ def test_bitcoind_ms_address(change, M_N, addr_fmt, clear_ms, goto_home, need_ke
press_select() # only one enrolled multisig - choose it
pick_menu_item('Descriptors')
pick_menu_item("Bitcoin Core")
contents = load_export(way, label="Bitcoin Core multisig setup", is_json=False, sig_check=False)
if way != "nfc":
contents, exp_fname = load_export(way, label="Bitcoin Core multisig setup", is_json=False,
sig_check=False, ret_fname=True)
garbage_collector.append(path_f(exp_fname))
else:
contents = load_export(way, label="Bitcoin Core multisig setup", is_json=False, sig_check=False)
text = contents.replace("importdescriptors ", "").strip()
# remove junk
r1 = text.find("[")
@ -2521,7 +2550,6 @@ def bitcoind_multisig(bitcoind, bitcoind_d_sim_watch, need_keypress, cap_story,
def test_legacy_multisig_witness_utxo_in_psbt(bitcoind, use_regtest, clear_ms, microsd_wipe, goto_home, need_keypress,
pick_menu_item, cap_story, load_export, microsd_path, cap_menu, try_sign,
is_q1, press_select):
use_regtest()
clear_ms()
microsd_wipe()
@ -2896,8 +2924,9 @@ def test_bitcoind_MofN_tutorial(m_n, desc_type, clear_ms, goto_home, need_keypre
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/1/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#sj7lxn0l"),
("All keys must be ranged", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/0,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#9h02aqg5"),
("Key derivation too long", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#fy9mm8dt"),
("Key origin info is required", "wsh(sortedmulti(2,tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#ypuy22nw"),
("xpub depth", "wsh(sortedmulti(2,[0f056943]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#nhjvt4wd"),
# ("Key origin info is required", "wsh(sortedmulti(2,tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#ypuy22nw"),
("xpub xfp wrong 0F056943", "wsh(sortedmulti(2,[0f056943]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#nhjvt4wd"),
("xpub depth", "wsh(sortedmulti(2,[0f056943/0h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))"),
("Key derivation too long", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0))#s487stua"),
("Cannot use hardened sub derivation path", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0'/*))#3w6hpha3"),
("M must be <= N", "wsh(sortedmulti(3,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#uueddtsy"),
@ -3426,7 +3455,7 @@ def test_unsort_multisig_setting(settings_set, import_ms_wallet, goto_home,
pick_menu_item, cap_story, need_keypress,
settings_get, clear_ms, press_select, is_q1):
clear_ms()
mi = "Unsorted Multisig" if is_q1 else "Unsorted Multi"
mi = "Unsorted Multisig?" if is_q1 else "Unsorted Multi?"
settings_set("unsort_ms", 0) # OFF by default
with pytest.raises(Exception) as e:
import_ms_wallet(2, 3, "p2wsh", descriptor=True, bip67=False,
@ -3439,8 +3468,9 @@ def test_unsort_multisig_setting(settings_set, import_ms_wallet, goto_home,
pick_menu_item(mi)
time.sleep(.1)
title, story = cap_story()
assert '"multi(...)" unsorted multisig wallets that do not follow BIP-67.' in story
assert 'preserve order of the keys' in story
assert '"multi(...)" unsorted multisig wallets that DO NOT follow BIP-67.' in story
assert ("CRUCIAL importance to backup multisig descriptor"
" for unsorted wallets in order to preserve key ordering") in story
assert 'USE AT YOUR OWN RISK' in story
assert 'Press (4)' in story
need_keypress("4")
@ -3547,4 +3577,195 @@ def test_json_import_failures(err, config, offer_ms_import):
offer_ms_import(json.dumps(config))
assert err in e.value.args[0]
@pytest.mark.parametrize("desc", [True, False])
def test_root_keys_import(desc, import_ms_wallet, clear_ms, fake_ms_txn, try_sign):
clear_ms()
M, N = 2, 3
import_ms_wallet(M, N, "p2wsh", accept=True, name="root",
common="m", descriptor=desc)
@pytest.mark.bitcoind
def test_cc_root_key(import_ms_wallet, bitcoind, use_regtest, clear_ms, microsd_wipe, goto_home,
pick_menu_item, cap_story, press_select, need_keypress, offer_ms_import,
cap_menu, load_export, try_sign, goto_address_explorer, settings_set):
# only CC has root key here, not practical to attempt get xpub from core, if possible
use_regtest()
clear_ms()
microsd_wipe()
M, N = 2, 2
cosigner = bitcoind.create_wallet(wallet_name=f"bds", disable_private_keys=False, blank=False,
passphrase=None, avoid_reuse=False, descriptors=True)
ms = bitcoind.create_wallet(
wallet_name=f"watch_only_roots", disable_private_keys=True,
blank=True, passphrase=None, avoid_reuse=False, descriptors=True
)
goto_home()
# get key from bitcoind cosigner
target_desc = ""
bitcoind_descriptors = cosigner.listdescriptors()["descriptors"]
for desc in bitcoind_descriptors:
if desc["desc"].startswith("pkh(") and desc["internal"] is False:
target_desc = desc["desc"]
core_desc, checksum = target_desc.split("#")
# remove pkh(....)
core_key = core_desc[4:-1]
desc = f"wsh(sortedmulti(2,{core_key},[{xfp2str(simulator_fixed_xfp).lower()}]{simulator_fixed_tpub}/0/*))"
desc_info = ms.getdescriptorinfo(desc)
desc_w_checksum = desc_info["descriptor"] # with checksum
title, story = offer_ms_import(desc_w_checksum)
assert "Create new multisig wallet?" in story
assert f"All {N} co-signers must approve spends" in story
assert "P2WSH" in story
press_select() # approve multisig import
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
menu = cap_menu()
pick_menu_item(menu[0]) # pick imported descriptor multisig wallet
pick_menu_item("Descriptors")
pick_menu_item("Bitcoin Core")
text = load_export("sd", label="Bitcoin Core multisig setup", is_json=False, sig_check=False)
text = text.replace("importdescriptors ", "").strip()
# remove junk
r1 = text.find("[")
r2 = text.find("]", -1, 0)
text = text[r1: r2]
core_desc_object = json.loads(text)
# import descriptors to watch only wallet
res = ms.importdescriptors(core_desc_object)
for obj in res:
assert obj["success"], obj
addr_type = "bech32"
multi_addr = ms.getnewaddress("", addr_type)
bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # mining
dest_addr = ms.getnewaddress("", addr_type)
# create funded PSBT
psbt_resp = ms.walletcreatefundedpsbt(
[], [{dest_addr: 5}], 0, {"fee_rate": 2, "change_type": addr_type}
)
_, updated = try_sign(base64.b64decode(psbt_resp.get("psbt")))
done = cosigner.walletprocesspsbt(base64.b64encode(updated).decode(), True)["psbt"]
rr = ms.finalizepsbt(done)
assert rr['complete']
tx_hex = rr["hex"]
res = bitcoind.supply_wallet.testmempoolaccept([tx_hex])
assert res[0]["allowed"]
txn_id = bitcoind.supply_wallet.sendrawtransaction(rr['hex'])
assert len(txn_id) == 64
bitcoind_addrs = ms.deriveaddresses(desc_w_checksum, [0,250])
goto_address_explorer()
pick_menu_item("2-of-2")
need_keypress('1') # SD
contents = load_export("sd", label="Address summary", is_json=False, sig_check=False)
cc_addrs = contents.strip().split("\n")[1:]
# Generate the addresses file and get each line in a list
for i, line in enumerate(cc_addrs):
addr = line.split(",")[1][1:-1]
assert addr == bitcoind_addrs[i]
@pytest.mark.parametrize("has_orig", [False, True])
def test_originless_keys(get_cc_key, bitcoin_core_signer, bitcoind, offer_ms_import,
pick_menu_item, load_export, goto_home, cap_menu, clear_ms,
use_regtest, press_select, start_sign, end_sign, cap_story,
has_orig, need_keypress):
# can be both:
# a.) just ranged xpub without origin info -> xpub1/<0;1>/*
# b.) ranged xpub with its fp -> [xpub1_fp]xpub1/<0;1>/*
use_regtest()
clear_ms()
af = "bech32"
name = "originless_multlisig"
cc_key = get_cc_key("m/84h/1h/0h")
cs, ck = bitcoin_core_signer(name+"_signer")
originless_ck = ck.split("]")[-1]
n = BIP32Node.from_hwif(originless_ck.split("/")[0]) # just extended key
fp_str = "[" + n.fingerprint().hex() + "]"
if has_orig:
originless_ck = fp_str + originless_ck
tmplt = "wsh(sortedmulti(2,@0,@1))"
desc = tmplt.replace("@0", cc_key)
desc = desc.replace("@1", originless_ck)
to_import = {"desc": desc, "name": name}
offer_ms_import(json.dumps(to_import))
press_select()
wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True,
passphrase=None, avoid_reuse=False, descriptors=True)
goto_home()
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
menu = cap_menu()
assert menu[0] == f"2/2: {name}"
pick_menu_item(menu[0]) # pick imported descriptor miniscript wallet
pick_menu_item("Descriptors")
pick_menu_item("Bitcoin Core")
text = load_export("sd", label="Bitcoin Core multisig setup", is_json=False, sig_check=False)
text = text.replace("importdescriptors ", "").strip()
# remove junk
r1 = text.find("[")
r2 = text.find("]", -1, 0)
text = text[r1: r2]
core_desc_object = json.loads(text)
res = wo.importdescriptors(core_desc_object)
for obj in res:
assert obj["success"]
# fund wallet
addr = wo.getnewaddress("", af)
assert bitcoind.supply_wallet.sendtoaddress(addr, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
unspent = wo.listunspent()
assert len(unspent) == 1
# split to 10 utxos
dest_addrs = [wo.getnewaddress(f"a{i}", af) for i in range(10)]
psbt_resp = wo.walletcreatefundedpsbt(
[],
[{a: 4} for a in dest_addrs] + [{bitcoind.supply_wallet.getnewaddress(): 5}],
0,
{"fee_rate": 3, "change_type": af, "subtractFeeFromOutputs": [0]},
)
psbt = psbt_resp.get("psbt")
start_sign(base64.b64decode(psbt))
time.sleep(.1)
title, story = cap_story()
assert title == "OK TO SEND?"
assert "Consolidating" not in story
cc_signed = end_sign(True)
cc_signed = base64.b64encode(cc_signed).decode()
final_psbt_o = cs.walletprocesspsbt(cc_signed, True, "ALL")
final_psbt = final_psbt_o["psbt"]
assert psbt != final_psbt
res = wo.finalizepsbt(final_psbt)
assert res["complete"]
tx_hex = res["hex"]
res = wo.testmempoolaccept([tx_hex])
assert res[0]["allowed"]
res = wo.sendrawtransaction(tx_hex)
assert len(res) == 64 # tx id
# EOF

View File

@ -5,7 +5,7 @@
import pytest, time, json, random, os, pdb
from helpers import prandom
from charcodes import *
from constants import AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2WPKH
from test_bbqr import readback_bbqr
from bbqr import split_qrs
@ -43,10 +43,10 @@ def goto_notes(cap_story, cap_menu, press_select, goto_home, pick_menu_item):
@pytest.fixture
def need_some_notes(settings_get, settings_set):
# create a note or use what's there, provide as obj
def doit():
def doit(title='Title Here', body='Body'):
notes = settings_get('notes', [])
if not notes:
settings_set('notes', [dict(misc='Body', title='Title Here')])
settings_set('notes', [dict(misc=body, title=title)])
return notes
return doit
@ -93,7 +93,7 @@ def delete_note(press_select, goto_notes, cap_menu, pick_menu_item,
@pytest.fixture
def build_note(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story,
need_keypress, cap_screen_qr, readback_bbqr, nfc_read_text,
press_select, press_cancel, is_headless):
press_select, press_cancel, is_headless, nfc_disabled):
def doit(n_title, n_body):
# we don't try to preserve leading/trailing spaces on note bodies
@ -142,12 +142,13 @@ def build_note(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story,
# hidden NFC button on menu feature
m = cap_menu()
assert m[1] == 'View Note'
need_keypress(KEY_NFC)
time.sleep(.1)
nfc_rb = nfc_read_text()
time.sleep(.1)
assert nfc_rb == n_body
press_cancel()
if not nfc_disabled:
need_keypress(KEY_NFC)
time.sleep(.1)
nfc_rb = nfc_read_text()
time.sleep(.1)
assert nfc_rb == n_body
press_cancel()
# export
pick_menu_item('Export')
@ -181,7 +182,7 @@ def build_password(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story,
cap_text_box, settings_get, settings_set, scan_a_qr,
press_select, press_cancel, is_headless):
def doit(n_title, n_user=None, n_pw=None, n_site=None, n_body=None, key_pw=None):
def doit(n_title, n_user=None, n_pw='secret', n_site=None, n_body=None, key_pw=None):
goto_notes('New Password')
enter_text(n_title)
if n_user:
@ -384,7 +385,7 @@ def test_huge_notes(size, encoding, goto_notes, enter_text, cap_menu, need_keypr
time.sleep(.5) # decompression time in some cases
m = cap_menu()
assert m[-1] == 'Export'
assert m[-2] == 'Export'
notes = settings_get('notes')
assert len(notes) == 1
@ -448,6 +449,33 @@ def test_top_export(goto_notes, pick_menu_item, cap_story, need_keypress, settin
assert obj['coldcard_notes'] == notes
need_keypress(KEY_ENTER)
def test_sort_by_title(goto_notes, pick_menu_item, cap_story, need_keypress, settings_get,
settings_set, build_note, cap_menu, build_password):
settings_set('notes', [])
build_note('ZZZ', 'b1')
goto_notes()
assert 'Sort By Title' not in cap_menu()
build_note('MMM', 'b2')
build_note('AAA', 'b3')
build_note('mmm', 'b2')
build_note('Aaa', 'b3')
build_password('Bbb')
notes = settings_get('notes')
goto_notes()
pick_menu_item('Sort By Title')
# effect is immedate
after = settings_get('notes', [])
assert sorted((i['title'] for i in after), key=lambda i:i.lower()) \
== [i['title'] for i in after]
def test_top_import(goto_notes, cap_menu, cap_story, need_keypress, settings_get,
settings_set, scan_a_qr, need_some_notes):
# make some
@ -624,4 +652,41 @@ def test_tmp_notes_separation(goto_notes, pick_menu_item, generate_ephemeral_wor
assert 'pwd-tmp' not in mm
assert 'note-tmp2' not in mm
@pytest.mark.parametrize("msg", ["COLDCARD rocks!", "cc\nCC"])
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH])
@pytest.mark.parametrize("acct", [None, 0, 9999])
@pytest.mark.parametrize("way", ["sd", "qr", "nfc", "vdisk"])
def test_sign_note_body(msg, addr_fmt, acct, need_some_notes,
pick_menu_item, sign_msg_from_text, way,
goto_notes, settings_set):
settings_set("notes", [])
title = "aaa"
need_some_notes(title, msg)
goto_notes()
pick_menu_item(f"1: {title}")
pick_menu_item("Sign Note Text")
sign_msg_from_text(msg, addr_fmt, acct, False, 0, way)
@pytest.mark.parametrize("chain", ["BTC", "XTN"])
@pytest.mark.parametrize("change", [True, False])
@pytest.mark.parametrize("idx", [None, 0, 9999])
def test_sign_password_free_form(chain, change, idx, need_some_passwords, settings_set,
goto_notes, pick_menu_item, sign_msg_from_text):
settings_set('notes', []) # clear
title = "A"
msg = 'More Notes AAAA'
settings_set('notes', [
{'misc': msg,
'password': 'fds65fd5f1sd51s',
'site': 'https://a.com',
'title': title,
'user': 'AAA'}
])
goto_notes()
pick_menu_item(f"1: {title}")
pick_menu_item("Sign Note Text")
sign_msg_from_text(msg, AF_P2WPKH, None, change, idx, "qr", chain)
# EOF

View File

@ -5,10 +5,11 @@
import pytest, time, io, csv, json
from txn import fake_address
from base58 import encode_base58_checksum
from helpers import hash160, taptweak
from helpers import hash160, taptweak, addr_from_display_format
from bip32 import BIP32Node
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from constants import simulator_fixed_xprv, simulator_fixed_tprv, addr_fmt_names
from charcodes import KEY_QR
@pytest.fixture
def wipe_cache(sim_exec):
@ -100,8 +101,7 @@ def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
menu_item = expect_name = 'Classic P2PKH'
path = "m/44h/{ct}h/{acc}h"
elif addr_fmt == AF_P2WPKH_P2SH:
expect_name = 'P2WPKH-in-P2SH'
menu_item = 'P2SH-Segwit'
menu_item = expect_name = 'P2SH-Segwit'
path = "m/49h/{ct}h/{acc}h"
clear_ms()
elif addr_fmt == AF_P2WPKH:
@ -169,25 +169,40 @@ def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
@pytest.mark.parametrize('valid', [ True, False] )
@pytest.mark.parametrize('testnet', [ True, False] )
@pytest.mark.parametrize('method', [ 'qr', 'nfc'] )
def test_ux(valid, testnet, method,
@pytest.mark.parametrize('multisig', [ True, False] )
def test_ux(valid, testnet, method,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
press_cancel, press_select, settings_set, is_q1, nfc_write, need_keypress,
cap_screen, cap_story, load_shared_mod, scan_a_qr
cap_screen, cap_story, load_shared_mod, scan_a_qr, skip_if_useless_way,
sign_msg_from_address, multisig, import_ms_wallet, clear_ms, verify_qr_address
):
skip_if_useless_way(method)
addr_fmt = AF_CLASSIC
if valid:
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
sk = mk.subkey_for_path(path)
addr = sk.address(chain="XTN" if testnet else "BTC")
if multisig:
from test_multisig import make_ms_address, HARD
M, N = 2, 3
expect_name = f'own_ux_test'
clear_ms()
keys = import_ms_wallet(M, N, AF_P2WSH, name=expect_name, accept=1)
# iffy: no cosigner index in this wallet, so indicated that w/ path_mapper
addr, scriptPubKey, script, details = make_ms_address(
M, keys, is_change=0, idx=50, addr_fmt=AF_P2WSH,
testnet=int(testnet), path_mapper=lambda cosigner: [HARD(45), 0, 50]
)
addr_fmt = AF_P2WSH
else:
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
sk = mk.subkey_for_path(path)
addr = sk.address(chain="XTN" if testnet else "BTC")
else:
addr = fake_address(addr_fmt, testnet)
addr = fake_address(addr_fmt, testnet)
if method == 'qr':
if not is_q1:
raise pytest.skip('no QR on Mk4')
goto_home()
pick_menu_item('Scan Any QR Code')
scan_a_qr(addr)
@ -195,7 +210,7 @@ def test_ux(valid, testnet, method,
title, story = cap_story()
assert addr in story
assert addr == addr_from_display_format(story.split("\n\n")[0])
assert '(1) to verify ownership' in story
need_keypress('1')
@ -220,16 +235,31 @@ def test_ux(valid, testnet, method,
time.sleep(1)
title, story = cap_story()
assert addr in story
assert addr == addr_from_display_format(story.split("\n\n")[0])
if title == 'Unknown Address' and not testnet:
assert 'That address is not valid on Bitcoin Testnet' in story
elif valid:
assert title == 'Verified Address'
assert title == ('Verified Address' if is_q1 else "Verified!")
assert 'Found in wallet' in story
assert 'Derivation path' in story
assert 'P2PKH' in story
if is_q1:
# check it can display as QR from here
need_keypress(KEY_QR)
verify_qr_address(addr_fmt, addr)
press_cancel()
if multisig:
assert expect_name in story
assert "Press (0) to sign message with this key" not in story
else:
assert 'P2PKH' in story
assert "Press (0) to sign message with this key" in story
need_keypress('0')
msg = "coinkite CC the most solid HWW"
sign_msg_from_address(msg, addr, path, addr_fmt, method, testnet)
else:
assert title == 'Unknown Address'
assert 'Searched ' in story
@ -240,7 +270,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
pick_menu_item, need_keypress, sim_exec, clear_ms,
import_ms_wallet, press_select, goto_home, nfc_write,
load_shared_mod, load_export_and_verify_signature,
cap_story, load_export, offer_minsc_import):
cap_story, load_export, offer_minsc_import, is_q1):
goto_home()
wipe_cache()
settings_set('accts', [])
@ -299,13 +329,12 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
time.sleep(1)
title, story = cap_story()
assert addr in story
assert title == 'Verified Address'
assert addr == addr_from_display_format(story.split("\n\n")[0])
assert title == ('Verified Address' if is_q1 else "Verified!")
assert 'Found in wallet' in story
# assert 'Derivation path' in story
if af == "P2SH-Segwit":
assert "P2WPKH-in-P2SH" in story
elif af == "Segwit P2WPKH":
if "msc" not in af:
assert 'Derivation path' in story
if af == "Segwit P2WPKH":
assert " P2WPKH " in story
else:
assert af in story

View File

@ -4,22 +4,22 @@
#
import time, pytest, os, random, pdb, struct, base64, binascii, itertools, datetime
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, MAX_TXN_LEN, CCUserRefused
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError
from binascii import b2a_hex, a2b_hex
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput, PSBT_IN_REDEEM_SCRIPT
from io import BytesIO
from pprint import pprint, pformat
from pprint import pprint
from decimal import Decimal
from base64 import b64encode, b64decode
from base58 import encode_base58_checksum
from helpers import B2A, fake_dest_addr, parse_change_back
from helpers import B2A, fake_dest_addr, parse_change_back, addr_from_display_format
from helpers import xfp2str, seconds2human_readable, hash160
from msg import verify_message
from bip32 import BIP32Node
from constants import ADDR_STYLES, ADDR_STYLES_SINGLE, SIGHASH_MAP, simulator_fixed_tpub
from constants import ADDR_STYLES, ADDR_STYLES_SINGLE, SIGHASH_MAP
from txn import *
from ctransaction import CTransaction, CTxOut, CTxIn, COutPoint
from ckcc_protocol.constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
from ckcc_protocol.constants import STXN_VISUALIZE, STXN_SIGNED
from charcodes import KEY_QR, KEY_RIGHT
@ -560,7 +560,9 @@ def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind,
time.sleep(.1)
_, story = cap_story()
assert chg_addr in story
split_sory = story.split("\n\n")[3].split("\n")
assert split_sory[0] == "Change back:"
assert chg_addr == addr_from_display_format(split_sory[-1])
b4 = BasicPSBT().parse(psbt)
check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[1,])
@ -630,7 +632,7 @@ def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_agains
time.sleep(.1)
_, story = cap_story()
assert chg_addr in story
assert chg_addr == addr_from_display_format(story.split("\n\n")[3].split("\n")[-1])
assert 'Change back:' not in story
end_sign(True)
@ -738,7 +740,9 @@ def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_re
check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[1,],
dests=[(1, expect_addr)])
assert expect_addr in story
split_sory = story.split("\n\n")[3].split("\n")
assert split_sory[0] == "Change back:"
assert expect_addr == addr_from_display_format(split_sory[-1])
assert parse_change_back(story) == (Decimal('1.09997082'), [expect_addr])
end_sign(True)
@ -2015,12 +2019,22 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
for idx, i in enumerate(y.inputs):
if len(sighash) == 1:
assert i.sighash == SIGHASH_MAP[sighash[0]]
target = sighash[0]
sh_num = SIGHASH_MAP[target]
if target == "ALL":
assert i.sighash is None
else:
assert i.sighash == sh_num
else:
assert i.sighash == SIGHASH_MAP[sighash[idx]]
# check signature hash correct checkusm appended
target = sighash[idx]
sh_num = SIGHASH_MAP[target]
if target == "ALL":
assert i.sighash is None
else:
assert i.sighash == sh_num
# check signature hash correct checksum appended
for _, sig in i.part_sigs.items():
assert sig[-1] == i.sighash
assert sig[-1] == sh_num
resp = finalize_v2_v0_convert(y)
@ -3106,7 +3120,7 @@ def test_taproot_keyspend(use_regtest, bitcoind_d_sim_watch, start_sign, end_sig
assert title == 'OK TO SEND?'
assert "Consolidating" in story # self-spend
assert " 1 input\n 2 outputs" in story
addrs = story.split("\n\n")[3].split("\n")[-2:]
addrs = [addr_from_display_format(l) for l in story.split("\n") if l and (l[0] == '\x02')]
assert len(addrs) == 2
for addr in addrs:
assert addr.startswith("bcrt1p")