commit
0427af8caa
@ -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.
|
||||
|
||||
|
||||
@ -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))`
|
||||
|
||||
2
external/micropython
vendored
2
external/micropython
vendored
@ -1 +1 @@
|
||||
Subproject commit 97d35f058f504a354fc6df79a8b3db5c91862501
|
||||
Subproject commit 4107246f8a080807b62c3b4838e71e812ea68b6f
|
||||
@ -74,4 +74,9 @@ special_chars = dict(small=[
|
||||
x x x x x
|
||||
'''),
|
||||
|
||||
# thin space
|
||||
('\u2009', dict(y=0, w=5), '''\
|
||||
|
||||
'''),
|
||||
|
||||
])
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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-----
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
366
shared/auth.py
366
shared/auth.py
@ -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):
|
||||
|
||||
@ -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.")
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -11,7 +11,6 @@ def make_flash_fs():
|
||||
os.VfsLfs2.mkfs(fl)
|
||||
|
||||
os.mount(fl, '/flash')
|
||||
|
||||
os.mkdir('/flash/settings')
|
||||
|
||||
def make_psram_fs():
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
15
shared/ux.py
15
shared/ux.py
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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...
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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\
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -44,6 +44,7 @@ addr_fmt_names = {
|
||||
AF_P2WSH: 'p2wsh',
|
||||
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
|
||||
AF_P2WSH_P2SH: 'p2wsh-p2sh',
|
||||
AF_P2TR: "p2tr",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"])
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user