Compare commits

...

53 Commits

Author SHA1 Message Date
Peter D. Gray
6bfb239faf
New release: 2024-12-18T1413-v6.3.4X 2024-12-18 09:13:27 -05:00
Peter D. Gray
39c6618f98
Signed for mk4 release. 2024-12-18 09:13:24 -05:00
Peter D. Gray
f6e2061949
For 2024-12-18T1413-v6.3.4X 2024-12-18 09:13:23 -05:00
Peter D. Gray
bdfe55586b
New release: 2024-12-18T1408-v6.3.4QX 2024-12-18 09:08:14 -05:00
Peter D. Gray
97af165b2f
Signed for q1 release. 2024-12-18 09:08:10 -05:00
Peter D. Gray
56db1def61
For 2024-12-18T1356-v6.3.4QX 2024-12-18 08:56:17 -05:00
scgbckbone
87be155bc2 Q fix 2024-12-17 16:10:55 +01:00
scgbckbone
1d27562d6e remove changes not included from ChangeLog 2024-12-17 16:10:55 +01:00
scgbckbone
407bf7b227 versions 2024-12-17 16:10:55 +01:00
scgbckbone
832787e1e6 fixes 2024-12-17 16:10:55 +01:00
scgbckbone
bac2f52251 NFC code optimizations 2024-12-17 16:10:55 +01:00
scgbckbone
8311ff73ec changelog 2024-12-17 16:10:55 +01:00
scgbckbone
e02b4bf745 prevent ownership yikes
(cherry picked from commit 8957ad3c10)
2024-12-17 16:10:55 +01:00
scgbckbone
69b21353a9 provide generalized nfc reader function (saving bytes)
(cherry picked from commit d270cf66c6)
2024-12-17 16:10:55 +01:00
scgbckbone
dec95d165f improve Wipe LFS UX message
(cherry picked from commit c425fc6bcc)
2024-12-17 16:10:55 +01:00
scgbckbone
ae4dfa6c12 save bytes drv_entro.py
(cherry picked from commit c9882d7a8a)
2024-12-17 16:10:55 +01:00
scgbckbone
2c7b10e6a2 Mk4: export descriptor as simple QR
(cherry picked from commit 85b478346b)
2024-12-17 16:10:55 +01:00
scgbckbone
aa154e1d8c do not allow to delete current active tmp seed from seed vault and purge its settings
(cherry picked from commit 95b13083dc)
2024-12-17 16:10:55 +01:00
scgbckbone
128658e1e6 deltamode & Seed Vault
(cherry picked from commit 5568082f35)
2024-12-17 16:10:55 +01:00
scgbckbone
282e8fbc28 deltamode & secure notes and passwords
(cherry picked from commit 8f86ed1c0e)
2024-12-17 16:10:55 +01:00
scgbckbone
2efa9e7f27 bugfix: bless firmware causes hanging progress bar
(cherry picked from commit 1b54536eff)
2024-12-17 16:10:55 +01:00
Henrique Albuquerque
a76408b335 Fix grammar error
(cherry picked from commit d1d104cb7e)
2024-12-17 16:10:55 +01:00
scgbckbone
205b26f392 do NOT allow to enable/disable Seed Vault while in temporary seed mode
(cherry picked from commit 9e1ce7a956)
2024-12-17 16:10:55 +01:00
scgbckbone
1cdb0900b7 bugfix: UX checkmark was still on the Miniscript menu item even after all miniscripts deleted - fixed 2024-12-16 09:06:39 -05:00
scgbckbone
053c9165bb bugfix: single key miniscript wallets 2024-12-16 09:06:39 -05:00
doc-hex
b07fb8a6b3
Merge pull request #435 from scgbckbone/edge_Sep2024_test
Edge Nov2024
2024-12-14 11:46:52 -05:00
scgbckbone
35c16c1491 big boy test 2024-11-26 07:57:16 +01:00
scgbckbone
49de639bac tests 2024-11-21 10:32:36 +01:00
scgbckbone
1a41164271 bugfix: fill_policy; always keep subderivation path in policy string 2024-11-20 03:55:52 +01:00
scgbckbone
e344bac322 Merge branch 'new_edge' into edge_Sep2024_test
# Conflicts:
#	releases/Next-ChangeLog.md
#	releases/signatures.txt
#	shared/actions.py
#	shared/auth.py
#	shared/bsms.py
#	shared/decoders.py
#	shared/descriptor.py
#	shared/multisig.py
#	shared/nfc.py
#	shared/psbt.py
#	shared/utils.py
#	stm32/COLDCARD_Q1/file_time.c
#	stm32/MK4-Makefile
#	stm32/Q1-Makefile
#	testing/conftest.py
#	testing/descriptor.py
#	testing/test_bsms.py
#	testing/test_multisig.py
#	testing/test_sign.py
2024-09-17 15:46:00 +02:00
scgbckbone
e3f75619d5 fixes after master rebase 2024-09-17 15:26:13 +02:00
scgbckbone
99ab403f66 correct edge versions 2024-09-17 15:26:13 +02:00
Peter D. Gray
9b4cc260aa New release: 2024-07-04T1501-v6.3.3X 2024-09-17 15:26:13 +02:00
Peter D. Gray
fce554257f New release: 2024-07-04T1500-v6.3.3QX 2024-09-17 15:26:13 +02:00
Peter D. Gray
06dde5af49 updated 2024-09-17 15:26:13 +02:00
scgbckbone
ef2c5a7f1f unspend( & ranged unspendable taproot internal keys 2024-09-17 15:26:13 +02:00
scgbckbone
b1fe5e194d miniscript/tapscript; BSMS; show multisig/miniscript addresses in exports 2024-09-17 15:26:13 +02:00
scgbckbone
427cf89975 taproot singlesig 2024-09-17 15:26:13 +02:00
scgbckbone
bb87cdd59a correct edge versions 2024-07-04 11:40:27 -04:00
Peter D. Gray
5b2772a4b0
New release: 2024-07-04T1501-v6.3.3X 2024-07-04 11:01:33 -04:00
Peter D. Gray
e7f8a1a71e
Signed for mk4 release. 2024-07-04 11:01:28 -04:00
Peter D. Gray
f7f41fe6e3
New release: 2024-07-04T1500-v6.3.3QX 2024-07-04 11:00:10 -04:00
Peter D. Gray
47754d1785
Signed for q1 release. 2024-07-04 11:00:05 -04:00
Peter D. Gray
bf67c5ad7e
updated 2024-07-04 10:58:22 -04:00
scgbckbone
a18938cefd unspend( & ranged unspendable taproot internal keys 2024-07-04 10:18:04 -04:00
scgbckbone
f618af12d1 miniscript/tapscript; BSMS; show multisig/miniscript addresses in exports 2024-07-04 10:18:04 -04:00
scgbckbone
d2920d1c60 taproot singlesig 2024-07-04 10:18:04 -04:00
scgbckbone
a798e96de0 ui: newline in visualize bip21 2024-07-04 10:18:04 -04:00
Peter D. Gray
89405e819a mistake 2024-07-04 10:18:04 -04:00
Peter D. Gray
eef1f6d561 switch to space in word menu 2024-07-04 10:18:04 -04:00
Peter D. Gray
98db85f2e2 restored 2024-07-04 10:18:04 -04:00
Peter D. Gray
e1ff15bab4 edits 2024-07-04 10:18:04 -04:00
Peter D. Gray
991965ac60
Token diff 2024-07-04 09:15:10 -04:00
76 changed files with 12825 additions and 1654 deletions

View File

@ -319,7 +319,7 @@ def doit(keydir, outfn=None, build_dir=None, high_water=False,
pubkey_num=pubkey_num,
timestamp=timestamp(backdate) )
assert FW_MIN_LENGTH <= hdr.firmware_length <= FW_MAX_LENGTH, hdr.firmware_length
assert FW_MIN_LENGTH <= hdr.firmware_length <= FW_MAX_LENGTH_MK4, hdr.firmware_length
if hw_compat & MK_3_OK:
# actual file length limited by size of SPI flash area reserved to txn data/uploads

View File

@ -128,7 +128,8 @@ We will summarize transaction outputs as "change" back into same wallet, however
- `p2wsh-p2sh`: _redeemScript_ (which is: `0x00 + 0x20 + sha256(witnessScript)`), and
_witnessScript_ (which contains the multisig script)
- `p2wsh`: only _witnessScript_ (which contains the actual multisig script)
- `p2tr`(keypath singlesig): no _redeemScript_, no _witnessScript_ and output key MUST commit to an unspendable script path as follows `Q = P + int(hashTapTweak(bytes(P)))G`
- `p2tr`(scriptpath multisig): _taproot_merkle_root_ and _leaf_script_ more info in docs/taproot.md
# Derivation Paths

27
docs/miniscript.md Normal file
View File

@ -0,0 +1,27 @@
# Miniscript
**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Miniscript and MiniTapscript.
## Import/Export
* `Settings` -> `Miniscript` -> `Import from file`
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) allowed for import
* `Settings` -> `Miniscript` -> `<name>` -> `Descriptors`
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) are exported
* export extended keys to participate in miniscript:
* `Advanced/Tools` -> `Export Wallet` -> `Generic JSON`
* `Settings` -> `Multisig Wallets` -> `Export XPUB`
## Address Explorer
Same as with basic multisig. After miniscript wallet is imported,
item with `<name>` is added to `Address Explorer` menu.
## Limitations
* no duplicate keys in miniscript (at least change indexes in subderivation has to be different)
* subderivation may be omitted during the import - default `<0;1>/*` is implied
* only keys with key origin info `[xfp/p/a/t/h]xpub`
* maximum number of keys allowed in segwit v0 miniscript is 20
* check MiniTapscript limitations in `docs/taproot.md`

75
docs/taproot.md Normal file
View File

@ -0,0 +1,75 @@
# Taproot
**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Schnorr signatures ([BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)),
Taproot ([BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki))
and Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support.
## Output script (a.k.a address) generation
If the spending conditions do not require a script path, the output key MUST commit to an unspendable script path.
`Q = P + int(hashTapTweak(bytes(P)))G` a.k.a internal key MUST be tweaked by `TapTweak` tagged hash of itself. If
the spending conditions require script path, internal key MUST be tweaked by `TapTweak` tagged hash of tree merkle root.
Addresses in `Address Explorer` for `p2tr` are generated with above-mentioned methods. Outputs `scriptPubkeys` in PSBT
MUST be generated with above-mentoned methods to be considered change.
## Allowed descriptors
1. Single signature wallet without script path: `tr(key)`
2. Tapscript multisig with internal key and up to 8 leaf scripts:
* `tr(internal_key, sortedmulti_a(2,@0,@1))`
* `tr(internal_key, pk(@0))`
* `tr(internal_key, {sortedmulti_a(2,@0,@1),pk(@2)})`
* `tr(internal_key, {or_d(pk(@0),and_v(v:pkh(@1),older(1000))),pk(@2)})`
## Provably unspendable internal key
There are few methods to provide/generate 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.
`tr(xpub/<0:1>/*, sortedmulti_a(2,@0,@1))` which is the same thing as `tr(xpub, sortedmulti_a(2,@0,@1))` because `/<0;1>/*` is implied if not derivation path not provided.
2. **(recommended)** Use `unspend(` [notation](https://gist.github.com/sipa/06c5c844df155d4e5044c2c8cac9c05e#unspendable-keys). Has to be ranged.
`tr(unspend(77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76)/<0:1>/*, sortedmulti_a(2,@0,@1))`
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))`
4. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable.
`tr(r=@, sortedmulti_a(MofN))`
5. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created.
`tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(2,@0,@1))`
Option 3. leaks the information that key path spending is not possible and therefore is not recommended privacy-wise.
Options 4. and 5. are problematic to some extent as internal key is static. Use recommended options 1. and 2. if the fact that internal key is unspendable should remain private.
## Limitations
### Tapscript Limitations
In current version only `TREE` of max depth 4 is allowed (max 8 leaf script allowed).
Taproot single leaf multisig has artificial limit of max 32 signers (M=N=32).
Number of keys in taptree is limited to 32.
If Coldcard can sign by both key path and script path - key path has precedence.
### PSBT Requirements
PSBT provider MUST provide following Taproot specific input fields in PSBT:
1. `PSBT_IN_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
2. `PSBT_IN_TAP_INTERNAL_KEY` MUST match internal key provided in `PSBT_IN_TAP_BIP32_DERIVATION`
3. `PSBT_IN_TAP_MERKLE_ROOT` MUST be empty if there is no script path. Otherwise it MUST match what Coldcard can calculate from registered descriptor.
4. `PSBT_IN_TAP_LEAF_SCRIPT` MUST be specified if there is a script path. Currently MUST be of length 1 (only one script allowed)
PSBT provider MUST provide following Taproot specific output fields in PSBT:
1. `PSBT_OUT_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
2. `PSBT_OUT_TAP_INTERNAL_KEY` must match internal key provided in `PSBT_OUT_TAP_BIP32_DERIVATION`
3. `PSBT_OUT_TAP_TREE` with depth, leaf version and script defined. Currently only one script is allowed.

45
releases/EdgeChangeLog.md Normal file
View File

@ -0,0 +1,45 @@
# Change Log
## Warning: Edge Version
```diff
- 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.
```
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
# Mk4 Specific Changes
## 6.3.4X - 2024-07-04
- all updates from `5.4.0`
- Enhancement: Export single sig descriptor with simple QR
# Q Specific Changes
## 6.3.4QX - 2024-07-04
- all updates from version `1.3.0Q`
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
# Release History
- [`History-Edge.md`](History-Edge.md)

73
releases/History-Edge.md Normal file
View File

@ -0,0 +1,73 @@
## Warning: Edge Version
```diff
- 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.
```
## 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
- New Feature: Address ownership for miniscript and tapscript wallets
- Enhancement: Address explorer simplified UI for tapscript addresses
- Bugfix: Constant `AFC_BECH32M` incorrectly set `AFC_WRAPPED` and `AFC_BECH32`.
- Bugfix: Trying to set custom URL for NFC push transaction caused yikes
### Mk4 Specific Changes
- Bugfix: Fix yikes displaying BIP-85 WIF when both NFC and VDisk are OFF
- Bugfix: Fix inability to export change addresses when both NFC and Vdisk id OFF
- Bugfix: In BIP-39 words menu, show space character rather than Nokia-style placeholder
which could be confused for an underscore.
### Q Specific Changes
- Enhancement: Miniscript and (BB)Qr codes
- Bugfix: Properly clear LCD screen after simple QR code is shown
## 6.2.2X - 2024-01-18
- New Feature: Miniscript [USB interface](https://github.com/Coldcard/ckcc-protocol/blob/master/README.md#miniscript)
- New Feature: Named miniscript imports. Wrap descriptor in json
`{"name:"n0", "desc":"<descriptor>"}` with `name` key to use this name instead of the
filename. Mostly usefull for USB and NFC imports that have no file, in which case name
was created from descriptor checksum.
- Enhancement: Allow keys with same origin, differentiated only by change index derivation
in miniscript descriptor.
- Enhancement: HSM `wallet` rule enabled for miniscript
- Enhancement: Add `msas` in to the `share_addrs` HSM [rule](https://coldcard.com/docs/hsm/rules/)
to be able to check miniscript addresses in HSM mode.
- Enhancement: HW Accelerated AES CTR for BSMS and passphrase saver
- Bugfix: Do not allow to import duplicate miniscript
wallets (thanks to [AnchorWatch](https://www.anchorwatch.com/))
- Bugfix: Saving passphrase on SD Card caused a freeze that required reboot
## 6.2.1X - 2023-10-26
- New Feature: Enroll Miniscript wallet via USB (requires ckcc `v1.4.0`)
- New Feature: Temporary Seed from COLDCARD encrypted backup
- Enhancement: Add current temporary seed to Seed Vault from within Seed Vault menu.
If current active temporary seed is not saved yet, `Add current tmp` menu item is
present in Seed Vault menu.
- Reorg: `12 Words` menu option preferred on the top of the menu in all the seed menus
- Enhancement: Mainnet/Testnet separation. Only show wallets for current active chain.
- contains all the changes from the newest stable `5.2.0-mk4` firmware
## 6.1.0X - 2023-06-20
- New Feature: Miniscript and MiniTapscript support (`docs/miniscript.md`)
- Enhancement: Tapscript up to 8 leafs
- Address explorer display refined slightly (cosmetic)
## 6.0.0X - 2023-05-12
- New Feature: Taproot keyspend & Tapscript multisig `sortedmulti_a` (tree depth = 0)
- New Feature: Support BIP-0129 Bitcoin Secure Multisig Setup (BSMS).
Both Coordinator and Signer roles are supported.
- Enhancement: change Key Origin Information export format in multisig `addresses.csv` according to [BIP-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions)
`(m=0F056943)/m/48'/1'/0'/2'/0/0` --> `[0F056943/48'/1'/0'/2'/0/0]`
- Bugfix: correct `scriptPubkey` parsing for segwit v1-v16
- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT

View File

@ -4,19 +4,25 @@ 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
# Mk4 Specific Changes
- tbd
## 5.4.? - 2024-??-??
- tbd
## 5.4.1 - 2024-??-??
- Enhancement: Export single sig descriptor with simple QR
# Q Specific Changes
## 1.3.?Q - 2024-??-??
## 1.3.1Q - 2024-??-??
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.

View File

@ -2,104 +2,33 @@
Hash: SHA256
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
97107b5be1c8b65efa4bd36b7d1798e4ed15917861bd2d40784d66302a61d335 Next-ChangeLog.md
1421e5c7a275a6b1e585460e5358292adfc6c0660315b318a1e86d2d6fdad9a3 Next-ChangeLog.md
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
d7738a68e64215ed512854cfb7daf52302047bb3683a2a9b6620bc51a292a65f History-Edge.md
e5fbd8b5384b2afd1522d6d2d362b482be3e66123dd68ecb1033d1aa57d0b5e6 EdgeChangeLog.md
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce ChangeLog.md
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
4db89ecffa1376bfc68a37110c2041a29afe52b005d527ecde701131168fc19c 2024-09-12T1733-v1.3.0Q-q1-coldcard-factory.dfu
4d83715772b31643abde3b9a0bb328003f4a31d14e2fe9c1e038077a518acaea 2024-07-05T1348-v5.3.3-mk4-coldcard.dfu
020d6d5c3baa724713b2f906112bb95f7eff43c3f5a4f8f11b77d8c2e96ccc88 2024-07-05T1348-v5.3.3-mk4-coldcard-factory.dfu
54da941c8df84fcb84adcc62fdd3ee97d1fc12e2a9a648551ca614fcbacade3f 2024-07-05T1342-v1.2.3Q-q1-coldcard.dfu
7f704aa37887ed84d6a25f124e9b4a31187430d7cf6b198eb83b86af8ae4e5ea 2024-07-05T1342-v1.2.3Q-q1-coldcard-factory.dfu
ddf5ce1ef1ee2e6ba2922b333213d0cb939a2658b294c0f24c0e489de3fe7c75 2024-07-04T1501-v6.3.3X-mk4-coldcard.dfu
9a2c5ef80a6f8212caa3b455e203da3549a79b08b473113662cf80fff587566a 2024-07-04T1459-v6.3.3QX-q1-coldcard.dfu
a990cc94066486a37071c011cd85a29caed433cb4ca3f1c4dce7f715ef81dc3c 2024-06-26T1741-v5.3.2-mk4-coldcard.dfu
218d17069d05c0ec2829e5629c5216121028d15b145c31b552e2f52daa7bf172 2024-06-26T1741-v5.3.2-mk4-coldcard-factory.dfu
b87505b407b0477e2d15f71cfb20645ac55ac5b7c74493d25a2c9c97e807b2b3 2024-06-26T1739-v1.2.2Q-q1-coldcard.dfu
efff41069f3f82d4e69d08a02a565ae0d2cd55c07dbbbe4c1328e6e3b6d8faa1 2024-06-26T1739-v1.2.2Q-q1-coldcard-factory.dfu
90b1edfbe194b093258f9cda8f4add4aa3317e9ea205ff35914da7d91410fdae 2024-05-09T1529-v1.2.1Q-q1-coldcard.dfu
c7889532323f7b0c08e84589c7cc756e2c46e209b4eea031bdfef4a633a813c1 2024-05-09T1529-v1.2.1Q-q1-coldcard-factory.dfu
ef6526d37bc1a929c94dc8388f3863f6cc1582addf26495f761123f0bfb7aa30 2024-05-09T1527-v5.3.1-mk4-coldcard.dfu
98c675e98a18b2437c52e30a9867c271bbca9969771caa34299556ef3fcb1a43 2024-05-09T1527-v5.3.1-mk4-coldcard-factory.dfu
c7c79a21c206e8b0e816c86ef1b43cd6932cb767ed97291d5fbc2f0e749f95b7 2024-05-06T1812-v1.2.0Q-q1-coldcard.dfu
5c6b69948f0193b3a7bd252195136d6d9f84ab14fbc8c5349150e7d238708c6f 2024-05-06T1812-v1.2.0Q-q1-coldcard-factory.dfu
bab6818787eec45ef28b6c297e2504ffd4fa041ab19da8a3fd27543dffe876b8 2024-05-06T1811-v5.3.0-mk4-coldcard.dfu
3da458c0dabe9a17eaeb92ee959006a64a3e6838eeb31f887a18840f020ef8b9 2024-05-06T1811-v5.3.0-mk4-coldcard-factory.dfu
101f336310b9b460d717d91d2572ea9e9ef7ac3edbdaf132c7c3aa46bb89050a 2024-04-02T1416-v1.1.0Q-q1-coldcard.dfu
5d034bc6b1abec49a067a90766bdb769faf9a1b52b2c9b7e541d32484cf783fc 2024-04-02T1416-v1.1.0Q-q1-coldcard-factory.dfu
6ea843a56e87d7d811d90be6bfa4703794bbc8318d9709e88ada05740e03b12d 2024-03-14T1419-v1.0.1Q-q1-coldcard.dfu
f53c79c64f02dd1e860a8d32f9319edd279485d97f07815b2a1eb180a1305459 2024-03-14T1419-v1.0.1Q-q1-coldcard-factory.dfu
122e6d757eb5a8ce073d98a85851f376adec97856336c5a8f05b953b5c87a533 2024-03-10T1537-v1.0.0Q-q1-coldcard.dfu
ae04aaac47f07e10143c75b5c772b54739830214c8234356d003137897f3f4f4 2024-03-10T1537-v1.0.0Q-q1-coldcard-factory.dfu
6aaa9d5bf1726fe4d4a4834010d9b9b6525e8592bb97945cd08cc728fc884068 2024-03-02T1750-v0.0.8Q-q1-coldcard.dfu
a0cd556693fae5b8b03f2a498c0abb1e6d747f91a92bd8f2559a676f8707d840 2024-03-02T1750-v0.0.8Q-q1-coldcard-factory.dfu
18fe081d84a950e1fddb2151ad50917697dfc218cd68e2e359229b0bdadbff37 2024-02-26T1442-v0.0.7Q-q1-coldcard.dfu
e4f4fe89cf3743d794568fd5b32b14551966139e9199602ea10468f925fab1cf 2024-02-26T1442-v0.0.7Q-q1-coldcard-factory.dfu
2dc7a27f43958f2de9851f221183c94258ac915ae43d997b39b644e7b9daff8f 2024-02-22T1423-v0.0.6Q-q1-coldcard.dfu
1e4f4d4c04835d78fcc4857d3264034a56dccf594e307d7408d7c4cdcdb0a926 2024-02-22T1423-v0.0.6Q-q1-coldcard-factory.dfu
d51573c72d8958ea35357d4e0a36ce6aaa2d05924577efb219e2cc189be63f08 2024-02-16T1635-v0.0.5Q-q1-coldcard.dfu
55f4ef9c3ae116f50db938acfc3a4b09717965f82cf6de8cc7385f68cd66d285 2024-02-16T1635-v0.0.5Q-q1-coldcard-factory.dfu
8fd1ced0d5e0338d845f6d5ec5ab069a5143cceade02d4f17e86b7d182b489eb 2024-02-15T1843-v0.0.4Q-q1-coldcard.dfu
43fac084727b0e69bae7fc040a62854673fd585dc2435d93bf146c80762e41cf 2024-02-15T1843-v0.0.4Q-q1-coldcard-factory.dfu
3064bf7f1a039e7cd5c1a13c6aff8cc4338e52ef2177abbdca4b196955f9e434 2024-02-08T2005-v0.0.3Q-q1-coldcard.dfu
788e7a1b182f920016617411b875fa7095ae007c6a53fc476afb1c93f0eed1c9 2024-02-08T2005-v0.0.3Q-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
a9d0b416c3cb4f122f2826283fce82bbc5fe4464817b601a3a5787b1f8aaba20 2024-01-18T1507-v6.2.2X-mk4-coldcard.dfu
4651fb81dc04ac07ae53535f4246ef7f32611c50853de9edaefa68f3c64e1fac 2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
a49cd00808732c67b359c9f86814ddeafc63a1040823b6c1d2035a870575c9ed 2023-12-21T1526-v5.2.2-mk4-coldcard-factory.dfu
06d1048bea43c5d7c72c5e5f395a676620ce884aed0cd152627a86d922e2f3ab 2023-12-19T1444-v5.2.1-mk4-coldcard.dfu
3eb9c4b1add88a6fe412d783b8f4b895241a67e423bbacc6a13816a5216a30fe 2023-12-19T1444-v5.2.1-mk4-coldcard-factory.dfu
cc93209e800bc05386b5613969e62c27b9acd4388e3a922686525da90a505778 2024-01-18T1507-v6.2.2X-mk4-coldcard-factory.dfu
f4457dc44d08cbed9517e6260aa7163ecc254457276d3cdb0c2611af0f49ba9b 2023-10-26T1343-v6.2.1X-mk4-coldcard.dfu
1dcfb450f81883afe8f655239f06e238de7bae51e740cd4aa5ae6a0541772ad8 2023-10-26T1343-v6.2.1X-mk4-coldcard-factory.dfu
7fbed097d2757b21fde920f4b10f5f50d7e1aeca01ff52186dfde4883af5cace 2023-10-10T1735-v5.2.0-mk4-coldcard.dfu
4e3023676be88d6c6480c7f37de302f3a865077f9a2214de9c5a55b24afcba2c 2023-10-10T1735-v5.2.0-mk4-coldcard-factory.dfu
fd707f2f69d006c9db84ceacd2a0dde79c3cb71730750e2676af610942898717 2023-09-08T2009-v5.1.4-mk4-coldcard.dfu
d2a4a8b71b0b102971bf8a6c98968dee776a77e0a5707db862e34be5276fbc78 2023-09-08T2009-v5.1.4-mk4-coldcard-factory.dfu
c03d4e2d1115e9440d1762c95fc82ae5a31122e84ee88d6537a8e75f26f66954 2023-09-07T1501-v5.1.3-mk4-coldcard.dfu
3602f307df06b6658d7731172c2eb3f192a0bc8ee02c606e3cb97c1aa8d49af2 2023-09-07T1501-v5.1.3-mk4-coldcard-factory.dfu
f6fb19d95bd1e38535f137bed60cafbfcd52379a686e3d12f372f881d78e640e 2023-06-26T1241-v4.1.9-coldcard.dfu
489e161f686a0c631fc605054f8e7271208b16191b669174b8a58f5af28b0f4a 2023-06-20T1506-v6.1.0X-mk4-coldcard.dfu
233398cc8f6b9e894072448eb8b8a82a4f546219ce461dd821f0ed0a38b61900 2023-06-19T1627-v4.1.8-coldcard.dfu
66c83c3f95fd3d0796b1e452d2e8ed8ac6a4abead53faf5ae793eceb6f7bbdb5 2023-06-20T1506-v6.1.0X-mk4-coldcard-factory.dfu
2e8ed970f518a476d0b34752ecbad75bab246669aa65de8f43801364c6f5753e 2023-05-12T1316-v6.0.0X-mk4-coldcard.dfu
7aefd5bcce533f15337e83618ebbd42925d336792c82a5ca19a430b209b30b8a 2023-04-07T1330-v5.1.2-mk4-coldcard.dfu
a6c007992139a847f0f238769023727e8cbc05c54c916b388a4dd8bc7490f0aa 2023-04-07T1330-v5.1.2-mk4-coldcard-factory.dfu
99804b440f41ea47675456b4e20e7bb4e9cb434556c5813ab83c26fcda0f4e80 2023-02-27T2105-v5.1.1-mk4-coldcard.dfu
8b37d0f2bf9ca8990f424e5a79fe62405e1ec3aca515760e509afec8f2dbacbc 2023-02-27T2105-v5.1.1-mk4-coldcard-factory.dfu
bcf4284f7733e9de8d4dba238368552b056a27308e466721be7ca624192e257f 2023-02-27T1509-v5.1.0-mk4-coldcard.dfu
cc946bcb63211e15d85db577e25ab2432d4a74d5dad77d710539e505dce7914a 2022-11-14T1854-v4.1.7-coldcard.dfu
010827a60ebfc25b8a6e2bb94cc69b938419957ac6d4a9b6c0b1357c4c6c8632 2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
bc4d0b2b985aea3a78eb9351cdadf60d1ab00801ed1e7192765b94181cb8933b 2022-10-05T1517-v4.1.6-coldcard.dfu
884f373717c9c605920a1dc29e0f890bf7b3cc6b141666814e396094aeedb3f8 2022-07-29T1816-v5.0.6-mk4-coldcard.dfu
3c680195ef49cd0eb86d8e2426443511e8834bcea2d0a86ab52a35cc9365a801 2022-07-20T1508-v5.0.5-mk4-coldcard.dfu
7bd2b98186370f2d895e1e43949694f6ba61a1c021f72a63f0f86a30f338a0fc 2022-05-27T1500-v5.0.4-mk4-coldcard.dfu
5aa2ccc65e2e5279db78b3068b9f3c60c34dd7cc330c2cc1243160db31a2d0f0 2022-05-04T1258-v4.1.5-coldcard.dfu
6dbf0aca0f98fb7bdc761eeead4786617b804dad4afb42ee02febf23d31b5e9b 2022-05-04T1254-v5.0.3-mk3-coldcard.dfu
d5d9bf50892a4aab6e2ffb106a3d206853a60f879daa94a6f90d68a69bf4fa33 2022-05-04T1252-v5.0.3-mk4-coldcard.dfu
9bb028d3e60239f0fcdb3b1f91075785e2c21795789b38c4c619c1f64c2950ef 2022-04-25T1618-v4.1.4-coldcard.dfu
a363b1f0d1b27b8f21dbaac32844a59dacab8c2fee126815cda84c4df31fd7cd 2022-04-19T1805-v5.0.2-mk4-coldcard.dfu
afb6048397af4093e63567563544098e1cfb45b7ca673536253eb6494d60125c 2022-03-24T1645-v5.0.1-mk3-coldcard.dfu
605807bd448711d54e14057892a100bac299a103f5b5fb6466d73f9a36d0694b 2022-03-24T1643-v5.0.1-mk4-coldcard.dfu
badd10c078996516c6464c9bfa5f696747dd7206c97d1e6a75d6f5ee0436619a 2022-03-14T1907-v5.0.0-mk4-coldcard.dfu
dedfcf8385e35dbdbb26b92f8c0667105404062ad83c8830d809cf9193434d9c 2021-09-02T1752-v4.1.3-coldcard.dfu
d01d81305b209dadcf960b9e9d20affb8d4f11e9f9f916c5a06be29298c80dc2 2021-07-28T1347-v4.1.2-coldcard.dfu
08e1ec1fd073afbbc9014db6da07fd96c6b20a6710fe491eb805afeba865fe3f 2021-04-30T1748-v4.1.1-coldcard.dfu
2c39330bef467af8dcd7e2f393a970e1ca177b1812f830269916657ff79598eb 2021-04-29T1725-v4.1.0-coldcard.dfu
5e0c5f4ba9fa0e5fd7f9846e25c6cd28821a86ff5e1207c56cc3a4f4c3741f15 2021-04-07T1424-v4.0.2-coldcard.dfu
f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T1927-v4.0.1-coldcard.dfu
3097fa3c173247637aa27376036e384940adeb67ce727c9795471f46deaa5210 2021-01-14T1617-v3.2.2-coldcard.dfu
9e4aeee48d4399a761fec5d4c65cb2495ef5bc0b46995c085d63a65cf67362cb 2021-01-07T1439-v3.2.1-coldcard.dfu
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
8dd5ff029bb2b08c857604f0c9b5773931f6683ee331ecbc35d9ab4c460b745f 2023-05-12T1316-v6.0.0X-mk4-coldcard-factory.dfu
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmbjJicACgkQo6MbrVoq
WxAnMwf/e2kR1aK6AJiriRa1n3XDomw8ivaUQXUApmK0kawBhVBDLKw5aa3lvTcS
dg80wnenzNdE/QxctL+FkaZzKYsKbFpstkBEbZKcgbHVcinypKJJfICrhIBVVyZw
wdhJMGOLEyWMysqfaYMtYJQPkg5nIn0rRxn4yWXIeXAQLcFgdlWzVykqfGZW1xYr
CcVvxMqufXfc6c5aRFQzBO/YVHiRYzvK1NGDPztJEjXYU3zxnExAZFxk0vgpxvE3
CahKfSSTNv54u4CTLxYCdHPRq9OM6yL/w3OUyUQFklCizk2PjrObsJQW4szbbjlx
r7+587Pc5cpJCZn73Q0Y5/SWgnqm4g==
=/h9F
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmdi2IQACgkQo6MbrVoq
WxBj5Af+PoIWKBXGjtO9OfgPJ7H4gqjs159ql1iF8k20R2BYQRLgspHcJeJP7PNc
rhM0NeGcpbljIUgNOZ7I1ZdyJZtuYQAep6/6rOsDz9aCDNcY+E5d8bUHlWMX2qaw
lUY+FWN+faZ5SdVg/mlKdiP65Ca0KpY5xd8Nptlgl5U9on+5nwnwBw5TTgXiSFIs
LBF/sNvaff+7/LXUmXsBq5v32hwUM4Jj4JNg9/LC5VeG5TGkMwLhCjP4HnVwI+WK
oljtYnHhu2Et7we1wZU2lTH8UsPQR3oADy/YOAJ54SOEKPHzd0y1LUGyLSuZWyka
8q6/Tp4D/5NKJOwqxmY1v9iZ5s6PHg==
=XDmY
-----END PGP SIGNATURE-----

View File

@ -16,7 +16,7 @@ from export import make_json_wallet, make_summary_file, make_descriptor_wallet_e
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet
from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, MAX_TXN_LEN_MK4
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR, MAX_TXN_LEN_MK4
from glob import settings
from pincodes import pa
from menu import start_chooser, MenuSystem, MenuItem
@ -872,6 +872,14 @@ async def start_login_sequence():
# is early in boot process
print("XFP save failed: %s" % exc)
# Version warning before HSM is offered
if version.is_edge and not ckcc.is_simulator():
await ux_show_story(
"This preview version of firmware has not yet been qualified and "
"tested to the same standard as normal Coinkite products."
"\n\nIt is recommended only for developers and early adopters for experimental use. "
"DO NOT use for large Bitcoin amounts.", title="Edge Version")
dis.draw_status(xfp=settings.get('xfp'))
# If HSM policy file is available, offer to start that,
@ -889,6 +897,14 @@ async def start_login_sequence():
await ar.interact()
except: pass
if pa.is_deltamode():
# pretend Secure Notes & Passwords is disabled
# pretend SeedVault is disabled
try:
settings.remove_key("secnap")
settings.master_set("seedvault", False)
except: pass
if version.has_nfc and settings.get('nfc', 0):
# Maybe allow NFC now
import nfc
@ -1014,7 +1030,7 @@ async def export_xpub(label, _2, item):
path = "m"
addr_fmt = AF_CLASSIC
else:
remap = {44:0, 49:1, 84:2}[mode]
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]
@ -1095,7 +1111,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]
allowed_af = [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2TR]
if item.arg:
int_ext, allowed_af, ll, f_pattern = item.arg
addition = " for " + ll
@ -1392,7 +1408,7 @@ async def wipe_filesystem(*A):
if not await ux_confirm('''\
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
used for settings, address search cache, and HSM config file. Does not affect funds, \
or seed words but will reset settings used with other BIP-39 passphrases. \
or seed words but will reset settings used with other temporary seeds & BIP-39 passphrases. \
Does not affect MicroSD card, if any.'''):
return
@ -1731,7 +1747,7 @@ async def bless_flash(*a):
pa.greenlight_firmware()
# redraw our screen
dis.show()
dis.busy_bar(False) # includes dis.show()
def is_psbt(filename):

View File

@ -8,27 +8,17 @@ 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
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from uasyncio import sleep_ms
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from glob import settings
from auth import write_sig_file
from utils import addr_fmt_label, censor_address
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
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):
@ -41,6 +31,7 @@ class KeypathMenu(MenuSystem):
MenuItem("m/44h/⋯", f=self.deeper),
MenuItem("m/49h/⋯", f=self.deeper),
MenuItem("m/84h/⋯", f=self.deeper),
MenuItem("m/86h/⋯", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done),
@ -67,7 +58,7 @@ class KeypathMenu(MenuSystem):
pl = p[0:p.rfind('/')].rfind('/')
else:
self.prefix = p # displayed on mk4 only
pl = len(p)-2
pl = len(p)-2
for mi in items:
mi.arg = mi.label
mi.label = ''+mi.label[pl:]
@ -112,9 +103,8 @@ class PickAddrFmtMenu(MenuSystem):
def __init__(self, path, parent):
self.parent = parent
items = [
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
MenuItem(addr_fmt_label(af), f=self.done, arg=(path, af))
for af in [AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH]
]
super().__init__(items)
if path.startswith("m/84h"):
@ -213,7 +203,11 @@ class AddressListMenu(MenuSystem):
# if they have MS wallets, add those next
for ms in MultisigWallet.iter_wallets():
if not ms.addr_fmt: continue
items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms))
items.append(MenuItem(ms.name, f=self.pick_miniscript, arg=ms))
# if they have miniscript wallets, add those next
for msc in MiniScriptWallet.iter_wallets():
items.append(MenuItem(msc.name, f=self.pick_miniscript, arg=msc))
else:
items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))
@ -245,10 +239,10 @@ class AddressListMenu(MenuSystem):
settings.put('axi', axi) # update last clicked address
await self.show_n_addresses(path, addr_fmt, None)
async def pick_multisig(self, _1, _2, item):
ms_wallet = item.arg
settings.put('axi', item.label) # update last clicked address
await self.show_n_addresses(None, None, ms_wallet)
async def pick_miniscript(self, _1, _2, item):
msc_wallet = item.arg
settings.put('axi', item.label) # update last clicked address
await self.show_n_addresses(None, msc_wallet.addr_fmt, msc_wallet)
async def make_custom(self, *a):
# picking a custom derivation path: makes a tree of menus, with chance
@ -280,7 +274,7 @@ Press (3) if you really understand and accept these risks.
start = self.start
def make_msg(change=0):
def make_msg(change=0, start=start, n=n):
# Build message and CTA about export, plus the actual addresses.
if n:
msg = "Addresses %d%d:\n\n" % (start, min(start + n - 1, MAX_BIP32_IDX))
@ -293,22 +287,7 @@ Press (3) if you really understand and accept these risks.
dis.fullscreen('Wait...')
if ms_wallet:
# IMPORTANT safety feature: never show complete address
# but show enough they can verify addrs shown elsewhere.
# - makes a redeem script
# - converts into addr
# - assumes 0/0 is first address.
for idx, addr, paths, script in ms_wallet.yield_addresses(start, n, change):
addrs.append(censor_address(addr))
if idx == 0 and ms_wallet.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '⋯/%d/%d =>\n' % (change, idx)
msg += truncate_address(addr) + '\n\n'
dis.progress_sofar(idx-start+1, n)
msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change)
else:
# single-signer wallets
from wallet import MasterSingleSigWallet
@ -325,10 +304,9 @@ Press (3) if you really understand and accept these risks.
# export options
k0 = 'to show change addresses' if allow_change and change == 0 else None
export_msg, escape = export_prompt_builder('address summary file',
no_qr=bool(ms_wallet), key0=k0,
force_prompt=True)
key0=k0, force_prompt=True)
if version.has_qwerty:
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN+KEY_QR
else:
escape += "79"
@ -342,8 +320,8 @@ Press (3) if you really understand and accept these risks.
return msg, addrs, escape
msg, addrs, escape = make_msg()
change = 0
msg, addrs, escape = make_msg(change, start)
while 1:
ch = await ux_show_story(msg, escape=escape)
@ -365,14 +343,9 @@ Press (3) if you really understand and accept these risks.
elif choice == KEY_QR:
# switch into a mode that shows them as QR codes
if ms_wallet:
# requires not multisig
continue
from ux import show_qr_codes
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
await show_qr_codes(addrs, is_alnum, start)
continue
elif NFC and (choice == KEY_NFC):
@ -408,7 +381,7 @@ Press (3) if you really understand and accept these risks.
else:
continue # 3 in non-NFC mode
msg, addrs, escape = make_msg(change)
msg, addrs, escape = make_msg(change, start)
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, change=0):
# Produce CSV file contents as a generator
@ -416,28 +389,13 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
from ownership import OWNERSHIP
if ms_wallet:
# For multisig, include redeem script and derivation for each signer
yield '"' + '","'.join(['Index', 'Payment Address', 'Redeem Script']
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
) + '"\n'
if (start == 0) and (n > 100) and change in (0, 1):
saver = OWNERSHIP.saver(ms_wallet, change, start)
else:
saver = None
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
if saver:
saver(addr)
# policy choice: never provide a complete multisig address to user.
addr = censor_address(addr)
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
ln += '"\n'
yield ln
for line in ms_wallet.generate_address_csv(start, n, change):
yield line
if saver:
saver(None) # close file
@ -496,7 +454,7 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
dis.progress_sofar(idx, count or 1)
sig_nice = None
if not ms_wallet:
if not ms_wallet and addr_fmt != AF_P2TR:
derive = path.format(account=account_num, change=change, idx=start) # first addr
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)

View File

@ -8,7 +8,7 @@ from ubinascii import b2a_base64, a2b_base64
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS, AF_P2TR
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
from sffile import SFFile
@ -310,6 +310,10 @@ class ApproveMessageSign(UserAuthorizedAction):
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
self.approved_cb = approved_cb
# temporary - no p2tr support
if self.addr_fmt == AF_P2TR:
raise ValueError("Unsupported address format: 'p2tr'")
from glob import dis
dis.fullscreen('Wait...')
@ -1431,7 +1435,7 @@ class ShowP2SHAddress(ShowAddressBase):
# calculate all the pubkeys involved.
self.subpath_help = ms.validate_script(witdeem_script, xfp_paths=xfp_paths)
self.address = ms.chain.p2sh_address(addr_fmt, witdeem_script)
self.address = chains.current_chain().p2sh_address(addr_fmt, witdeem_script)
def get_msg(self):
return '''\
@ -1447,6 +1451,41 @@ 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))
class ShowMiniscriptAddress(ShowAddressBase):
def setup(self, msc, change, idx):
self.msc = msc
self.change = change
self.idx = idx
d = self.msc.desc.derive(None, change=change).derive(idx)
self.address = chains.current_chain().render_address(d.script_pubkey())
self.addr_fmt = self.msc.addr_fmt
def get_msg(self):
return '''\
{addr}
Wallet:
{name}
Index:
{idx}
Change:
{change}'''.format(addr=self.address, name=self.msc.name, idx=self.idx, change=bool(self.change))
def start_show_miniscript_address(msc, change, index):
UserAuthorizedAction.check_busy(ShowAddressBase)
UserAuthorizedAction.active_request = ShowMiniscriptAddress(msc, change, index)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
# provide the value back to attached desktop
return UserAuthorizedAction.active_request.address
def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script):
# Show P2SH address to user, also returns it.
# - first need to find appropriate multisig wallet associated
@ -1505,40 +1544,77 @@ def usb_show_address(addr_format, subpath):
return active_request.address
class NewEnrollRequest(UserAuthorizedAction):
def __init__(self, ms):
class MiniscriptDeleteRequest(UserAuthorizedAction):
def __init__(self, msc):
super().__init__()
self.wallet = ms
# self.result ... will be re-serialized xpub
self.wallet = msc
async def interact(self):
from multisig import MultisigOutOfSpace
from miniscript import miniscript_delete
await miniscript_delete(self.wallet)
self.done()
def maybe_delete_miniscript(msc):
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = MiniscriptDeleteRequest(msc)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
class NewMiniscriptEnrollRequest(UserAuthorizedAction):
def __init__(self, msc, bsms_index=None):
super().__init__()
self.wallet = msc
self.bsms_index = bsms_index
async def interact(self):
from wallet import WalletOutOfSpace
ms = self.wallet
try:
ch = await ms.confirm_import()
if ch != 'y':
if ch not in ('y'+KEY_ENTER):
# they don't want to!
self.refused = True
await ux_dramatic_pause("Refused.", 2)
except MultisigOutOfSpace:
elif self.bsms_index is not None:
# remove signer round 2 from settings after multisig import is approved by user
from bsms import BSMSSettings
BSMSSettings.signer_delete(self.bsms_index)
except WalletOutOfSpace:
return await self.failure('No space left')
except BaseException as exc:
self.failed = "Exception"
sys.print_exception(exc)
finally:
UserAuthorizedAction.cleanup() # because no results to store
self.pop_menu()
UserAuthorizedAction.cleanup() # because no results to store
if self.bsms_index is not None:
# bsms special case, get him back to multisig menu
from ux import the_ux, restore_menu
from multisig import MultisigMenu
while 1:
top = the_ux.top_of_stack()
if not top: break
if not isinstance(top, MultisigMenu):
the_ux.pop()
continue
break
restore_menu()
else:
self.pop_menu()
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False):
# Offer to import (enroll) a new multisig wallet. Allow reject by user.
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None, miniscript=False):
# Offer to import (enroll) a new multisig/miniscript wallet. Allow reject by user.
from glob import dis
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
UserAuthorizedAction.cleanup()
dis.fullscreen('Wait...') # needed
dis.fullscreen('Wait...')
dis.busy_bar(True)
try:
@ -1560,9 +1636,19 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False):
# this call will raise on parsing errors, so let them rise up
# and be shown on screen/over usb
ms = MultisigWallet.from_file(config, name=name)
if miniscript is None:
# autodetect
try:
msc = MiniScriptWallet.from_file(config, name=name)
except AssertionError:
msc = MultisigWallet.from_file(config, name=name)
UserAuthorizedAction.active_request = NewEnrollRequest(ms)
elif miniscript:
msc = MiniScriptWallet.from_file(config, name=name)
else:
msc = MultisigWallet.from_file(config, name=name)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(msc, bsms_index=bsms_index)
if ux_reset:
# for USB case, and import from PSBT
@ -1573,9 +1659,9 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False):
from ux import the_ux
the_ux.push(UserAuthorizedAction.active_request)
finally:
# always finish busy bar
dis.busy_bar(False)
class FirmwareUpgradeRequest(UserAuthorizedAction):
def __init__(self, hdr, length, hdr_check=False, psram_offset=None):
super().__init__()

View File

@ -280,7 +280,7 @@ async def restore_tmp_from_dict_ll(vals):
if not k[:8] == "setting.":
continue
key = k[8:]
if key in ["multisig"]:
if key in ["multisig", "miniscript"]:
# whitelist
settings.set(k, v)

1092
shared/bsms.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,8 @@ from ubinascii import hexlify as b2a_hex
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
from serializations import hash160, ser_compact_size, disassemble
from public_constants import TAPROOT_LEAF_TAPSCRIPT, TAPROOT_LEAF_MASK
from serializations import hash160, ser_compact_size, disassemble, ser_string
from ucollections import namedtuple
from opcodes import OP_RETURN, OP_1, OP_16
@ -26,6 +27,27 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
# - also electrum source: electrum/lib/constants.py
def taptweak(internal_key, tweak=None):
# BIP 341 states: "If the spending conditions do not require a script path,
# the output key should commit to an unspendable script path instead of having no script path.
# This can be achieved by computing the output key point as:
# Q = P + int(hashTapTweak(bytes(P)))G."
actual_tweak = internal_key if tweak is None else internal_key + tweak
tweak = ngu.secp256k1.tagged_sha256(b"TapTweak", actual_tweak)
xo_pubkey = ngu.secp256k1.xonly_pubkey(internal_key)
xo_pubkey_tweaked = xo_pubkey.tweak_add(tweak)
return xo_pubkey_tweaked.to_bytes()
def tapscript_serialize(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
# leaf version is only 7 msb
lv = leaf_version % TAPROOT_LEAF_MASK
return bytes([lv]) + ser_string(script)
def tapleaf_hash(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
return ngu.secp256k1.tagged_sha256(b"TapLeaf",
tapscript_serialize(script, leaf_version))
class ChainsBase:
curve = 'secp256k1'
@ -110,23 +132,30 @@ class ChainsBase:
# - works only with single-key addresses
assert not addr_fmt & AFC_SCRIPT
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
if addr_fmt == AF_P2TR:
assert len(pubkey) == 32 # internal
script = b'\x51\x20' + taptweak(pubkey)
else:
raise ValueError('bad address template: %s' % addr_fmt)
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
else:
raise ValueError('bad address template: %s' % addr_fmt)
return cls.render_address(script)
@classmethod
def address(cls, node, addr_fmt):
# return a human-readable, properly formatted address
if addr_fmt == AF_P2TR:
xo_pk = node.pubkey()[1:]
return ngu.codecs.segwit_encode(cls.bech32_hrp, 1, taptweak(xo_pk))
if addr_fmt == AF_CLASSIC:
# olde fashioned P2PKH
@ -299,6 +328,7 @@ class BitcoinMain(ChainsBase):
AF_P2WPKH: Slip132Version(0x04b24746, 0x04b2430c, 'z'),
AF_P2WSH_P2SH: Slip132Version(0x0295b43f, 0x0295b005, 'Y'),
AF_P2WSH: Slip132Version(0x02aa7ed3, 0x02aa7a99, 'Z'),
AF_P2TR: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
}
bech32_hrp = 'bc'
@ -320,6 +350,7 @@ class BitcoinTestnet(BitcoinMain):
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
}
bech32_hrp = 'tb'
@ -342,6 +373,7 @@ class BitcoinRegtest(BitcoinMain):
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
}
bech32_hrp = 'bcrt'
@ -376,6 +408,13 @@ def current_chain():
return get_chain(chain)
def current_key_chain():
c = current_chain()
if c == BitcoinRegtest:
# regtest has same extended keys as testnet
c = BitcoinTestnet
return c
# Overbuilt: will only be testnet and mainchain.
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
@ -403,6 +442,8 @@ CommonDerivations = [
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
( 'BIP-84 (Native Segwit P2WPKH)', "m/84h/{coin_type}h/{account}h/{change}/{idx}",
AF_P2WPKH ), # generates bc1 bech32 addresses
('BIP-86 (Taproot Segwit P2TR)', "m/86h/{coin_type}h/{account}h/{change}/{idx}",
AF_P2TR), # generates bc1p bech32m addresses
]

View File

@ -194,11 +194,6 @@ def decode_short_text(got):
# was something else.
pass
# multisig descriptor
# multi( catches both multi( and sortedmulti(
if ("multi(" in got):
return 'multi', (got,)
if ("\n" in got) and ('pub' in got):
# legacy multisig import/export format
# [0-9a-fA-F]{8}\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]{107}
@ -214,6 +209,10 @@ def decode_short_text(got):
if c > 1:
return 'multi', (got,)
from descriptor import Descriptor
if Descriptor.is_descriptor(got):
return 'minisc', (got,)
# Things with newlines in them are not URL's
# - working URLs are not >4k
# - might be a story in text, etc.

558
shared/desc_utils.py Normal file
View File

@ -0,0 +1,558 @@
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Copyright (c) 2020 Stepan Snigirev MIT License embit/arguments.py
#
import ngu, chains, ustruct
from io import BytesIO
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_CLASSIC, AF_P2TR
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from utils import keypath_to_str, str_to_keypath, swab32, xfp2str
from serializations import ser_compact_size
WILDCARD = "*"
PROVABLY_UNSPENDABLE = b'\x02P\x92\x9bt\xc1\xa0IT\xb7\x8bK`5\xe9z^\x07\x8aZ\x0f(\xec\x96\xd5G\xbf\xee\x9a\xce\x80:\xc0'
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def polymod(c, val):
c0 = c >> 35
c = ((c & 0x7ffffffff) << 5) ^ val
if (c0 & 1):
c ^= 0xf5dee51989
if (c0 & 2):
c ^= 0xa9fdca3312
if (c0 & 4):
c ^= 0x1bab10e32d
if (c0 & 8):
c ^= 0x3706b1677a
if (c0 & 16):
c ^= 0x644d626ffd
return c
def descriptor_checksum(desc):
c = 1
cls = 0
clscount = 0
for ch in desc:
pos = INPUT_CHARSET.find(ch)
if pos == -1:
raise ValueError(ch)
c = polymod(c, pos & 31)
cls = cls * 3 + (pos >> 5)
clscount += 1
if clscount == 3:
c = polymod(c, cls)
cls = 0
clscount = 0
if clscount > 0:
c = polymod(c, cls)
for j in range(0, 8):
c = polymod(c, 0)
c ^= 1
rv = ''
for j in range(0, 8):
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
return rv
def append_checksum(desc):
return desc + "#" + descriptor_checksum(desc)
def parse_desc_str(string):
"""Remove comments, empty lines and strip line. Produce single line string"""
res = ""
for l in string.split("\n"):
strip_l = l.strip()
if not strip_l:
continue
if strip_l.startswith("#"):
continue
res += strip_l
return res
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
if addr_fmt == AF_P2WSH_P2SH:
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
elif addr_fmt == AF_P2WSH:
descriptor_template = "wsh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2SH:
descriptor_template = "sh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2TR:
# provably unspendable BIP-0341
descriptor_template = "tr(" + b2a_hex(PROVABLY_UNSPENDABLE[1:]).decode() + ",sortedmulti_a(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
def read_until(s, chars=b",)(#"):
# TODO potential infinite loop
# what is the longest possible element? (proly some raw( but that is unsupported)
#
res = b""
chunk = b""
char = None
while True:
chunk = s.read(1)
if len(chunk) == 0:
return res, None
if chunk in chars:
return res, chunk
res += chunk
return res, None
class KeyOriginInfo:
def __init__(self, fingerprint: bytes, derivation: list):
self.fingerprint = fingerprint
self.derivation = derivation
self.cc_fp = swab32(int(b2a_hex(self.fingerprint).decode(), 16))
def __eq__(self, other):
return self.psbt_derivation() == other.psbt_derivation()
def __hash__(self):
return hash(tuple(self.psbt_derivation()))
def str_derivation(self):
return keypath_to_str(self.derivation, prefix='m/', skip=0)
def psbt_derivation(self):
res = [self.cc_fp]
for i in self.derivation:
res.append(i)
return res
@classmethod
def from_string(cls, s: str):
arr = s.split("/")
xfp = a2b_hex(arr[0])
assert len(xfp) == 4
arr[0] = "m"
path = "/".join(arr)
derivation = str_to_keypath(xfp, path)[1:] # ignoring xfp here, already stored
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"))
class KeyDerivationInfo:
def __init__(self, indexes=None):
self.indexes = indexes
if self.indexes is None:
self.indexes = [[0, 1], WILDCARD]
self.multi_path_index = 0
else:
self.multi_path_index = None
@property
def is_int_ext(self):
if self.multi_path_index is not None:
return True
return False
@property
def is_external(self):
if self.is_int_ext:
return True
elif self.indexes[-2] % 2 == 0:
return True
return False
@property
def branches(self):
if self.is_int_ext:
return self.indexes[self.multi_path_index]
else:
return [self.indexes[-2]]
@classmethod
def from_string(cls, s):
fail_msg = "Cannot use hardened sub derivation path"
if not s:
return cls()
res = []
mp = 0
mpi = None
for idx, i in enumerate(s.split("/")):
start_i = i.find("<")
if start_i != -1:
end_i = s.find(">")
assert end_i
inner = s[start_i+1:end_i]
assert ";" in inner
inner_split = inner.split(";")
assert len(inner_split) == 2, "wrong multipath"
res.append([int(i) for i in inner_split])
mp += 1
mpi = idx
else:
if i == WILDCARD:
res.append(WILDCARD)
else:
assert "'" not in i, fail_msg
assert "h" not in i, fail_msg
res.append(int(i))
# only one <x;y> allowed in subderivation
assert mp <= 1, "too many multipaths (%d)" % mp
if res == [0, WILDCARD]:
obj = cls()
else:
assert len(res) == 2, "Key derivation too long"
assert res[-1] == WILDCARD, "All keys must be ranged"
obj = cls(res)
obj.multi_path_index = mpi
return obj
def to_string(self, external=True, internal=True):
res = []
for i in self.indexes:
if isinstance(i, list):
if internal is True and external is False:
i = str(i[1])
elif internal is False and external is True:
i = str(i[0])
else:
i = "<%d;%d>" % (i[0], i[1])
else:
i = str(i)
res.append(i)
return "/".join(res)
def to_int_list(self, branch_idx, idx):
assert branch_idx in self.indexes[0]
return [branch_idx, idx]
class Key:
def __init__(self, node, origin, derivation=None, taproot=False, chain_type=None):
self.origin = origin
self.node = node
self.derivation = derivation
self.taproot = taproot
self.chain_type = chain_type
def __eq__(self, other):
return self.origin == other.origin \
and self.derivation.indexes == other.derivation.indexes
def __hash__(self):
return hash(self.to_string())
def __len__(self):
return 34 - int(self.taproot) # <33:sec> or <32:xonly>
@property
def fingerprint(self):
return self.origin.fingerprint
def serialize(self):
return self.key_bytes()
def compile(self):
d = self.serialize()
return ser_compact_size(len(d)) + d
@classmethod
def parse(cls, s):
first = s.read(1)
origin = None
if first == b"u":
s.seek(-1, 1)
return Unspend.parse(s)
if first == b"[":
prefix, char = read_until(s, b"]")
if char != b"]":
raise ValueError("Invalid key - missing ] in key origin info")
origin = KeyOriginInfo.from_string(prefix.decode())
else:
s.seek(-1, 1)
k, char = read_until(s, b",)/")
der = b""
if char == b"/":
der, char = read_until(s, b"<,)")
if char == b"<":
der += b"<"
branch, char = read_until(s, b">")
if char is None:
raise ValueError("Failed reading the key, missing >")
der += branch + b">"
rest, char = read_until(s, b",)")
der += rest
if char is not None:
s.seek(-1, 1)
# parse key
node, chain_type = cls.parse_key(k)
der = KeyDerivationInfo.from_string(der.decode())
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)
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)
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
if i in self.derivation.indexes[mp_i]:
idx = i
break
else:
assert False
elif idx is None:
# derive according to key subderivation if any
if self.derivation is None:
idx = 1 if change else 0
else:
if self.derivation.multi_path_index is not None:
ext, inter = self.derivation.indexes[self.derivation.multi_path_index]
idx = inter if change else ext
new_node = self.node.copy()
new_node.derive(idx, False)
if self.origin:
origin = KeyOriginInfo(self.origin.fingerprint, self.origin.derivation + [idx])
else:
fp = ustruct.pack('<I', swab32(self.node.my_fp()))
origin = KeyOriginInfo(fp, [idx])
derivation = KeyDerivationInfo(self.derivation.indexes[1:])
return type(self)(new_node, origin, derivation, taproot=self.taproot)
@classmethod
def read_from(cls, s, taproot=False):
return cls.parse(s)
@classmethod
def from_cc_data(cls, xfp, deriv, xpub):
koi = KeyOriginInfo.from_string("%s/%s" % (xfp2str(xfp), deriv.replace("m/", "")))
node = ngu.hdnode.HDNode()
node.deserialize(xpub)
return cls(node, koi, KeyDerivationInfo())
def to_cc_data(self):
ch = chains.current_chain()
return (self.origin.cc_fp,
self.origin.str_derivation(),
ch.serialize_public(self.node, AF_CLASSIC))
@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:
return "[%s]" % self.origin
return ""
def key_bytes(self):
kb = self.node
if not isinstance(kb, bytes):
kb = self.node.pubkey()
if self.taproot:
if len(kb) == 33:
kb = kb[1:]
assert len(kb) == 32
return kb
def extended_public_key(self):
return chains.current_chain().serialize_public(self.node)
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()
return key
@classmethod
def from_string(cls, s):
s = BytesIO(s.encode())
return cls.parse(s)
class Unspend(Key):
def __init__(self, node, origin=None, derivation=None, taproot=True, chain_type=None):
super().__init__(node, origin, derivation, taproot, chain_type)
assert self.taproot
def __eq__(self, other):
return self.node.chain_code() == other.node.chain_code() \
and self.node.pubkey() == other.node.pubkey() \
and self.derivation.indexes == other.derivation.indexes
@classmethod
def parse(cls, s):
assert s.read(8) == b"unspend("
chain_code, c = read_until(s, b")")
chain_code = a2b_hex(chain_code)
assert len(chain_code) == 32, "chain code length"
assert c
char = s.read(1)
if char != b"/":
raise ValueError("ranged unspend required")
der, char = read_until(s, b"<,)")
if char == b"<":
der += b"<"
branch, char = read_until(s, b">")
if char is None:
raise ValueError("Failed reading the key, missing >")
der += branch + b">"
rest, char = read_until(s, b",)")
der += rest
if char is not None:
s.seek(-1, 1)
node = ngu.hdnode.HDNode().from_chaincode_pubkey(chain_code,
PROVABLY_UNSPENDABLE)
der = KeyDerivationInfo.from_string(der.decode())
return cls(node, None, der, chain_type=None)
def to_string(self, external=True, internal=True, subderiv=True):
res = "unspend(%s)" % b2a_hex(self.node.chain_code()).decode()
if self.derivation and subderiv:
res += "/" + self.derivation.to_string(external, internal)
return res
@property
def is_provably_unspendable(self):
return True
def fill_policy(policy, keys, external=True, internal=True):
orig_keys = []
for k in keys:
if not isinstance(k, str):
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 k_orig not in orig_keys:
orig_keys.append(k_orig)
for i in range(len(orig_keys) - 1, -1, -1):
k = orig_keys[i]
ph = "@%d" % i
ph_len = len(ph)
while True:
ix = policy.find(ph)
if ix == -1:
break
assert policy[ix+ph_len] == "/"
# subderivation is part of the policy
x = ix + ph_len
substr = policy[x:x+26] # 26 is the longest possible subderivation allowed "/<2147483647;2147483646>/*"
mp_start = substr.find("<")
assert mp_start != -1
mp_end = substr.find(">")
mp = substr[mp_start:mp_end + 1]
_ext, _int = mp[1:-1].split(";")
if external and not internal:
sub = _ext
elif internal and not external:
sub = _int
else:
sub = None
if sub is not None:
policy = policy[:x + mp_start] + sub + policy[x + mp_end + 1:]
x = policy[ix:ix + ph_len]
assert x == ph
policy = policy[:ix] + k + policy[ix + ph_len:]
return policy
def taproot_tree_helper(scripts):
from miniscript import Miniscript
if isinstance(scripts, Miniscript):
script = scripts.compile()
assert isinstance(script, bytes)
h = ngu.secp256k1.tagged_sha256(b"TapLeaf", chains.tapscript_serialize(script))
return [(chains.TAPROOT_LEAF_TAPSCRIPT, script, bytes())], h
if len(scripts) == 1:
return taproot_tree_helper(scripts[0])
split_pos = len(scripts) // 2
left, left_h = taproot_tree_helper(scripts[0:split_pos])
right, right_h = taproot_tree_helper(scripts[split_pos:])
left = [(version, script, control + right_h) for version, script, control in left]
right = [(version, script, control + left_h) for version, script, control in right]
if right_h < left_h:
right_h, left_h = left_h, right_h
h = ngu.secp256k1.tagged_sha256(b"TapBranch", left_h + right_h)
return left + right, h

View File

@ -1,256 +1,461 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# descriptor.py - Bitcoin Core's descriptors and their specialized checksums.
# Copyright (c) 2020 Stepan Snigirev MIT License embit/descriptor.py
#
# Based on: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
#
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
import ngu, chains
from io import BytesIO
from collections import OrderedDict
from binascii import hexlify as b2a_hex
from utils import cleanup_deriv_path, check_xpub, xfp2str, swab32
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from public_constants import AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, MAX_SIGNERS, MAX_TR_SIGNERS
from desc_utils import parse_desc_str, append_checksum, descriptor_checksum, Key
from desc_utils import taproot_tree_helper, fill_policy, Unspend
from miniscript import Miniscript
MULTI_FMT_TO_SCRIPT = {
AF_P2SH: "sh(%s)",
AF_P2WSH_P2SH: "sh(wsh(%s))",
AF_P2WSH: "wsh(%s)",
None: "wsh(%s)",
# hack for tests
"p2sh": "sh(%s)",
"p2sh-p2wsh": "sh(wsh(%s))",
"p2wsh-p2sh": "sh(wsh(%s))",
"p2wsh": "wsh(%s)",
}
SINGLE_FMT_TO_SCRIPT = {
AF_P2WPKH: "wpkh(%s)",
AF_CLASSIC: "pkh(%s)",
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
None: "wpkh(%s)",
"p2pkh": "pkh(%s)",
"p2wpkh": "wpkh(%s)",
"p2sh-p2wpkh": "sh(wpkh(%s))",
"p2wpkh-p2sh": "sh(wpkh(%s))",
}
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
try:
from utils import xfp2str, str2xfp
except ModuleNotFoundError:
import struct
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
# assuming not micro python
def xfp2str(xfp):
# Standardized way to show an xpub's fingerprint... it's a 4-byte string
# and not really an integer. Used to show as '0x%08x' but that's wrong endian.
return b2a_hex(struct.pack('<I', xfp)).decode().upper()
def str2xfp(txt):
# Inverse of xfp2str
return struct.unpack('<I', a2b_hex(txt))[0]
class DescriptorException(ValueError):
pass
class WrongCheckSumError(Exception):
pass
def polymod(c, val):
c0 = c >> 35
c = ((c & 0x7ffffffff) << 5) ^ val
if (c0 & 1):
c ^= 0xf5dee51989
if (c0 & 2):
c ^= 0xa9fdca3312
if (c0 & 4):
c ^= 0x1bab10e32d
if (c0 & 8):
c ^= 0x3706b1677a
if (c0 & 16):
c ^= 0x644d626ffd
class Tapscript:
def __init__(self, tree=None, keys=None, policy=None):
self.tree = tree
self.keys = keys
self.policy = policy
self._merkle_root = None
return c
@staticmethod
def iter_leaves(tree):
if isinstance(tree, Miniscript):
yield tree
else:
assert isinstance(tree, list)
for lv in tree:
yield from Tapscript.iter_leaves(lv)
def descriptor_checksum(desc):
c = 1
cls = 0
clscount = 0
for ch in desc:
pos = INPUT_CHARSET.find(ch)
if pos == -1:
raise ValueError(ch)
@property
def merkle_root(self):
if not self._merkle_root:
self.process_tree()
return self._merkle_root
c = polymod(c, pos & 31)
cls = cls * 3 + (pos >> 5)
clscount += 1
if clscount == 3:
c = polymod(c, cls)
cls = 0
clscount = 0
@staticmethod
def _derive(tree, idx, key_map, change=False):
if isinstance(tree, Miniscript):
return tree.derive(idx, key_map, change=change)
else:
if len(tree) == 1 and isinstance(tree[0], Miniscript):
return tree[0].derive(idx, key_map, change=change)
l, r = tree
return [Tapscript._derive(l, idx, key_map, change=change),
Tapscript._derive(r, idx, key_map, change=change)]
if clscount > 0:
c = polymod(c, cls)
for j in range(0, 8):
c = polymod(c, 0)
c ^= 1
def derive(self, idx=None, change=False):
derived_keys = OrderedDict()
for k in self.keys:
derived_keys[k] = k.derive(idx, change=change)
tree = Tapscript._derive(self.tree, idx, derived_keys, change=change)
return type(self)(tree, policy=self.policy, keys=list(derived_keys.values()))
rv = ''
for j in range(0, 8):
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
def process_tree(self):
info, mr = taproot_tree_helper(self.tree)
self._merkle_root = mr
return info, mr
return rv
@classmethod
def read_from(cls, s):
num_leafs = 0
depth = 0
tapscript = []
p0 = s.read(1)
if p0 != b"{":
# depth zero
s.seek(-1, 1)
alone = Miniscript.read_from(s, taproot=True)
alone.is_sane(taproot=True)
alone.verify()
tapscript.append(alone)
num_leafs += 1
else:
assert p0 == b"{"
depth += 1
itmp = None
itmp_p = None
while True:
p1 = s.read(1)
if p1 == b'':
break
elif p1 == b")":
s.seek(-1, 1)
break
elif p1 == b",":
continue
elif p1 == b"{":
if itmp is None:
itmp = []
else:
if itmp_p:
itmp[itmp_p].append([])
else:
itmp.append(([]))
itmp_p = -1
def append_checksum(desc):
return desc + "#" + descriptor_checksum(desc)
depth += 1
continue
elif p1 == b"}":
depth -= 1
if depth == 1:
tapscript.append(itmp)
itmp = None
if depth <= 2:
itmp_p = None
continue
def parse_desc_str(string):
"""Remove comments, empty lines and strip line. Produce single line string"""
res = ""
for l in string.split("\n"):
strip_l = l.strip()
if not strip_l:
continue
if strip_l.startswith("#"):
continue
res += strip_l
return res
s.seek(-1, 1)
item = Miniscript.read_from(s, taproot=True)
item.is_sane(taproot=True)
item.verify()
num_leafs += 1
if itmp is None:
tapscript.append(item)
else:
if itmp_p and depth == 4:
itmp[itmp_p][itmp_p].append(item)
elif itmp_p:
itmp[itmp_p].append(item)
else:
itmp.append(item)
assert num_leafs <= 8, "num_leafs > 8"
ts = cls(tapscript)
ts.parse_policy()
return ts
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
if addr_fmt == AF_P2WSH_P2SH:
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
elif addr_fmt == AF_P2WSH:
descriptor_template = "wsh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2SH:
descriptor_template = "sh(sortedmulti(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
def parse_policy(self):
self.policy, self.keys = self._parse_policy(self.tree, [])
orig_keys = OrderedDict()
for k in self.keys:
if k.origin not in orig_keys:
orig_keys[k.origin] = []
orig_keys[k.origin].append(k)
for i, k_lst in enumerate(orig_keys.values()):
# always keep subderivation in policy string
self.policy = self.policy.replace(k_lst[0].to_string(subderiv=False), chr(64) + str(i))
@staticmethod
def _parse_policy(tree, all_keys):
if isinstance(tree, Miniscript):
keys, leaf_str = tree.keys, tree.to_string()
for k in keys:
if k not in all_keys:
all_keys.append(k)
return leaf_str, all_keys
else:
assert isinstance(tree, list)
if len(tree) == 1 and isinstance(tree[0], Miniscript):
keys, leaf_str = tree[0].keys, tree[0].to_string()
for k in keys:
if k not in all_keys:
all_keys.append(k)
return leaf_str, all_keys
else:
l, r = tree
ll, all_keys = Tapscript._parse_policy(l, all_keys)
rr, all_keys = Tapscript._parse_policy(r, all_keys)
return "{" + ll + "," + rr + "}", all_keys
@staticmethod
def script_tree(tree):
if isinstance(tree, Miniscript):
return b2a_hex(chains.tapscript_serialize(tree.compile())).decode()
else:
assert isinstance(tree, list)
if len(tree) == 1 and isinstance(tree[0], Miniscript):
return b2a_hex(chains.tapscript_serialize(tree[0].compile())).decode()
else:
l, r = tree
ll = Tapscript.script_tree(l)
rr = Tapscript.script_tree(r)
return "{" + ll + "," + rr + "}"
def to_string(self, external=True, internal=True):
return fill_policy(self.policy, self.keys, external, internal)
class Descriptor:
__slots__ = (
"keys",
"addr_fmt",
)
def __init__(self, miniscript=None, sh=False, wsh=True, key=None, wpkh=True,
taproot=False, tapscript=None):
if key is None and miniscript is None:
raise DescriptorException("Provide either miniscript or a key")
def __init__(self, keys, addr_fmt):
self.keys = keys
self.addr_fmt = addr_fmt
self.sh = sh
self.wsh = wsh
self.key = key
self.miniscript = miniscript
self.wpkh = wpkh
self.taproot = taproot
self.tapscript = tapscript
@staticmethod
def checksum_check(desc_w_checksum , csum_required=False):
try:
desc, checksum = desc_w_checksum.split("#")
except ValueError:
if csum_required:
raise ValueError("Missing descriptor checksum")
return desc_w_checksum, None
calc_checksum = descriptor_checksum(desc)
if calc_checksum != checksum:
raise WrongCheckSumError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
return desc, checksum
if taproot:
if self.key:
self.key.taproot = True
for k in self.keys:
k.taproot = taproot
@staticmethod
def parse_key_orig_info(key):
# key origin info is required for our MultisigWallet
close_index = key.find("]")
if key[0] != "[" or close_index == -1:
raise ValueError("Key origin info is required for %s" % (key))
key_orig_info = key[1:close_index] # remove brackets
key = key[close_index + 1:]
assert "/" in key_orig_info, "Malformed key derivation info"
return key_orig_info, key
@staticmethod
def parse_key_derivation_info(key):
invalid_subderiv_msg = "Invalid subderivation path - only 0/* or <0;1>/* allowed"
slash_split = key.split("/")
assert len(slash_split) > 1, invalid_subderiv_msg
if all(["h" not in elem and "'" not in elem for elem in slash_split[1:]]):
assert slash_split[-1] == "*", invalid_subderiv_msg
assert slash_split[-2] in ["0", "<0;1>", "<1;0>"], invalid_subderiv_msg
assert len(slash_split[1:]) == 2, invalid_subderiv_msg
return slash_split[0]
else:
raise ValueError("Cannot use hardened sub derivation path")
def checksum(self):
return descriptor_checksum(self._serialize())
def serialize_keys(self, internal=False, int_ext=False):
result = []
for xfp, deriv, xpub in self.keys:
if deriv[0] == "m":
# get rid of 'm'
deriv = deriv[1:]
elif deriv[0] != "/":
# input "84'/0'/0'" would lack slash separtor with xfp
deriv = "/" + deriv
if not isinstance(xfp, str):
xfp = xfp2str(xfp)
koi = xfp + deriv
# normalize xpub to use h for hardened instead of '
key_str = "[%s]%s" % (koi.lower(), xpub)
if int_ext:
key_str = key_str + "/" + "<0;1>" + "/" + "*"
def validate(self):
from glob import settings
if self.miniscript:
if self.is_basic_multisig:
assert len(self.keys) <= MAX_SIGNERS
else:
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
result.append(key_str.replace("'", "h"))
return result
assert len(self.keys) <= 20
self.miniscript.verify()
if self.miniscript.type != "B":
raise DescriptorException("Top level miniscript should be 'B'")
def _serialize(self, internal=False, int_ext=False):
"""Serialize without checksum"""
assert len(self.keys) == 1 # "Multiple keys for single signature script"
desc_base = SINGLE_FMT_TO_SCRIPT[self.addr_fmt]
inner = self.serialize_keys(internal=internal, int_ext=int_ext)[0]
return desc_base % (inner)
has_mine = 0
my_xfp = settings.get('xfp')
to_check = self.keys.copy()
if self.tapscript:
assert len(self.keys) <= MAX_TR_SIGNERS
assert self.key # internal key (would fail during parse)
if not self.key.is_provably_unspendable:
to_check += [self.key]
else:
assert self.key is None and self.miniscript, "not miniscript"
def serialize(self, internal=False, int_ext=False):
"""Serialize with checksum"""
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
c = chains.current_key_chain().ctype
for k in to_check:
assert k.chain_type == c, "wrong chain"
xfp = k.origin.cc_fp
deriv = k.origin.str_derivation()
xpub = k.extended_public_key()
deriv = cleanup_deriv_path(deriv)
is_mine, _ = check_xpub(xfp, xpub, deriv, c, my_xfp, False)
if is_mine:
has_mine += 1
@classmethod
def parse(cls, desc_w_checksum):
# remove garbage
desc_w_checksum = parse_desc_str(desc_w_checksum)
# check correct checksum
desc, checksum = cls.checksum_check(desc_w_checksum)
# legacy
if desc.startswith("pkh("):
addr_fmt = AF_CLASSIC
tmp_desc = desc.replace("pkh(", "")
tmp_desc = tmp_desc.rstrip(")")
assert has_mine != 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
# native segwit
elif desc.startswith("wpkh("):
addr_fmt = AF_P2WPKH
tmp_desc = desc.replace("wpkh(", "")
tmp_desc = tmp_desc.rstrip(")")
def storage_policy(self):
if self.tapscript:
return self.tapscript.policy
# wrapped segwit
elif desc.startswith("sh(wpkh("):
addr_fmt = AF_P2WPKH_P2SH
tmp_desc = desc.replace("sh(wpkh(", "")
tmp_desc = tmp_desc.rstrip("))")
s = self.miniscript.to_string()
orig_keys = OrderedDict()
for k in self.keys:
if k.origin not in orig_keys:
orig_keys[k.origin] = []
orig_keys[k.origin].append(k)
for i, k_lst in enumerate(orig_keys.values()):
s = s.replace(k_lst[0].to_string(subderiv=False), chr(64) + str(i))
return s
def ux_policy(self):
if self.tapscript:
return "Taproot tree keys:\n\n" + self.tapscript.policy
return self.storage_policy()
@property
def script_len(self):
if self.taproot:
return 34 # OP_1 <32:xonly>
if self.miniscript:
return len(self.miniscript)
if self.wpkh:
return 22 # 00 <20:pkh>
return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG
def xfp_paths(self):
res = []
if self.taproot:
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())])
for k in self.keys:
if k.origin:
res.append(k.origin.psbt_derivation())
return res
@property
def is_wrapped(self):
return self.sh and self.is_segwit
@property
def is_legacy(self):
return not (self.is_segwit or self.is_taproot)
@property
def is_segwit(self):
return (self.wsh and self.miniscript) or (self.wpkh and self.key) or self.taproot
@property
def is_pkh(self):
return self.key is not None and not self.taproot
@property
def is_taproot(self):
return self.taproot
@property
def is_basic_multisig(self):
return self.miniscript and self.miniscript.NAME in ["multi", "sortedmulti"]
@property
def is_sortedmulti(self):
return self.is_basic_multisig and self.miniscript.NAME == "sortedmulti"
@property
def keys(self):
if self.tapscript:
return self.tapscript.keys
elif self.key:
return [self.key]
return self.miniscript.keys
@property
def addr_fmt(self):
if self.sh and not self.wsh:
af = AF_P2SH
elif self.wsh and not self.sh:
af = AF_P2WSH
elif self.sh and self.wsh:
af = AF_P2WSH_P2SH
elif self.taproot:
af = AF_P2TR
elif self.sh and self.wpkh:
af = AF_P2WPKH_P2SH
elif self.wpkh and not self.sh:
af = AF_P2WPKH
else:
af = AF_CLASSIC
return af
def set_from_addr_fmt(self, addr_fmt):
self.taproot = False
self.wsh = False
self.wpkh = False
self.sh = False
if addr_fmt == AF_P2TR:
self.taproot = True
assert self.key
elif addr_fmt == AF_P2WPKH:
self.wpkh = True
self.miniscript = None
assert self.key
elif addr_fmt == AF_P2WPKH_P2SH:
self.wpkh = True
self.sh = True
self.miniscript = None
assert self.key
elif addr_fmt == AF_P2SH:
self.sh = True
assert self.miniscript
assert not self.key
elif addr_fmt == AF_P2WSH:
self.wsh = True
assert self.miniscript
assert not self.key
elif addr_fmt == AF_P2WSH_P2SH:
self.wsh = True
self.sh = True
assert self.miniscript
assert not self.key
else:
# AF_CLASSIC
assert self.key
assert not self.miniscript
def scriptpubkey_type(self):
if self.is_taproot:
return "p2tr"
if self.sh:
return "p2sh"
if self.is_pkh:
if self.is_legacy:
return "p2pkh"
if self.is_segwit:
return "p2wpkh"
else:
return "p2wsh"
def derive(self, idx=None, change=False):
if self.taproot:
return type(self)(
None,
self.sh,
self.wsh,
self.key.derive(idx, change=change),
self.wpkh,
self.taproot,
tapscript=self.tapscript.derive(idx, change=change),
)
if self.miniscript:
return type(self)(
self.miniscript.derive(idx, change=change),
self.sh,
self.wsh,
None,
self.wpkh,
self.taproot,
tapscript=None,
)
else:
return type(self)(
None, self.sh, self.wsh,
self.key.derive(idx, change=change),
self.wpkh, self.taproot, tapscript=None
)
def witness_script(self):
if self.wsh and self.miniscript is not None:
return self.miniscript.compile()
def redeem_script(self):
if not self.sh:
return None
if self.miniscript:
if self.wsh:
return b"\x00\x20" + ngu.hash.sha256s(self.miniscript.compile())
else:
return self.miniscript.compile()
else:
raise ValueError("Unsupported descriptor. Supported: pkh(), wpkh(), sh(wpkh()).")
return b"\x00\x14" + ngu.hash.hash160(self.key.node.pubkey())
koi, key = cls.parse_key_orig_info(tmp_desc)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
def script_pubkey(self):
if self.taproot:
tweak = None
if self.tapscript:
tweak = self.tapscript.merkle_root
output_pubkey = chains.taptweak(self.key.serialize(), tweak)
return b"\x51\x20" + output_pubkey
if self.sh:
return b"\xa9\x14" + ngu.hash.hash160(self.redeem_script()) + b"\x87"
if self.wsh:
return b"\x00\x20" + ngu.hash.sha256s(self.witness_script())
if self.miniscript:
return self.miniscript.compile()
if self.wpkh:
return b"\x00\x14" + ngu.hash.hash160(self.key.serialize())
return b"\x76\xa9\x14" + ngu.hash.hash160(self.key.serialize()) + b"\x88\xac"
@classmethod
def is_descriptor(cls, desc_str):
# Quick method to guess whether this is a descriptor
"""Quick method to guess whether this is a descriptor"""
try:
temp = parse_desc_str(desc_str)
except:
@ -267,142 +472,142 @@ class Descriptor:
return True
return False
def bitcoin_core_serialize(self, external_label=None):
@staticmethod
def checksum_check(desc_w_checksum, csum_required=False):
try:
desc, checksum = desc_w_checksum.split("#")
except ValueError:
if csum_required:
raise ValueError("Missing descriptor checksum")
return desc_w_checksum, None
calc_checksum = descriptor_checksum(desc)
if calc_checksum != checksum:
raise WrongCheckSumError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
return desc, checksum
@classmethod
def from_string(cls, desc, checksum=False):
desc = parse_desc_str(desc)
desc, cs = cls.checksum_check(desc)
s = BytesIO(desc.encode())
res = cls.read_from(s)
left = s.read()
if len(left) > 0:
raise ValueError("Unexpected characters after descriptor: %r" % left)
if checksum:
if cs is None:
_, cs = res.to_string().split("#")
return res, cs
return res
@classmethod
def read_from(cls, s, taproot=False):
start = s.read(8)
sh = False
wsh = False
wpkh = False
is_miniscript = True
internal_key = None
tapscript = None
if start.startswith(b"tr("):
is_miniscript = False # miniscript vs. tapscript (that can contain miniscripts in tree)
taproot = True
s.seek(-5, 1)
internal_key = Key.parse(s) # internal key is a must - also handles unspend(
internal_key.taproot = True
sep = s.read(1)
if sep == b")":
s.seek(-1, 1)
else:
assert sep == b","
tapscript = Tapscript.read_from(s)
elif start.startswith(b"sh(wsh("):
sh = True
wsh = True
s.seek(-1, 1)
elif start.startswith(b"wsh("):
sh = False
wsh = True
s.seek(-4, 1)
elif start.startswith(b"sh(wpkh("):
is_miniscript = False
sh = True
wpkh = True
elif start.startswith(b"wpkh("):
is_miniscript = False
wpkh = True
s.seek(-3, 1)
elif start.startswith(b"pkh("):
is_miniscript = False
s.seek(-4, 1)
elif start.startswith(b"sh("):
sh = True
wsh = False
s.seek(-5, 1)
else:
raise ValueError("Invalid descriptor")
if is_miniscript:
miniscript = Miniscript.read_from(s)
miniscript.is_sane(taproot=False)
key = internal_key
nbrackets = int(sh) + int(wsh)
elif taproot:
miniscript = None
key = internal_key
nbrackets = 1
else:
miniscript = None
key = Key.parse(s)
nbrackets = 1 + int(sh)
end = s.read(nbrackets)
if end != b")" * nbrackets:
raise ValueError("Invalid descriptor")
o = cls(miniscript, sh=sh, wsh=wsh, key=key, wpkh=wpkh,
taproot=taproot, tapscript=tapscript)
o.validate()
return o
def to_string(self, external=True, internal=True, checksum=True):
if self.taproot:
desc = "tr(%s" % self.key.to_string(external, internal)
if self.tapscript:
desc += ","
tree = self.tapscript.to_string(external, internal)
desc += tree
desc = desc + ")"
return append_checksum(desc)
if self.miniscript is not None:
res = self.miniscript.to_string(external, internal)
if self.wsh:
res = "wsh(%s)" % res
else:
if self.wpkh:
res = "wpkh(%s)" % self.key.to_string(external, internal)
else:
res = "pkh(%s)" % self.key.to_string(external, internal)
if self.sh:
res = "sh(%s)" % res
if checksum:
res = append_checksum(res)
return res
def bitcoin_core_serialize(self):
# this will become legacy one day
# instead use <0;1> descriptor format
res = []
for internal in [False, True]:
for external in (True, False):
desc_obj = {
"desc": self.serialize(internal=internal),
"desc": self.to_string(external, not external),
"active": True,
"timestamp": "now",
"internal": internal,
"internal": not external,
"range": [0, 100],
}
if internal is False and external_label:
desc_obj["label"] = external_label
res.append(desc_obj)
return res
class MultisigDescriptor(Descriptor):
# only supprt with key derivation info
# only xpubs
# can be extended when needed
__slots__ = (
"M",
"N",
"keys",
"addr_fmt",
"is_sorted" # whether to use sortedmulti() or multi()
)
def __init__(self, M, N, keys, addr_fmt, is_sorted=True):
self.M = M
self.N = N
self.is_sorted = is_sorted
super().__init__(keys, addr_fmt)
@classmethod
def parse(cls, desc_w_checksum):
# remove garbage
desc_w_checksum = parse_desc_str(desc_w_checksum)
# check correct checksum
desc, checksum = cls.checksum_check(desc_w_checksum)
is_sorted = "sortedmulti(" in desc
rplc = "sortedmulti(" if is_sorted else "multi("
# wrapped segwit
if desc.startswith("sh(wsh("+rplc):
addr_fmt = AF_P2WSH_P2SH
tmp_desc = desc.replace("sh(wsh("+rplc, "")
tmp_desc = tmp_desc.rstrip(")))")
# native segwit
elif desc.startswith("wsh("+rplc):
addr_fmt = AF_P2WSH
tmp_desc = desc.replace("wsh("+rplc, "")
tmp_desc = tmp_desc.rstrip("))")
# legacy
elif desc.startswith("sh("+rplc):
addr_fmt = AF_P2SH
tmp_desc = desc.replace("sh("+rplc, "")
tmp_desc = tmp_desc.rstrip("))")
else:
raise ValueError("Unsupported descriptor. Supported: sh(), sh(wsh()), wsh().")
splitted = tmp_desc.split(",")
M, keys = int(splitted[0]), splitted[1:]
N = int(len(keys))
if M > N:
raise ValueError("M must be <= N: got M=%d and N=%d" % (M, N))
res_keys = []
for key in keys:
koi, key = cls.parse_key_orig_info(key)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
res_keys.append((xfp, origin_deriv, xpub))
return cls(M=M, N=N, keys=res_keys, addr_fmt=addr_fmt, is_sorted=is_sorted)
def _serialize(self, internal=False, int_ext=False):
"""Serialize without checksum"""
desc_base = MULTI_FMT_TO_SCRIPT[self.addr_fmt]
_type = "sortedmulti" if self.is_sorted else "multi"
_type += "(%s)"
desc_base = desc_base % _type
assert len(self.keys) == self.N
inner = str(self.M) + "," + ",".join(
self.serialize_keys(internal=internal, int_ext=int_ext))
return desc_base % (inner)
def pretty_serialize(self):
"""Serialize in pretty and human-readable format"""
_type = "sortedmulti" if self.is_sorted else "multi"
res = "# Coldcard descriptor export\n"
if self.is_sorted:
res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n"
else:
res += ("# !!! DANGER: order of keys in descriptor MUST be preserved. "
"Correct order of keys is required to compose valid redeem/witness script.\n")
if self.addr_fmt == AF_P2SH:
res += "# bare multisig - p2sh\n"
res += "sh("+_type+"(\n%s\n))"
# native segwit
elif self.addr_fmt == AF_P2WSH:
res += "# native segwit - p2wsh\n"
res += "wsh("+_type+"(\n%s\n))"
# wrapped segwit
elif self.addr_fmt == AF_P2WSH_P2SH:
res += "# wrapped segwit - p2sh-p2wsh\n"
res += "sh(wsh(" + _type + "(\n%s\n)))"
else:
raise ValueError("Malformed descriptor")
assert len(self.keys) == self.N
inner = "\t" + "# %d of %d (%s)\n" % (
self.M, self.N,
"requires all participants to sign" if self.M == self.N else "threshold")
inner += "\t" + str(self.M) + ",\n"
ser_keys = self.serialize_keys()
for i, key_str in enumerate(ser_keys, start=1):
if i == self.N:
inner += "\t" + key_str
else:
inner += "\t" + key_str + ",\n"
checksum = self.serialize().split("#")[1]
return (res % inner) + "#" + checksum
# EOF

View File

@ -4,7 +4,7 @@
#
import machine, uzlib, ckcc, utime
from ssd1306 import SSD1306_SPI
from version import is_devmode
from version import is_devmode, is_edge
import framebuf
from graphics_mk4 import Graphics
@ -146,6 +146,12 @@ class Display:
self.text(-2, 21, 'D', font=FontTiny, invert=1)
self.text(-2, 28, 'E', font=FontTiny, invert=1)
self.text(-2, 35, 'V', font=FontTiny, invert=1)
elif is_edge:
self.dis.fill_rect(128 - 6, 19, 5, 26, 1)
self.text(-2, 20, 'E', font=FontTiny, invert=1)
self.text(-2, 27, 'D', font=FontTiny, invert=1)
self.text(-2, 33, 'G', font=FontTiny, invert=1)
self.text(-2, 39, 'E', font=FontTiny, invert=1)
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".

View File

@ -56,32 +56,32 @@ still backed-up.''')
def bip85_derive(picked, index):
# implement the core step of BIP85 from our master secret
path = "m/83696968h/"
if picked in (0,1,2):
# BIP-39 seed phrases (we only support English)
num_words = stash.SEED_LEN_OPTS[picked]
width = (16, 24, 32)[picked] # of bytes
path = "m/83696968h/39h/0h/{num_words}h/{index}h".format(num_words=num_words, index=index)
path += "39h/0h/%dh/%dh" % (num_words, index)
s_mode = 'words'
elif picked == 3:
# HDSeed for Bitcoin Core: but really a WIF of a private key, can be used anywhere
# HDSeed for Bitcoin Core: but really a WIF of a private key
s_mode = 'wif'
path = "m/83696968h/2h/{index}h".format(index=index)
path += "2h/%dh" % index
width = 32
elif picked == 4:
# New XPRV
path = "m/83696968h/32h/{index}h".format(index=index)
path += "32h/%dh" % index
s_mode = 'xprv'
width = 64
elif picked in (5, 6):
width = 32 if picked == 5 else 64
path = "m/83696968h/128169h/{width}h/{index}h".format(width=width, index=index)
path += "128169h/%dh/%dh" % (width, index)
s_mode = 'hex'
elif picked == 7:
width = 64
# hardcoded width for now
# b"pwd".hex() --> 707764
path = "m/83696968h/707764h/{pwd_len}h/{index}h".format(pwd_len=BIP85_PWD_LEN, index=index)
path += "707764h/%dh/%dh" % (BIP85_PWD_LEN, index)
s_mode = 'pw'
else:
raise ValueError(picked)

View File

@ -9,7 +9,7 @@ from utils import xfp2str, swab32, chunk_writer
from ux import ux_show_story
from glob import settings
from auth import write_sig_file
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from ownership import OWNERSHIP
@ -103,7 +103,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 != AF_CLASSIC and (addr_fmt in chain.slip132):
if show_slip132 and 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)))
@ -121,7 +121,8 @@ be needed for different systems.
yield ('\n\n')
from multisig import MultisigWallet
if MultisigWallet.exists():
exists, exists_other_chain = MultisigWallet.exists()
if exists:
yield '\n# Your Multisig Wallets\n\n'
for ms in MultisigWallet.get_all():
@ -133,14 +134,15 @@ be needed for different systems.
yield fp.getvalue()
del fp
async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
async def write_text_file(fname_pattern, body, title, derive, addr_fmt,
force_prompt=False):
# Export data as a text file.
from glob import dis, NFC
from files import CardSlot, CardMissingError, needs_microsd
from ux import import_export_prompt
choice = await import_export_prompt("%s file" % title, is_import=False,
no_qr=(not version.has_qwerty))
force_prompt=force_prompt) # QR offered also on Mk4
if choice == KEY_CANCEL:
return
elif choice == KEY_QR:
@ -160,8 +162,10 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
with open(fname, 'wb') as fd:
chunk_writer(fd, body)
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
sig_nice = None
if addr_fmt != AF_P2TR:
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
except CardMissingError:
await needs_microsd()
@ -170,8 +174,9 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
await ux_show_story('Failed to write!\n\n\n'+str(e))
return
msg = '%s file written:\n\n%s\n\n%s signature file written:\n\n%s' % (title, nice, title,
sig_nice)
msg = '%s file written:\n\n%s' % (title, nice)
if sig_nice:
msg += '\n\n%s signature file written:\n\n%s' % (title, sig_nice)
await ux_show_story(msg)
async def make_summary_file(fname_pattern='public.txt'):
@ -195,10 +200,11 @@ async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.tx
# make the data
examples = []
imp_multi, imp_desc = generate_bitcoin_core_wallet(account_num, examples)
imp_multi, imp_desc, imp_desc_tr = generate_bitcoin_core_wallet(account_num, examples)
imp_multi = ujson.dumps(imp_multi)
imp_desc = ujson.dumps(imp_desc)
imp_desc_tr = ujson.dumps(imp_desc_tr)
body = '''\
# Bitcoin Core Wallet Import File
@ -214,7 +220,10 @@ Wallet operates on blockchain: {nb}
The following command can be entered after opening Window -> Console
in Bitcoin Core, or using bitcoin-cli:
importdescriptors '{imp_desc}'
p2wpkh:
importdescriptors '{imp_desc}'
p2tr:
importdescriptors '{imp_desc_tr}'
> **NOTE** If your UTXO was created before generating `importdescriptors` command, you should adjust the value of `timestamp` before executing command in bitcoin core.
By default it is set to `now` meaning do not rescan the blockchain. If approximate time of UTXO creation is known - adjust `timestamp` from `now` to UNIX epoch time.
@ -229,13 +238,15 @@ importmulti '{imp_multi}'
## Resulting Addresses (first 3)
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, xfp=xfp, nb=chains.current_chain().name)
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, imp_desc_tr=imp_desc_tr,
xfp=xfp, nb=chains.current_chain().name)
body += '\n'.join('%s => %s' % t for t in examples)
body += '\n'
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
ch = chains.current_chain()
derive = "84h/{coin_type}h/{account}h".format(account=account_num, coin_type=ch.b44_cointype)
@ -244,44 +255,65 @@ importmulti '{imp_multi}'
def generate_bitcoin_core_wallet(account_num, example_addrs):
# Generate the data for an RPC command to import keys into Bitcoin Core
# - yields dicts for json purposes
from descriptor import Descriptor
from descriptor import Descriptor, Key
chain = chains.current_chain()
derive = "84h/{coin_type}h/{account}h".format(account=account_num,
coin_type=chain.b44_cointype)
derive_v0 = "84h/{coin_type}h/{account}h".format(
account=account_num, coin_type=chain.b44_cointype
)
derive_v1 = "86h/{coin_type}h/{account}h".format(
account=account_num, coin_type=chain.b44_cointype
)
with stash.SensitiveValues() as sv:
prefix = sv.derive_path(derive)
xpub = chain.serialize_public(prefix)
prefix = sv.derive_path(derive_v0)
xpub_v0 = chain.serialize_public(prefix)
for i in range(3):
sp = '0/%d' % i
node = sv.derive_path(sp, master=prefix)
a = chain.address(node, AF_P2WPKH)
example_addrs.append( ('m/%s/%s' % (derive, sp), a) )
example_addrs.append(('m/%s/%s' % (derive_v0, sp), a))
with stash.SensitiveValues() as sv:
prefix = sv.derive_path(derive_v1)
xpub_v1 = chain.serialize_public(prefix)
for i in range(3):
sp = '0/%d' % i
node = sv.derive_path(sp, master=prefix)
a = chain.address(node, AF_P2TR)
example_addrs.append(('m/%s/%s' % (derive_v1, sp), a))
xfp = settings.get('xfp')
_, vers, _ = version.get_mpy_version()
key0 = Key.from_cc_data(xfp, derive_v0, xpub_v0)
desc_v0 = Descriptor(key=key0)
desc_v0.set_from_addr_fmt(AF_P2WPKH)
key1 = Key.from_cc_data(xfp, derive_v1, xpub_v1)
desc_v1 = Descriptor(key=key1)
desc_v1.set_from_addr_fmt(AF_P2TR)
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
desc_obj = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=AF_P2WPKH)
# for importmulti
imm_list = [
{
'desc': desc_obj.serialize(internal=internal),
'desc': desc_v0.to_string(external, internal),
'range': [0, 1000],
'timestamp': 'now',
'internal': internal,
'keypool': True,
'watchonly': True
}
for internal in [False, True]
for external, internal in [(True, False), (False, True)]
]
# for importdescriptors
imd_list = desc_obj.bitcoin_core_serialize()
return imm_list, imd_list
imd_list = desc_v0.bitcoin_core_serialize()
imd_list_v1 = desc_v1.bitcoin_core_serialize()
return imm_list, imd_list, imd_list_v1
def generate_wasabi_wallet():
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
@ -347,7 +379,8 @@ def generate_unchained_export(account_num=0):
def generate_generic_export(account_num=0):
# Generate data that other programers will use to import Coldcard (single-signer)
from descriptor import Descriptor, multisig_descriptor_template
from descriptor import Descriptor, Key
from desc_utils import multisig_descriptor_template
chain = chains.current_chain()
master_xfp = settings.get("xfp")
@ -361,12 +394,14 @@ def generate_generic_export(account_num=0):
with stash.SensitiveValues() as sv:
# each of these paths would have /{change}/{idx} in usage (not hardened)
for name, deriv, fmt, atype, is_ms in [
( 'bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False ),
( 'bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh"
( 'bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False ),
( 'bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ),
( 'bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True ),
( 'bip45', "m/45h", AF_P2SH, 'p2sh', True ),
('bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False),
('bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False), # was "p2wpkh-p2sh"
('bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False),
('bip86', "m/86h/{ct}h/{acc}h", AF_P2TR, 'p2tr', False),
('bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True),
('bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True),
('bip48_3', "m/48h/{ct}h/{acc}h/3h", AF_P2TR, 'p2tr', True),
('bip45', "m/45h", AF_P2SH, 'p2sh', True),
]:
if fmt == AF_P2SH and account_num:
continue
@ -375,11 +410,14 @@ def generate_generic_export(account_num=0):
node = sv.derive_path(dd)
xfp = xfp2str(swab32(node.my_fp()))
xp = chain.serialize_public(node, AF_CLASSIC)
zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None
zp = chain.serialize_public(node, fmt) if fmt not in (AF_CLASSIC, AF_P2TR) else None
if is_ms:
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
else:
desc = Descriptor(keys=[(master_xfp, dd, xp)], addr_fmt=fmt).serialize(int_ext=True)
key = Key.from_cc_data(master_xfp, dd, xp)
desc_obj = Descriptor(key=key)
desc_obj.set_from_addr_fmt(fmt)
desc = desc_obj.to_string()
OWNERSHIP.note_wallet_used(fmt, account_num)
@ -505,7 +543,7 @@ async def make_json_wallet(label, func, fname_pattern='new-wallet.json'):
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
fname_pattern="descriptor.txt"):
from descriptor import Descriptor
from descriptor import Descriptor, Key
from glob import dis
dis.fullscreen('Generating...')
@ -520,34 +558,41 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
elif addr_type == AF_P2TR:
mode = 86
else:
raise ValueError(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num)
derive = "m/{mode}h/{coin_type}h/{account}h".format(mode=mode,
account=account_num, coin_type=chain.b44_cointype)
derive = "m/{mode}h/{coin_type}h/{account}h".format(
mode=mode, account=account_num, coin_type=chain.b44_cointype
)
dis.progress_bar_show(0.2)
with stash.SensitiveValues() as sv:
dis.progress_bar_show(0.3)
xpub = chain.serialize_public(sv.derive_path(derive))
dis.progress_bar_show(0.7)
desc = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=addr_type)
key = Key.from_cc_data(xfp, derive, xpub)
desc = Descriptor(key=key)
desc.set_from_addr_fmt(addr_type)
dis.progress_bar_show(0.8)
if int_ext:
# with <0;1> notation
body = desc.serialize(int_ext=True)
body = desc.to_string()
else:
# external descriptor
# internal descriptor
body = "%s\n%s" % (
desc.serialize(internal=False),
desc.serialize(internal=True),
desc.to_string(internal=False),
desc.to_string(external=False),
)
dis.progress_bar_show(1)
await write_text_file(fname_pattern, body, "Descriptor", derive + "/0/0", addr_type)
await write_text_file(fname_pattern, body, "Descriptor", derive + "/0/0",
addr_type, force_prompt=True)
# EOF

View File

@ -10,6 +10,7 @@ from actions import *
from choosers import *
from mk4 import dev_enable_repl
from multisig import make_multisig_menu, import_multisig_nfc
from miniscript import make_miniscript_menu
from seed import make_ephemeral_seed_menu, make_seed_vault_menu, start_b39_pw
from address_explorer import address_explore
from drv_entro import drv_entro_start, password_entry
@ -138,6 +139,8 @@ SettingsMenu = [
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
NonDefaultMenuItem('Multisig Wallets', 'multisig',
menu=make_multisig_menu, predicate=has_secrets),
NonDefaultMenuItem('Miniscript', 'miniscript',
menu=make_miniscript_menu, predicate=has_secrets),
NonDefaultMenuItem('NFC Push Tx', 'ptxurl', menu=pushtx_setup_menu),
MenuItem('Display Units', chooser=value_resolution_chooser),
MenuItem('Max Network Fee', chooser=max_fee_chooser),
@ -156,7 +159,7 @@ which take apart the flash chips of the SDCard may still be able to find the \
data or filenames.'''),
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
story='''When enabled, allows scrolling past menu top/bottom \
(wrap around). By default, this is only happens in very large menus.'''),
(wrap around). By default, this only happens in very large menus.'''),
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
story=('Forces display of XFP (seed fingerprint) '
'at top of main menu. Normally, XFP is shown only when '
@ -176,6 +179,7 @@ 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("Master XPUB", f=export_xpub, arg=0),
MenuItem("Current XFP", f=export_xpub, arg=-1),
@ -291,7 +295,7 @@ DangerZoneMenu = [
"WARNING: Seed Vault is encrypted (AES-256-CTR) by your seed,"
" but not held directly inside secure elements. Backups are required"
" after any change to vault! Recommended for experiments or temporary use."),
predicate=has_se_secrets),
predicate=has_real_secret),
MenuItem('Perform Selftest', f=start_selftest), # little harmful
MenuItem("Set High-Water", f=set_highwater),
MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available),
@ -352,7 +356,7 @@ AdvancedNormalMenu = [
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem("File Management", menu=FileMgmtMenu),
NonDefaultMenuItem('Secure Notes & Passwords', 'notes', menu=make_notes_menu,
NonDefaultMenuItem('Secure Notes & Passwords', 'secnap', menu=make_notes_menu,
predicate=version.has_qwerty),
MenuItem('Derive Seed B85' if not version.has_qwerty else 'Derive Seeds (BIP-85)',
f=drv_entro_start),
@ -424,7 +428,7 @@ NormalSystem = [
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
predicate=lambda: version.has_qwerty and (settings.get("notes", False) != False)),
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
MenuItem('Type Passwords', f=password_entry, shortcut='t',
predicate=lambda: settings.get("emu", False) and has_secrets()),
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',

View File

@ -4,16 +4,15 @@
#
# Unattended signing of transactions and messages, subject to a set of rules.
#
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu, version
from sffile import SFFile
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
from pincodes import AE_LONG_SECRET_LEN
from stash import blank_object
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
from public_constants import MAX_USERNAME_LEN
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from ucollections import OrderedDict
from files import CardSlot, CardMissingError
@ -88,13 +87,13 @@ def pop_list(j, fld_name, cleanup_fcn=None):
else:
return []
def pop_deriv_list(j, fld_name, extra_val=None):
def pop_deriv_list(j, fld_name, extra_vals=None):
# expect a list of derivation paths, but also 'any' meaning accept all
# - maybe also 'p2sh' as special value
# - also, path can have n
def cu(s):
if s.lower() == 'any': return s.lower()
if extra_val and s.lower() == extra_val: return s.lower()
if extra_vals and s.lower() in extra_vals:
return s.lower()
try:
return cleanup_deriv_path(s, allow_star=True)
except:
@ -195,7 +194,7 @@ class ApprovalRule:
# - users: list of authorized users
# - min_users: how many of those are needed to approve
# - local_conf: local user must also confirm w/ code
# - wallet: which multisig wallet to restrict to, or '1' for single signer only
# - wallet: which multisig/miniscript wallet to restrict to, or '1' for single signer only
# - min_pct_self_transfer: minimum percentage of own input value that must go back to self
# - patterns: list of transaction patterns to check for. Valid values:
# * EQ_NUM_INS_OUTS: the number of inputs and outputs must be equal
@ -212,6 +211,7 @@ class ApprovalRule:
return u
self.index = idx+1
self.ms_type = "multisig"
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
self.users = pop_list(j, 'users', check_user)
@ -238,8 +238,11 @@ class ApprovalRule:
# if specified, 'wallet' must be an existing multisig wallet's name
if self.wallet and self.wallet != '1':
names = [ms.name for ms in MultisigWallet.get_all()]
assert self.wallet in names, "unknown MS wallet: "+self.wallet
ms_names = [ms.name for ms in MultisigWallet.get_all()]
msc_names = [msc.name for msc in MiniScriptWallet.get_all()]
assert self.wallet in (ms_names+msc_names), "unknown wallet: "+self.wallet
if self.wallet in msc_names:
self.ms_type = "miniscript"
# patterns must be valid
for p in self.patterns:
@ -283,9 +286,9 @@ class ApprovalRule:
rv = 'Any amount'
if self.wallet == '1':
rv += ' (non multisig)'
rv += ' (singlesig only)'
elif self.wallet:
rv += ' from multisig wallet "%s"' % self.wallet
rv += ' from %s wallet "%s"' % (self.ms_type, self.wallet)
if self.users:
rv += ' may be authorized by '
@ -328,10 +331,12 @@ class ApprovalRule:
# rule limited to one wallet
if psbt.active_multisig:
# if multisig signing, might need to match specific wallet name
assert self.wallet == psbt.active_multisig.name, 'wrong wallet'
assert self.wallet == psbt.active_multisig.name, 'wrong multisig wallet'
elif psbt.active_miniscript:
assert self.wallet == psbt.active_miniscript.name, 'wrong miniscript wallet'
else:
# non multisig, but does this rule apply to all wallets or single-singers
assert self.wallet == '1', 'not multisig'
assert self.wallet == '1', 'singlesig only'
if self.max_amount is not None:
assert total_out <= self.max_amount, 'amount exceeded'
@ -504,9 +509,9 @@ class HSMPolicy:
self.warnings_ok = pop_bool(j, 'warnings_ok')
# a list of paths we can accept for signing
self.msg_paths = pop_deriv_list(j, 'msg_paths')
self.share_xpubs = pop_deriv_list(j, 'share_xpubs')
self.share_addrs = pop_deriv_list(j, 'share_addrs', 'p2sh')
self.msg_paths = pop_deriv_list(j, 'msg_paths', ['any'])
self.share_xpubs = pop_deriv_list(j, 'share_xpubs', ['any'])
self.share_addrs = pop_deriv_list(j, 'share_addrs', ['p2sh', 'any', 'msas'])
# free text shown at top
self.notes = pop_string(j, 'notes', 1, 80)
@ -814,12 +819,15 @@ class HSMPolicy:
return match_deriv_path(self.share_xpubs, subpath)
def approve_address_share(self, subpath=None, is_p2sh=False):
def approve_address_share(self, subpath=None, is_p2sh=False, miniscript=False):
# Are we allowing "show address" requests over USB?
if not self.share_addrs:
return False
if miniscript:
return ('msas' in self.share_addrs)
if is_p2sh:
return ('p2sh' in self.share_addrs)
@ -894,6 +902,7 @@ class HSMPolicy:
# reject anything with warning, probably
if psbt.warnings:
print(psbt.warnings)
if self.warnings_ok:
log.info("Txn has warnings, but policy is to accept anyway.")
else:
@ -994,7 +1003,8 @@ def hsm_status_report():
rv['approval_wait'] = True
rv['users'] = Users.list()
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()]
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()] \
+ [msc.name for msc in MiniScriptWallet.get_all()]
rv['chain'] = settings.get('chain', 'BTC')

View File

@ -6,12 +6,14 @@ freeze_as_mpy('', [
'address_explorer.py',
'auth.py',
'backups.py',
'bsms.py',
'callgate.py',
'chains.py',
'choosers.py',
'compat7z.py',
'countdowns.py',
'descriptor.py',
'desc_utils.py',
'dev_helper.py',
'display.py',
'drv_entro.py',
@ -26,6 +28,7 @@ freeze_as_mpy('', [
'login.py',
'main.py',
'menu.py',
'miniscript.py',
'multisig.py',
'numpad.py',
'nvstore.py',

1906
shared/miniscript.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,19 +3,23 @@
# multisig.py - support code for multisig signing and p2sh in general.
#
import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson, version
from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str, to_ascii_printable
from utils import str_to_keypath, problem_file_line, parse_extended_key, get_filesize
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 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
from descriptor import MultisigDescriptor, multisig_descriptor_template
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS
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 opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue
from glob import settings
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from wallet import WalletABC, MAX_BIP32_IDX
from serializations import disassemble
from wallet import BaseStorageWallet, MAX_BIP32_IDX
# PSBT Xpub trust policies
TRUST_VERIFY = const(0)
@ -23,14 +27,11 @@ TRUST_OFFER = const(1)
TRUST_PSBT = const(2)
class MultisigOutOfSpace(RuntimeError):
pass
def disassemble_multisig_mn(redeem_script):
# pull out just M and N from script. Simple, faster, no memory.
assert MAX_SIGNERS == 15
assert redeem_script[-1] == OP_CHECKMULTISIG, 'need CHECKMULTISIG'
if redeem_script[-1] != OP_CHECKMULTISIG:
return None, None
M = redeem_script[0] - 80
N = redeem_script[-2] - 80
@ -42,9 +43,7 @@ def disassemble_multisig(redeem_script):
# - only for multisig scripts, not general purpose
# - expect OP_1 (pk1) (pk2) (pk3) OP_3 OP_CHECKMULTISIG for 1 of 3 case
# - returns M, N, (list of pubkeys)
# - for very unlikely/impossible asserts, dont document reason; otherwise do.
from serializations import disassemble
# - for very unlikely/impossible asserts, don't document reason; otherwise do.
M, N = disassemble_multisig_mn(redeem_script)
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
assert len(redeem_script) == 1 + (N * 34) + 1 + 1, 'bad len'
@ -107,7 +106,7 @@ def make_redeem_script(M, nodes, subkey_idx, bip67=True):
return b''.join(pubkeys)
class MultisigWallet(WalletABC):
class MultisigWallet(BaseStorageWallet):
# Capture the info we need to store long-term in order to participate in a
# multisig wallet as a co-signer.
# - can be saved to nvram
@ -122,19 +121,20 @@ class MultisigWallet(WalletABC):
(AF_P2SH, 'p2sh'),
(AF_P2WSH, 'p2wsh'),
(AF_P2WSH_P2SH, 'p2sh-p2wsh'), # preferred
(AF_P2TR, 'p2tr'),
(AF_P2WSH_P2SH, 'p2wsh-p2sh'), # obsolete (now an alias)
]
# optional: user can short-circuit many checks (system wide, one power-cycle only)
disable_checks = False
key_name = "multisig"
def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, chain_type='BTC', bip67=True):
self.storage_idx = -1
def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, chain_type=None, bip67=True):
super().__init__(chain_type=chain_type)
self.name = name
assert len(m_of_n) == 2
self.M, self.N = m_of_n
self.chain_type = chain_type or 'BTC'
assert len(xpubs[0]) == 3
self.xpubs = xpubs # list of (xfp(int), deriv, xpub(str))
self.addr_fmt = addr_fmt # address format for wallet
@ -163,17 +163,13 @@ class MultisigWallet(WalletABC):
deriv = derivs[0]
return deriv + '/%d/%d' % (change_idx, idx)
@property
def chain(self):
return chains.get_chain(self.chain_type)
@classmethod
def get_trust_policy(cls):
which = settings.get('pms', None)
exists, _ = cls.exists()
if which is None:
which = TRUST_VERIFY if cls.exists() else TRUST_OFFER
which = TRUST_VERIFY if exists else TRUST_OFFER
return which
@ -239,14 +235,29 @@ class MultisigWallet(WalletABC):
return rv
@classmethod
def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmt=None):
def is_correct_chain(cls, o, curr_chain):
# for newer versions, last element can be bip67 marker
d = o[-1] if isinstance(o[-1], dict) else o[-2]
if "ch" not in d:
# mainnet
ch = "BTC"
else:
ch = d["ch"]
if ch == curr_chain.ctype:
return True
return False
@classmethod
def iter_wallets(cls, M=None, N=None, addr_fmt=None):
# yield MS wallets we know about, that match at least right M,N if known.
# - this is only place we should be searching this list, please!!
lst = settings.get('multisig', [])
lst = settings.get(cls.key_name, [])
c = chains.current_key_chain()
for idx, rec in enumerate(lst):
if idx == not_idx:
# ignore one by index
if not cls.is_correct_chain(rec, c):
continue
if M or N:
@ -343,57 +354,6 @@ class MultisigWallet(WalletABC):
return False
@classmethod
def get_all(cls):
# return them all, as a generator
return cls.iter_wallets()
@classmethod
def exists(cls):
# are there any wallets defined?
return bool(settings.get('multisig', False))
@classmethod
def get_by_idx(cls, nth):
# instance from index number (used in menu)
lst = settings.get('multisig', [])
try:
obj = lst[nth]
except IndexError:
return None
return cls.deserialize(obj, nth)
def commit(self):
# data to save
# - important that this fails immediately when nvram overflows
obj = self.serialize()
v = settings.get('multisig', [])
orig = v.copy()
if not v or self.storage_idx == -1:
# create
self.storage_idx = len(v)
v.append(obj)
else:
# update in place
v[self.storage_idx] = obj
settings.set('multisig', v)
# save now, rather than in background, so we can recover
# from out-of-space situation
try:
settings.save()
except:
# back out change; no longer sure of NVRAM state
try:
settings.set('multisig', orig)
settings.save()
except: pass # give up on recovery
raise MultisigOutOfSpace
def has_similar(self):
# check if we already have a saved duplicate to this proposed wallet
# - return (name_change, diff_items, count_similar) where:
@ -454,12 +414,12 @@ class MultisigWallet(WalletABC):
else:
raise IndexError # consistency bug
lst = settings.get('multisig', [])
lst = settings.get(self.key_name, [])
del lst[self.storage_idx]
if lst:
settings.set('multisig', lst)
settings.set(self.key_name, lst)
else:
settings.remove_key('multisig')
settings.remove_key(self.key_name)
settings.save()
self.storage_idx = -1
@ -472,7 +432,7 @@ class MultisigWallet(WalletABC):
def yield_addresses(self, start_idx, count, change_idx=0):
# Assuming a suffix of /0/0 on the defined prefix's, yield
# possible deposit addresses for this wallet.
ch = self.chain
ch = chains.current_chain()
assert self.addr_fmt, 'no addr fmt known'
@ -501,6 +461,35 @@ class MultisigWallet(WalletABC):
idx += 1
count -= 1
def make_addresses_msg(self, msg, start, n, change=0):
from glob import dis
addrs = []
for idx, addr, paths, script in self.yield_addresses(start, n, change):
if idx == 0 and self.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '.../%d/%d =>\n' % (change, idx)
addrs.append(addr)
msg += truncate_address(addr) + '\n\n'
dis.progress_sofar(idx - start + 1, n)
return msg, addrs
def generate_address_csv(self, start, n, change):
yield '"' + '","'.join(['Index', 'Payment Address',
'Redeem Script (%d of %d)' % (self.M, self.N)]
+ (['Derivation'] * self.N)) + '"\n'
for (idx, addr, derivs, script) in self.yield_addresses(start, n, change_idx=change):
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
ln += '"\n'
yield ln
def validate_script(self, redeem_script, subpaths=None, xfp_paths=None):
# Check we can generate all pubkeys in the redeem script, raise on errors.
# - working from pubkeys in the script, because duplicate XFP can happen
@ -572,7 +561,7 @@ class MultisigWallet(WalletABC):
found_pk = node.pubkey()
# Document path(s) used. Not sure this is useful info to user tho.
# - Do not show what we can't verify: we don't really know the hardeneded
# - Do not show what we can't verify: we don't really know the hardened
# part of the path from fingerprint to here.
here = '[%s]' % xfp2str(xfp)
if dp != len(path):
@ -683,7 +672,9 @@ class MultisigWallet(WalletABC):
continue
# deserialize, update list and lots of checks
is_mine = cls.check_xpub(xfp, value, deriv, chains.current_chain().ctype, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, value, deriv, chains.current_key_chain().ctype,
my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
@ -696,21 +687,35 @@ class MultisigWallet(WalletABC):
my_xfp = settings.get('xfp')
xpubs = []
desc = MultisigDescriptor.parse(descriptor)
for xfp, deriv, xpub in desc.keys:
descriptor = Descriptor.from_string(descriptor)
assert descriptor.is_basic_multisig, "not multisig" # raises
addr_fmt = descriptor.addr_fmt
M, N = descriptor.miniscript.m_n()
for key in descriptor.miniscript.keys:
assert key.derivation.is_external, "Invalid subderivation path - only 0/* or <0;1>/* allowed"
xfp = key.origin.cc_fp
deriv = key.origin.str_derivation()
xpub = key.extended_public_key()
deriv = cleanup_deriv_path(deriv)
is_mine = cls.check_xpub(xfp, xpub, deriv, chains.current_chain().ctype, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, xpub, deriv, chains.current_key_chain().ctype,
my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
return None, desc.addr_fmt, xpubs, has_mine, desc.M, desc.N, desc.is_sorted
return None, addr_fmt, xpubs, has_mine, M, N, descriptor.is_sortedmulti
def to_descriptor(self):
return MultisigDescriptor(
M=self.M, N=self.N,
keys=self.xpubs,
addr_fmt=self.addr_fmt,
is_sorted=self.bip67,
)
keys = [
Key.from_cc_data(xfp, deriv, xpub)
for xfp, deriv, xpub in self.xpubs
]
_cls = Sortedmulti if self.bip67 else Multi
miniscript = _cls(Number(self.M), *keys)
desc = Descriptor(miniscript=miniscript)
desc.set_from_addr_fmt(self.addr_fmt)
return desc
@classmethod
def from_file(cls, config, name=None):
@ -731,8 +736,10 @@ class MultisigWallet(WalletABC):
# - M of N line (assume N of N if not spec'd)
# - xpub: any bip32 serialization we understand, but be consistent
#
expect_chain = chains.current_chain().ctype
if MultisigDescriptor.is_descriptor(config):
expect_chain = chains.current_key_chain().ctype
if Descriptor.is_descriptor(config):
# assume descriptor, classic config should not contain sertedmulti( and check for checksum separator
# ignore name
_, addr_fmt, xpubs, has_mine, M, N, bip67 = cls.from_descriptor(config)
if not bip67 and not settings.get("unsort_ms", 0):
# BIP-67 disabled, but unsort_ms not allowed - raise
@ -775,83 +782,6 @@ class MultisigWallet(WalletABC):
return cls(name, (M, N), xpubs, addr_fmt=addr_fmt,
chain_type=expect_chain, bip67=bip67)
@classmethod
def check_xpub(cls, xfp, xpub, deriv, expect_chain, my_xfp, xpubs):
# Shared code: consider an xpub for inclusion into a wallet, if ok, append
# to list: xpubs with a tuple: (xfp, deriv, xpub)
# return T if it's our own key
# - deriv can be None, and in very limited cases can recover derivation path
# - could enforce all same depth, and/or all depth >= 1, but
# seems like more restrictive than needed, so "m" is allowed
try:
# Note: addr fmt detected here via SLIP-132 isn't useful
node, chain, _ = parse_extended_key(xpub)
except:
raise AssertionError('unable to parse xpub')
try:
assert node.privkey() == None # 'no privkeys plz'
except ValueError:
pass
if expect_chain == "XRT":
# HACK but there is no difference extended_keys - just bech32 hrp
assert chain.ctype == "XTN"
else:
assert chain.ctype == expect_chain, 'wrong chain'
depth = node.depth()
if depth == 1:
if not xfp:
# allow a shortcut: zero/omit xfp => use observed parent value
xfp = swab32(node.parent_fp())
else:
# generally cannot check fingerprint values, but if we can, do so.
if not cls.disable_checks:
assert swab32(node.parent_fp()) == xfp, 'xfp depth=1 wrong'
assert xfp, 'need fingerprint' # happens if bare xpub given
# In most cases, we cannot verify the derivation path because it's hardened
# and we know none of the private keys involved.
if depth == 1:
# but derivation is implied at depth==1
kn, is_hard = node.child_number()
if is_hard: kn |= 0x80000000
guess = keypath_to_str([kn], skip=0)
if deriv:
if not cls.disable_checks:
assert guess == deriv, '%s != %s' % (guess, deriv)
else:
deriv = guess # reachable? doubt it
assert deriv, 'empty deriv' # or force to be 'm'?
assert deriv[0] == 'm'
# path length of derivation given needs to match xpub's depth
if not cls.disable_checks:
p_len = deriv.count('/')
assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % (
p_len, depth, xfp2str(xfp))
if xfp == my_xfp:
# its 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:
chk_node = sv.derive_path(deriv)
assert node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
# serialize xpub w/ BIP-32 standard now.
# - this has effect of stripping SLIP-132 confusion away
xpubs.append((xfp, deriv, chain.serialize_public(node, AF_P2SH)))
return (xfp == my_xfp)
def make_fname(self, prefix, suffix='txt'):
rv = '%s-%s.%s' % (prefix, self.name, suffix)
return rv.replace(' ', '_')
@ -956,7 +886,7 @@ class MultisigWallet(WalletABC):
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to write!\n\n\n'+str(e))
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
return
def render_export(self, fp, hdr_comment=None, descriptor=False, core=False, desc_pretty=True):
@ -969,9 +899,10 @@ class MultisigWallet(WalletABC):
print("importdescriptors '%s'\n" % core_str, file=fp)
else:
if desc_pretty:
desc = desc_obj.pretty_serialize()
# TODO pretty serialize
desc = desc_obj.to_string(internal=False)
else:
desc = desc_obj.serialize()
desc = desc_obj.to_string(internal=False)
print("%s\n" % desc, file=fp)
else:
if hdr_comment:
@ -1043,8 +974,9 @@ class MultisigWallet(WalletABC):
for k, v in xpubs_list:
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
xpub = ngu.codecs.b58_encode(v)
is_mine = cls.check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
expect_chain, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
expect_chain, my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
addr_fmt = cls.guess_addr_fmt(path)
@ -1054,7 +986,7 @@ class MultisigWallet(WalletABC):
name = 'PSBT-%d-of-%d' % (M, N)
# this will always create sortedmulti multisig (BIP-67)
# because BIP-174 came years after wide spread acceptance of BIP-67 policy
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH)
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH) # TODO why legacy
# may just keep in-memory version, no approval required, if we are
# trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet
@ -1078,7 +1010,9 @@ class MultisigWallet(WalletABC):
# cleanup and normalize xpub
tmp = []
self.check_xpub(xfp, xpub, keypath_to_str(path, skip=0), self.chain_type, 0, tmp)
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
self.chain_type, 0, self.disable_checks)
tmp.append(item)
(_, deriv, xpub_reserialized) = tmp[0]
assert deriv # because given as arg
@ -1182,7 +1116,7 @@ Press (1) to see extended public keys, '''.format(M=M, N=N, name=self.name, exp=
continue
if ch == 'y' and not is_dup:
# save to nvram, may raise MultisigOutOfSpace
# save to nvram, may raise WalletOutOfSpace
if name_change:
name_change.delete()
@ -1215,7 +1149,7 @@ Press (1) to see extended public keys, '''.format(M=M, N=N, name=self.name, exp=
msg.write('%s:\n %s\n\n%s\n' % (xfp2str(xfp), deriv, xpub))
if self.addr_fmt != AF_P2SH:
if self.addr_fmt not in (AF_P2SH, AF_P2TR):
# SLIP-132 format [yz]pubs here when not p2sh mode.
# - has same info as proper bitcoin serialization, but looks much different
node = self.chain.deserialize_node(xpub, AF_P2SH)
@ -1343,8 +1277,11 @@ class MultisigMenu(MenuSystem):
def construct(cls):
# Dynamic menu with user-defined names of wallets shown
if not MultisigWallet.exists():
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
from bsms import make_ms_wallet_bsms_menu
exists, exists_other_chain = MultisigWallet.exists()
if not exists:
rv = [MenuItem(MultisigWallet.none_setup_yet(exists_other_chain), f=no_ms_yet)]
else:
rv = []
for ms in MultisigWallet.get_all():
@ -1357,6 +1294,7 @@ class MultisigMenu(MenuSystem):
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc,
predicate=bool(NFC), shortcut=KEY_NFC))
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
rv.append(MenuItem('BSMS (BIP-129)', menu=make_ms_wallet_bsms_menu))
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))
@ -1451,7 +1389,7 @@ async def ms_wallet_show_descriptor(menu, label, item):
dis.fullscreen("Wait...")
ms = item.arg
desc = ms.to_descriptor()
desc_str = desc.serialize()
desc_str = desc.to_string(internal=False)
ch = await ux_show_story("Press (1) to export in pretty human readable format.\n\n" + desc_str, escape="1")
if ch == "1":
await ms.export_wallet_file(descriptor=True, desc_pretty=True)
@ -1516,6 +1454,8 @@ P2SH-P2WSH:
m/48h/{coin}h/{{acct}}h/1h
P2WSH:
m/48h/{coin}h/{{acct}}h/2h
P2TR:
m/48h/{coin}h/{{acct}}h/3h
{ok} to continue. {x} to abort.'''.format(coin=chain.b44_cointype, ok=OK, x=X)
@ -1534,9 +1474,10 @@ P2WSH:
dis.fullscreen('Generating...')
todo = [
( "m/45h", 'p2sh', AF_P2SH), # iff acct_num == 0
( "m/48h/{coin}h/{acct_num}h/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH ),
( "m/48h/{coin}h/{acct_num}h/2h", 'p2wsh', AF_P2WSH ),
("m/45h", 'p2sh', AF_P2SH), # iff acct_num == 0
("m/48h/{coin}h/{acct_num}h/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH),
("m/48h/{coin}h/{acct_num}h/2h", 'p2wsh', AF_P2WSH),
("m/48h/{coin}h/{acct_num}h/3h", 'p2tr', AF_P2TR),
]
def render(fp):
@ -1604,7 +1545,9 @@ async def validate_xpub_for_ms(obj, af_str, chain, my_xfp, xpubs):
deriv = cleanup_deriv_path(obj[af_str + '_deriv'])
ln = obj.get(af_str)
return MultisigWallet.check_xpub(xfp, ln, deriv, chain.ctype, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, ln, deriv, chain.ctype, my_xfp, xpubs)
xpubs.append(item)
return is_mine
async def ms_coordinator_qr(af_str, my_xfp, chain):
# Scan a number of JSON files from BBQr w/ derive, xfp and xpub details.
@ -1663,7 +1606,7 @@ async def ms_coordinator_file(af_str, my_xfp, chain, slot_b=None):
# sigh, OS/filesystem variations
file_size = var[1] if len(var) == 2 else get_filesize(full_fname)
if not (0 <= file_size <= 1100):
if not (0 <= file_size <= 1500):
# out of range size
continue
@ -1763,9 +1706,9 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False)
ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, addr_fmt=addr_fmt)
if num_mine:
from auth import NewEnrollRequest, UserAuthorizedAction
from auth import NewMiniscriptEnrollRequest, UserAuthorizedAction
UserAuthorizedAction.active_request = NewEnrollRequest(ms)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(ms)
# menu item case: add to stack
from ux import the_ux
@ -1818,7 +1761,7 @@ async def import_multisig_nfc(*a):
from glob import NFC
# this menu option should not be available if NFC is disabled
try:
return await NFC.import_multisig_nfc()
return await NFC.import_miniscript_nfc(legacy_multisig=True)
except Exception as e:
await ux_show_story(title="ERROR", msg="Failed to import multisig. %s" % str(e))
@ -1865,7 +1808,7 @@ async def import_multisig(*a):
if 'pub' in ln:
return True
fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=20*200,
fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=350*200,
taster=possible, force_vdisk=force_vdisk)
if not fn: return

View File

@ -613,7 +613,6 @@ class NFCHandler:
aborted = await n.share_text("NFC is working: %s" % n.get_uid(), allow_enter=False)
assert not aborted, "Aborted"
async def share_file(self):
# Pick file from SD card and share over NFC...
from actions import file_picker
@ -663,51 +662,40 @@ class NFCHandler:
# user is pushing a file downloaded from another CC over NFC
# - would need an NFC app in between for the sneakernet step
# get some data
data = await self.start_nfc_rx()
if not data: return
def f(m):
if len(m) < 70:
return
m = m.decode()
winner = None
for urn, msg, meta in ndef.record_parser(data):
if len(msg) < 70: continue
msg = bytes(msg).decode() # from memory view
# multi( catches both multi( and sortedmulti(
if 'pub' in msg or "multi(" in msg:
winner = msg
break
if 'pub' in m or "multi(" in m:
return m
if not winner:
await ux_show_story('Unable to find multisig descriptor.')
return
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
if winner:
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_ephemeral_seed_words_nfc(self, *a):
data = await self.start_nfc_rx()
if not data: return
def f(m):
sm = m.decode().strip().split(" ")
if len(sm) in stash.SEED_LEN_OPTS:
return sm
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode().strip() # from memory view
split_msg = msg.split(" ")
if len(split_msg) in stash.SEED_LEN_OPTS:
winner = split_msg
break
winner = await self._nfc_reader(f, 'Unable to find seed words')
if not winner:
await ux_show_story('Unable to find seed words')
return
try:
from seed import set_ephemeral_seed_words
await set_ephemeral_seed_words(winner, meta='NFC Import')
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
if winner:
try:
from seed import set_ephemeral_seed_words
await set_ephemeral_seed_words(winner, meta='NFC Import')
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def confirm_share_loop(self, string):
while True:
@ -720,21 +708,16 @@ class NFCHandler:
break
async def address_show_and_share(self):
from auth import show_address, ApproveMessageSign
from auth import show_address
data = await self.start_nfc_rx()
if not data: return
def f(m):
sm = m.decode().split("\n")
if 1 <= len(sm) <= 2:
return sm
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
split_msg = msg.split("\n")
if 1 <= len(split_msg) <= 2:
winner = split_msg
break
winner = await self._nfc_reader(f, 'Expected address and derivation path.')
if not winner:
await ux_show_story('Expected address and derivation path.')
return
if len(winner) == 1:
@ -759,19 +742,15 @@ class NFCHandler:
UserAuthorizedAction.cleanup()
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
split_msg = msg.split("\n")
def f(m):
m = m.decode()
split_msg = m.split("\n")
if 1 <= len(split_msg) <= 3:
winner = split_msg
break
return split_msg
winner = await self._nfc_reader(f, 'Unable to find correctly formated message to sign.')
if not winner:
await ux_show_story('Unable to find correctly formated message to sign.')
return
if len(winner) == 1:
@ -805,82 +784,94 @@ class NFCHandler:
async def verify_sig_nfc(self):
from auth import verify_armored_signed_msg
data = await self.start_nfc_rx()
if not data: return
f = lambda x: x.decode().strip() if b"SIGNED MESSAGE" in x else None
winner = await self._nfc_reader(f, 'Unable to find signed message.')
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
if "SIGNED MESSAGE" in msg:
winner = msg.strip()
break
if not winner:
await ux_show_story('Unable to find signed message.')
return
await verify_armored_signed_msg(winner, digest_check=False)
if winner:
await verify_armored_signed_msg(winner, digest_check=False)
async def verify_address_nfc(self):
# Get an address or complete bip-21 url even and search it... slow.
from utils import decode_bip21_text
data = await self.start_nfc_rx()
if not data: return
def f(m):
m = m.decode()
what, vals = decode_bip21_text(m)
if what == 'addr':
return vals[1]
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
try:
what, vals = decode_bip21_text(msg)
if what == 'addr':
winner = vals[1]
break
except ValueError:
pass
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
if not winner:
await ux_show_story('Unable to find address from NFC data.')
return
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(winner)
if winner:
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(winner)
async def read_extended_private_key(self):
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
if "prv" in msg:
winner = msg.strip()
break
if not winner:
await ux_show_story('Unable to find extended private key.')
return
return winner
f = lambda x: x.decode().strip() if b"prv" in x else None
return await self._nfc_reader(f, 'Unable to find extended private key.')
async def read_tapsigner_b64_backup(self):
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
async def _nfc_reader(self, func, fail_msg):
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
msg = bytes(msg)
try:
if 150 <= len(msg) <= 280:
winner = a2b_base64(msg)
r = func(msg)
if r is not None:
winner = r
break
except:
pass
if not winner:
await ux_show_story('Unable to find base64 encoded TAPSIGNER backup.')
await ux_show_story(fail_msg)
return
return winner
async def read_bsms_token(self):
def f(m):
m = m.decode().strip()
try:
int(m, 16)
return m
except: pass
return await self._nfc_reader(f, 'Unable to find BSMS token in NDEF data')
async def read_bsms_data(self):
def f(m):
m = m.decode().strip() # from memory view
try:
if "BSMS" in m or int(m[:6], 16):
# unencrypted/encrypted case
return m
except: pass
return await self._nfc_reader(f, 'Unable to find BSMS data in NDEF data')
async def import_miniscript_nfc(self, legacy_multisig=False):
def f(m):
if len(m) < 70: return
m = m.decode()
# TODO this should be Descriptor.is_descriptor() ?
if 'pub' in m:
return m
winner = await self._nfc_reader(f, 'Unable to find miniscript descriptor expected in NDEF')
if not winner:
return
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner, miniscript=not legacy_multisig)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
# EOF

View File

@ -21,7 +21,13 @@ from utils import problem_file_line, url_decode
ONE_LINE = CHARS_W-2
async def make_notes_menu(*a):
if settings.get('notes', False) == False:
from pincodes import pa
if pa.is_deltamode():
import callgate
callgate.fast_wipe()
if not settings.get('secnap', False):
# Explain feature, and then enable if interested. Drop them into menu.
ch = await ux_show_story('''\
Enable this feature to store short text notes and passwords inside the Coldcard.
@ -34,8 +40,10 @@ Press ENTER to enable and get started otherwise CANCEL.''',
if ch != 'y':
return
# mark as enabled (altho empty)
settings.set('notes', [])
# mark as enabled
settings.set('secnap', True)
if settings.get('notes', None) is None:
settings.set('notes', [])
# need to correct top menu now, so this choice is there.
goto_top_menu()
@ -170,6 +178,7 @@ class NotesMenu(MenuSystem):
async def disable_notes(cls, *a):
# they don't want feature anymore; already checked no notes in effect
# - no need for confirm, they aren't loosing anything
settings.remove_key('secnap')
settings.remove_key('notes')
settings.save()

View File

@ -33,6 +33,7 @@ from utils import call_later_ms
# _age = internal verison number for data (see below)
# tested = selftest has been completed successfully
# multisig = list of defined multisig wallets (complex)
# miniscript = list of defined miniscript wallets (complex)
# pms = trust/import/distrust xpubs found in PSBT files
# fee_limit = (int) percentage of tx value allowed as max fee
# axi = index of last selected address in explorer
@ -56,6 +57,7 @@ from utils import call_later_ms
# seedvault = (bool) opt-in enable seed vault feature
# seeds = list of stored secrets for seedvault feature
# bright = (int:0-255) LCD brightness when on battery
# secnap = (bool) opt-in enable Secure Notes & Passwords feature
# notes = (complex) Secure notes held for user, see notes.py
# accts = (list of tuples: (addr_fmt, account#)) Single-sig wallets we've seen them use
# aei = (bool) allow changing start index in Address Explorer
@ -76,7 +78,7 @@ from utils import call_later_ms
# terms_ok = customer has signed-off on the terms of sale
# settings linked to seed
# LINKED_SETTINGS = ["multisig", "tp", "ovc", "xfp", "xpub", "words"]
# LINKED_SETTINGS = ["multisig","miniscript", "tp", "ovc", "xfp", "xpub", "words"]
# settings that does not make sense to copy to temporary secret
# LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"]
# prelogin settings - do not need to be part of other saved settings

View File

@ -82,7 +82,7 @@ OP_RETURN = const(106)
#OP_RSHIFT = const(153)
#OP_BOOLAND = const(154)
#OP_BOOLOR = const(155)
#OP_NUMEQUAL = const(156)
OP_NUMEQUAL = const(156)
#OP_NUMEQUALVERIFY = const(157)
#OP_NUMNOTEQUAL = const(158)
#OP_LESSTHAN = const(159)
@ -114,6 +114,7 @@ OP_CHECKMULTISIGVERIFY = const(175)
#OP_NOP8 = const(183)
#OP_NOP9 = const(184)
#OP_NOP10 = const(185)
OP_CHECKSIGADD = const(186)
#OP_NULLDATA = const(252)
#OP_PUBKEYHASH = const(253)
#OP_PUBKEY = const(254)

View File

@ -7,6 +7,8 @@ from glob import settings
from ucollections import namedtuple
from ubinascii import hexlify as b2a_hex
from exceptions import UnknownAddressExplained
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
@ -49,7 +51,7 @@ class AddressCacheFile:
def __init__(self, wallet, change_idx):
self.wallet = wallet
self.change_idx = change_idx
desc = wallet.to_descriptor().serialize()
desc = wallet.to_descriptor().to_string(internal=False)
h = b2a_hex(ngu.hash.sha256d(wallet.chain.ctype + desc))
self.fname = h[0:32] + '-%d.own' % change_idx
self.salt = h[32:]
@ -158,8 +160,8 @@ class AddressCacheFile:
self.setup(self.change_idx, start_idx)
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
change_idx=self.change_idx):
# change_idx is used as flag here
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count, self.change_idx):
if here == addr:
# Found it! But keep going a little for next time.
@ -207,7 +209,7 @@ class OwnershipCache:
# - returns wallet object, and tuple2 of final 2 subpath components
# - if you start w/ testnet, we'll follow that
from multisig import MultisigWallet
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
from miniscript import MiniScriptWallet
from glob import dis
ch = chains.current_chain()
@ -220,21 +222,28 @@ class OwnershipCache:
possibles = []
msc_exists = MiniScriptWallet.exists()[0]
if addr_fmt == AF_P2TR and msc_exists:
possibles.extend([w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2TR])
if addr_fmt & AFC_SCRIPT:
# multisig or script at least.. must exist already
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
msc = [w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == addr_fmt]
possibles.extend(msc)
if addr_fmt == AF_P2SH:
# might look like P2SH but actually be AF_P2WSH_P2SH
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH))
msc = [w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2WSH_P2SH]
possibles.extend(msc)
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
# thing that hopefully is going away, so if they have any multisig wallets,
# defined, assume that that's the only p2sh address source.
addr_fmt = AF_P2WPKH_P2SH
# TODO: add tapscript and such fancy stuff here
try:
# Construct possible single-signer wallets, always at least account=0 case
from wallet import MasterSingleSigWallet
@ -252,7 +261,7 @@ class OwnershipCache:
if not possibles:
# can only happen w/ scripts; for single-signer we have things to check
raise UnknownAddressExplained(
"No suitable multisig wallets are currently defined.")
"No suitable multisig/miniscript wallets are currently defined.")
# "quick" check first, before doing any generations
@ -314,7 +323,8 @@ class OwnershipCache:
msg = addr
msg += '\n\nFound in wallet:\n ' + wallet.name
msg += '\nDerivation path:\n ' + wallet.render_path(*subpath)
if hasattr(wallet, "render_path"):
msg += '\nDerivation path:\n ' + wallet.render_path(*subpath)
if version.has_qwerty:
esc = KEY_QR
else:
@ -325,11 +335,15 @@ class OwnershipCache:
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)
await show_qr_code(addr,
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
msg=addr)
except UnknownAddressExplained as exc:
await ux_show_story(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)))
@classmethod
def note_subpath_used(cls, subpath):

View File

@ -3,14 +3,15 @@
#
# paper.py - generate paper wallets, based on random values (not linked to wallet)
#
import ujson
import ujson, ngu, chains
from ubinascii import hexlify as b2a_hex
from utils import imported
from public_constants import AF_CLASSIC, AF_P2WPKH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
from ux import ux_show_story, ux_dramatic_pause
from files import CardSlot, CardMissingError, needs_microsd
from actions import file_picker
from menu import MenuSystem, MenuItem
from stash import blank_object
background_msg = '''\
Coldcard will pick a random private key (which has no relation to your seed words), \
@ -29,10 +30,6 @@ can still be made. Visit the Coldcard website to get some interesting templates.
SECP256K1_ORDER = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xaf\x48\xa0\x3b\xbf\xd2\x5e\x8c\xd0\x36\x41\x41"
# Aprox. time of this feature release (Nov 20/2019) so no need to scan
# blockchain earlier than this during "importmulti"
FEATURE_RELEASE_TIME = const(1574277000)
# These very-specific text values are matched on the Coldcard; cannot be changed.
class placeholders:
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
@ -51,6 +48,12 @@ class PaperWalletMaker:
self.my_menu = my_menu
self.template_fn = None
self.is_segwit = False
self.is_taproot = False
def atype(self):
if self.is_taproot: return 2, 'Taproot P2TR'
if self.is_segwit: return 1, 'Segwit P2WPKH'
return 0, 'Classic P2PKH'
async def pick_template(self, *a):
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
@ -62,17 +65,17 @@ class PaperWalletMaker:
def addr_format_chooser(self, *a):
# simple bool choice
def set(idx, text):
self.is_segwit = bool(idx)
self.is_segwit = idx == 1
self.is_taproot = idx == 2
self.update_menu()
return int(self.is_segwit), ['Classic P2PKH', 'Segwit P2WPKH'], set
return self.atype()[0], ['Classic P2PKH', 'Segwit P2WPKH', 'Taproot P2TR'], set
def update_menu(self):
# Reconstruct the menu contents based on our state.
self.my_menu.replace_items([
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
f=self.pick_template),
MenuItem('Classic P2PKH' if not self.is_segwit else 'Segwit P2WPKH',
chooser=self.addr_format_chooser),
MenuItem(self.atype()[1], chooser=self.addr_format_chooser),
MenuItem('Use Dice', f=self.use_dice),
MenuItem('GENERATE WALLET', f=self.doit),
], keep_position=True)
@ -82,12 +85,6 @@ class PaperWalletMaker:
from glob import dis, VD
try:
import ngu
from auth import write_sig_file
from chains import current_chain
from serializations import hash160
from stash import blank_object
if not have_key:
# get some random bytes
await ux_dramatic_pause("Picking key...", 2)
@ -104,12 +101,16 @@ class PaperWalletMaker:
dis.fullscreen("Rendering...")
# make payment address
digest = hash160(pubkey)
ch = current_chain()
ch = chains.current_chain()
if self.is_segwit:
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
af = AF_P2WPKH
elif self.is_taproot:
af = AF_P2TR
pubkey = pubkey[1:]
else:
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
af = AF_CLASSIC
addr = ch.pubkey_to_address(pubkey, af)
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
@ -164,8 +165,11 @@ class PaperWalletMaker:
else:
nice_pdf = ''
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
nice_sig = None
if af != AF_P2TR:
from auth import write_sig_file
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
# Half-hearted attempt to cleanup secrets-contaminated memory
# - better would be force user to reboot
@ -185,7 +189,8 @@ class PaperWalletMaker:
story = "Done! Created file(s):\n\n%s" % nice_txt
if nice_pdf:
story += "\n\n%s" % nice_pdf
story += "\n\n%s" % nice_sig
if nice_sig:
story += "\n\n%s" % nice_sig
await ux_show_story(story)
async def use_dice(self, *a):
@ -214,10 +219,17 @@ class PaperWalletMaker:
fp.write('Bitcoin Core command:\n\n')
# new hotness: output descriptors
desc = ('wpkh(%s)' if self.is_segwit else 'pkh(%s)') % wif
multi = ujson.dumps(dict(timestamp=FEATURE_RELEASE_TIME, desc=append_checksum(desc)))
fp.write(" bitcoin-cli importmulti '[%s]'\n\n" % multi)
fp.write('# OR (more compatible, but slower)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
if self.is_taproot:
desc = 'tr(%s)'
elif self.is_segwit:
desc = 'wpkh(%s)'
else:
desc = 'pkh(%s)'
desc = desc % wif
descriptor = ujson.dumps(dict(timestamp="now", desc=append_checksum(desc)))
fp.write(" bitcoin-cli importdescriptors '[%s]'\n\n" % descriptor)
if not self.is_taproot:
fp.write('# OR (only supported with legacy wallets)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
if qr_addr and qr_wif:
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')

File diff suppressed because it is too large Load Diff

View File

@ -423,9 +423,11 @@ async def add_seed_to_vault(encoded, meta=None):
if not settings.master_get("seedvault", False):
# seed vault disabled
# this can be re-enabled by attacker in deltamode
return
if pa.is_secret_blank():
if pa.is_secret_blank() or pa.is_deltamode():
# do not save anything if no SE secret yet
# do not offer any access to SV in deltamode
return
# do not offer to store secrets that are already in vault
@ -828,42 +830,37 @@ class SeedVaultMenu(MenuSystem):
async def _remove(menu, label, item):
from glob import dis, settings
esc = ""
tmp_val = False
idx, xfp_str, encoded = item.arg
current_active = (pa.tmp_value == bytes(encoded))
msg = ("Remove seed from seed vault and delete its "
"settings?\n\nPress %s to continue, press (1) to "
"only remove from seed vault and keep "
"encrypted settings for later use.\n\n"
"WARNING: Funds will be lost if wallet is"
" not backed-up elsewhere.") % OK
msg = "Remove seed from seed vault "
if pa.tmp_value and current_active:
tmp_val = True
msg += "?\n\n"
else:
msg += ("and delete its settings?\n\n"
"Press %s to continue, press (1) to "
"only remove from seed vault and keep "
"encrypted settings for later use.\n\n") % OK
esc += "1"
ch = await ux_show_story(title="[" + xfp_str + "]", msg=msg, escape="1")
msg += "WARNING: Funds will be lost if wallet is not backed-up elsewhere."
ch = await ux_show_story(title="[" + xfp_str + "]", msg=msg, escape=esc)
if ch == "x": return
dis.fullscreen("Saving...")
wipe_slot = (ch != "1")
tmp_val = False
if pa.tmp_value:
tmp_val = True
wipe_slot = not current_active and (ch != "1")
if wipe_slot:
# are we deleting current active ephemeral wallet
# and its settings ?
# slot wiping
if tmp_val:
# wipe current settings
settings.blank()
pa.tmp_value = False
settings.return_to_master_seed()
else:
# in main settings
xs = SettingsObject()
xs.set_key(encoded)
xs.load()
xs.blank()
del xs
xs = SettingsObject()
xs.set_key(encoded)
xs.load()
xs.blank()
del xs
# CAUTION: will get shadow copy if in tmp seed mode already
seeds = settings.master_get("seeds", [])
@ -970,6 +967,12 @@ class SeedVaultMenu(MenuSystem):
from glob import settings
from pincodes import pa
if pa.is_deltamode():
# attacker has re-enabled SeedVault in Settings
import callgate
callgate.fast_wipe()
rv = []
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)

View File

@ -16,7 +16,6 @@ ser_*, deser_*: functions that handle serialization/deserialization
"""
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
import ustruct as struct
import ngu
from opcodes import *
@ -30,6 +29,7 @@ hash160 = ngu.hash.hash160
def bytes_to_hex_str(s):
return str(b2a_hex(s), 'ascii')
SIGHASH_DEFAULT = const(0) # in taproot meaning same as SIGHASH_ALL (over whole TX)
SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2)
SIGHASH_SINGLE = const(3)
@ -37,6 +37,7 @@ SIGHASH_ANYONECANPAY = const(0x80)
# list containing all flags that we support signing for
ALL_SIGHASH_FLAGS = [
SIGHASH_DEFAULT,
SIGHASH_ALL,
SIGHASH_NONE,
SIGHASH_SINGLE,
@ -56,14 +57,20 @@ def ser_compact_size(l):
else:
return struct.pack("<BQ", 255, l)
def deser_compact_size(f):
def deser_compact_size(f, ret_num_bytes=False):
nit = struct.unpack("<B", f.read(1))[0]
num_bytes = 1
if nit == 253:
nit = struct.unpack("<H", f.read(2))[0]
num_bytes += 2
elif nit == 254:
nit = struct.unpack("<I", f.read(4))[0]
num_bytes += 4
elif nit == 255:
nit = struct.unpack("<Q", f.read(8))[0]
num_bytes += 8
if ret_num_bytes:
return nit, num_bytes
return nit
def deser_string(f):
@ -367,6 +374,11 @@ class CTxOut(object):
# aka. P2WPKH
return 'p2pkh', self.scriptPubKey[2:2+20], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 81 and self.scriptPubKey[1] == 32:
# aka. P2TR
return 'p2tr', self.scriptPubKey[2:2+32], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
# aka. P2WSH

View File

@ -53,7 +53,7 @@ HSM_WHITELIST = frozenset({
'blkc', 'hsts', # report status values
'stok', 'smok', # completion check: sign txn or msg
'xpub', 'msck', # quick status checks
'p2sh', 'show', # limited by HSM policy
'p2sh', 'show', 'msas', # limited by HSM policy
'user', # auth HSM user, other user cmds not allowed
'gslr', # read storage locker; hsm mode only, limited usage
})
@ -483,7 +483,7 @@ class USBHandler:
file_len, file_sha = unpack_from('<I32s', args)
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 100 < file_len <= (20*200), "badlen"
assert 100 < file_len <= (32*200), "badlen"
# Start an UX interaction, return immediately here
from auth import maybe_enroll_xpub
@ -491,6 +491,82 @@ class USBHandler:
return None
if cmd == 'mins':
# Enroll new xpubkey to be involved in miniscript.
# - descriptor text config file must already be uploaded
file_len, file_sha = unpack_from('<I32s', args)
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 100 < file_len <= (100 * 200), "badlen"
# Start an UX interaction, return immediately here
from auth import maybe_enroll_xpub
maybe_enroll_xpub(sf_len=file_len, ux_reset=True, miniscript=True)
return None
if cmd == "msls":
# list all registered miniscript wallet names
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
wallets = [w.name for w in MiniScriptWallet.iter_wallets()]
import ujson
return b'asci' + ujson.dumps(wallets)
if cmd == "msdl":
# delete miniscript wallet by its name (unique id)
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
assert len(args) < 40, "len args"
for w in MiniScriptWallet.iter_wallets():
if w.name == str(args, 'ascii'):
break
else:
return b'err_Miniscript wallet not found'
from auth import maybe_delete_miniscript
maybe_delete_miniscript(w)
return None
if cmd == "msgt":
# takes name and returns descriptor + name json
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
assert len(args) < 40, "len args"
for w in MiniScriptWallet.iter_wallets():
if w.name == str(args, 'ascii'):
import ujson
return b'asci' + ujson.dumps({"name": w.name, "desc": w.to_string()})
return b'err_Miniscript wallet not found'
if cmd == "msas":
# get miniscript address based on int/ext index
assert self.encrypted_req, 'must encrypt'
if hsm_active and not hsm_active.approve_address_share(miniscript=True):
raise HSMDenied
from miniscript import MiniScriptWallet
change, idx, = unpack_from('<II', args)
assert change in (0, 1), "change not bool"
assert 0 <= idx < (2 ** 31), "child idx"
name = args[8:]
msc = None
for w in MiniScriptWallet.iter_wallets():
if w.name == str(name, 'ascii'):
msc = w
break
else:
return b'err_Miniscript wallet not found'
from auth import start_show_miniscript_address
return b'asci' + start_show_miniscript_address(msc, change, idx)
if cmd == 'msck':
# Quick check to test if we have a wallet already installed.
from multisig import MultisigWallet

View File

@ -2,12 +2,14 @@
#
# utils.py - Misc utils. My favourite kind of source file.
#
import gc, sys, ustruct, ngu, chains, ure, time, bip39
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 uhashlib import sha256
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
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
B2A = lambda x: str(b2a_hex(x), 'ascii')
@ -91,7 +93,6 @@ def pop_count(i):
def get_filesize(fn):
# like os.path.getsize()
import uos
try:
return uos.stat(fn)[6]
except OSError:
@ -220,8 +221,6 @@ def to_ascii_printable(s, strip=False):
def problem_file_line(exc):
# return a string of just the filename.py and line number where
# an exception occured. Best used on AssertionError.
import uio, sys, ure
tmp = uio.StringIO()
sys.print_exception(exc, tmp)
lines = tmp.getvalue().split('\n')[-3:]
@ -251,7 +250,6 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# - assume 'm' prefix, so '34' becomes 'm/34', etc
# - do not assume /// is m/0/0/0
# - if allow_star, then final position can be * or *h (wildcard)
import ure
from public_constants import MAX_PATH_DEPTH
s = to_ascii_printable(bin_path, strip=True).lower()
@ -345,6 +343,13 @@ def match_deriv_path(patterns, path):
return False
def validate_derivation_path_length(length, allow_master=False):
# force them to use a derived key, never the master
if not allow_master:
assert length >= 4, 'too short key path'
assert (length % 4) == 0, 'corrupt key path'
assert (length // 4) <= MAX_PATH_DEPTH, 'too deep'
class DecodeStreamer:
def __init__(self):
self.runt = bytearray()
@ -431,7 +436,7 @@ def clean_shutdown(style=0):
# wipe SPI flash and shutdown (wiping main memory)
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
# - bootrom wipes every byte of SRAM, so no need to repeat here
import callgate, version, uasyncio
import callgate, uasyncio
# save if anything pending
from glob import settings
@ -507,9 +512,7 @@ def word_wrap(ln, w):
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC, AF_P2TR]:
return addr_fmt
addr_fmt = addr_fmt.lower()
@ -519,9 +522,10 @@ def parse_addr_fmt_str(addr_fmt):
return AF_CLASSIC
elif addr_fmt == "p2wpkh":
return AF_P2WPKH
elif addr_fmt == "p2tr":
return AF_P2TR
else:
raise ValueError("Invalid address format: '%s'\n\n"
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
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.
@ -563,7 +567,10 @@ def addr_fmt_label(addr_fmt):
return {
AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH"
AF_P2WPKH: "Segwit P2WPKH",
AF_P2TR: "Taproot P2TR",
AF_P2WSH: "Segwit P2WSH",
AF_P2WSH_P2SH: "P2SH-P2WSH"
}[addr_fmt]
@ -615,11 +622,6 @@ def datetime_to_str(dt, fmt="%d-%02d-%02d %02d:%02d:%02d"):
dts = fmt % (y, mo, d, h, mi, s)
return dts + " UTC"
def censor_address(addr):
# We don't like to show the user multisig addresses because we cannot be certain
# they are valid and could actually be signed. And yet, dont blank too many
# spots or else an attacker could grind out a suitable replacement.
return addr[0:12] + '___' + addr[12+3:]
def txid_from_fname(fname):
if len(fname) >= 64:
@ -698,7 +700,95 @@ def decode_bip21_text(got):
raise ValueError('not bip-21')
def check_xpub(xfp, xpub, deriv, expect_chain, my_xfp, disable_checks=False):
# Shared code: consider an xpub for inclusion into a wallet
# return T if it's our own key and parsed details in form (xfp, deriv, xpub)
# - deriv can be None, and in very limited cases can recover derivation path
# - could enforce all same depth, and/or all depth >= 1, but
# seems like more restrictive than needed, so "m" is allowed
import stash
from public_constants import AF_P2SH
try:
# Note: addr fmt detected here via SLIP-132 isn't useful
node, chain, _ = parse_extended_key(xpub)
except:
raise AssertionError('unable to parse xpub')
try:
assert node.privkey() == None # 'no privkeys plz'
except ValueError:
pass
if expect_chain == "XRT":
# HACK but there is no difference extended_keys - just bech32 hrp
assert chain.ctype == "XTN"
else:
assert chain.ctype == expect_chain, 'wrong chain'
depth = node.depth()
if depth == 1:
if not xfp:
# allow a shortcut: zero/omit xfp => use observed parent value
xfp = swab32(node.parent_fp())
else:
# generally cannot check fingerprint values, but if we can, do so.
if not disable_checks:
assert swab32(node.parent_fp()) == xfp, 'xfp depth=1 wrong'
assert xfp, 'need fingerprint' # happens if bare xpub given
# In most cases, we cannot verify the derivation path because it's hardened
# and we know none of the private keys involved.
if depth == 1:
# but derivation is implied at depth==1
kn, is_hard = node.child_number()
if is_hard: kn |= 0x80000000
guess = keypath_to_str([kn], skip=0)
if deriv:
if not disable_checks:
assert guess == deriv, '%s != %s' % (guess, deriv)
else:
deriv = guess # reachable? doubt it
assert deriv, 'empty deriv' # or force to be 'm'?
assert deriv[0] == 'm'
# 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 xfp == my_xfp:
# its 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:
chk_node = sv.derive_path(deriv)
assert node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
# serialize xpub w/ BIP-32 standard now.
# - 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)
# EOF

View File

@ -456,7 +456,7 @@ def import_export_prompt_decode(ch):
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
no_nfc=False, title=None, intro='', footnotes='',
slot_b_only=False):
slot_b_only=False, force_prompt=False):
# Show story allowing user to select source for importing/exporting
# - return either str(mode) OR dict(file_args)
# - KEY_NFC or KEY_QR for those sources
@ -466,7 +466,8 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
if is_import:
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
else:
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc)
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc,
force_prompt=force_prompt)
# TODO: detect if we're only asking A or B, when just one card is inserted
# - assume that's what they want to do

View File

@ -934,12 +934,13 @@ class QRScannerInteraction:
await ux_visualize_bip21(proto, addr, args)
return
if what == "multi":
if what in ("multi", "minisc"):
from auth import maybe_enroll_xpub
from ux import ux_show_story
ms_config, = vals
try:
maybe_enroll_xpub(config=ms_config)
maybe_enroll_xpub(config=ms_config,
miniscript=False if what == "multi" else None)
except Exception as e:
await ux_show_story(
'Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))

View File

@ -122,6 +122,9 @@ def probe_system():
# what firmware signing key did we boot with? are we in dev mode?
is_devmode = get_is_devmode()
# newer, edge code in effect?
is_edge = (get_mpy_version()[1][-1] == 'X')
# increase size limits for mk4
from public_constants import MAX_TXN_LEN_MK4, MAX_UPLOAD_LEN_MK4
MAX_UPLOAD_LEN = MAX_UPLOAD_LEN_MK4

View File

@ -3,12 +3,17 @@
# wallet.py - A place you find UTXO, addresses and descriptors.
#
import chains
from descriptor import Descriptor
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from glob import settings
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from stash import SensitiveValues
MAX_BIP32_IDX = (2 ** 31) - 1
class WalletOutOfSpace(RuntimeError):
pass
class WalletABC:
# How to make this ABC useful without consuming memory/code space??
# - be more of an "interface" than a base class
@ -40,8 +45,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_P2WPKH:
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:
@ -66,7 +73,6 @@ class MasterSingleSigWallet(WalletABC):
if self.chain.ctype == 'XRT':
n += ' (Regtest)'
self.name = n
self.addr_fmt = addr_fmt
@ -82,7 +88,6 @@ class MasterSingleSigWallet(WalletABC):
self._path = p
def yield_addresses(self, start_idx, count, change_idx=None):
# Render a range of addresses. Slow to start, since accesses SE in general
# - if count==1, don't derive any subkey, just do path.
@ -126,10 +131,132 @@ class MasterSingleSigWallet(WalletABC):
def to_descriptor(self):
from glob import settings
from descriptor import Descriptor, Key
xfp = settings.get('xfp')
xpub = settings.get('xpub')
keys = (xfp, self._path, xpub)
return Descriptor([keys], self.addr_fmt)
d = Descriptor(key=Key.from_cc_data(xfp, self._path, xpub))
d.set_from_addr_fmt(self.addr_fmt)
return d
class BaseStorageWallet(WalletABC):
key_name = None
def __init__(self, chain_type=None):
self.storage_idx = -1
self.chain_type = chain_type or 'BTC'
@property
def chain(self):
return chains.get_chain(self.chain_type)
@classmethod
def none_setup_yet(cls, other_chain=False):
return '(none setup yet)' + ("*" if other_chain else "")
@classmethod
def is_correct_chain(cls, o, curr_chain):
if o[1] is None:
# mainnet
ch = "BTC"
else:
ch = o[1]
if ch == curr_chain.ctype:
return True
return False
@classmethod
def exists(cls):
# are there any wallets defined?
exists = False
exists_other_chain = False
c = chains.current_key_chain()
for o in settings.get(cls.key_name, []):
if cls.is_correct_chain(o, c):
exists = True
else:
exists_other_chain = True
return exists, exists_other_chain
@classmethod
def get_all(cls):
# return them all, as a generator
return cls.iter_wallets()
@classmethod
def iter_wallets(cls):
# - this is only place we should be searching this list, please!!
lst = settings.get(cls.key_name, [])
c = chains.current_key_chain()
for idx, rec in enumerate(lst):
if cls.is_correct_chain(rec, c):
yield cls.deserialize(rec, idx)
def serialize(self):
raise NotImplemented
@classmethod
def deserialize(cls, c, idx=-1):
raise NotImplemented
@classmethod
def get_by_idx(cls, nth):
# instance from index number (used in menu)
lst = settings.get(cls.key_name, [])
try:
obj = lst[nth]
except IndexError:
return None
return cls.deserialize(obj, nth)
def commit(self):
# data to save
# - important that this fails immediately when nvram overflows
obj = self.serialize()
v = settings.get(self.key_name, [])
orig = v.copy()
if not v or self.storage_idx == -1:
# create
self.storage_idx = len(v)
v.append(obj)
else:
# update in place
v[self.storage_idx] = obj
settings.set(self.key_name, v)
# save now, rather than in background, so we can recover
# from out-of-space situation
try:
settings.save()
except:
# back out change; no longer sure of NVRAM state
try:
settings.set(self.key_name, orig)
settings.save()
except: pass # give up on recovery
raise WalletOutOfSpace
def delete(self):
# remove saved entry
# - important: not expecting more than one instance of this class in memory
assert self.storage_idx >= 0
lst = settings.get(self.key_name, [])
try:
del lst[self.storage_idx]
if lst:
settings.set(self.key_name, lst)
else:
settings.remove_key(self.key_name)
settings.save() # actual write
except IndexError: pass
self.storage_idx = -1
# EOF

View File

@ -2,12 +2,12 @@
//
// AUTO-generated.
//
// built: 2024-09-12
// version: 5.4.0
// built: 2024-12-18
// version: 6.3.4X
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x592c2880UL;
return 0x59923060UL;
}

View File

@ -2,12 +2,12 @@
//
// AUTO-generated.
//
// built: 2024-09-12
// version: 1.3.0Q
// built: 2024-12-18
// version: 6.3.4QX
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x592c0860UL;
return 0x59923060UL;
}

View File

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

View File

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

View File

@ -239,7 +239,6 @@ def bitcoind_d_sim_watch(bitcoind):
descriptors = [
{
"timestamp": "now",
"label": "Coldcard 0f056943 segwit v0",
"active": True,
"desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/0/*)#erexmnep",
"internal": False
@ -252,7 +251,6 @@ def bitcoind_d_sim_watch(bitcoind):
},
{
"timestamp": "now",
"label": "Coldcard 0f056943 segwit v1",
"active": True,
"desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/0/*)#6ghw47ge",
"internal": False
@ -265,7 +263,6 @@ def bitcoind_d_sim_watch(bitcoind):
},
{
"timestamp": "now",
"label": "Coldcard 0f056943 p2pkh",
"active": True,
"desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/0/*)#fxwk08tc",
"internal": False
@ -278,7 +275,6 @@ def bitcoind_d_sim_watch(bitcoind):
},
{
"timestamp": "now",
"label": "Coldcard 0f056943 p2sh-p2wpkh",
"active": True,
"desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/0/*))#weah3vek",
"internal": False
@ -324,7 +320,6 @@ def bitcoind_d_sim_sign(bitcoind):
descriptors = [
{
"timestamp": "now",
"label": "Coldcard 0f056943",
"active": True,
"desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/0/*)#mzg0pna0",
"internal": False
@ -337,7 +332,6 @@ def bitcoind_d_sim_sign(bitcoind):
},
{
"timestamp": "now",
"label": "Coldcard 0f056943 segwit v1",
"active": True,
"desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/0/*)#x7dfk9mw",
"internal": False
@ -350,7 +344,6 @@ def bitcoind_d_sim_sign(bitcoind):
},
{
"timestamp": "now",
"label": "Coldcard 0f056943",
"active": True,
"desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/0/*)#kjnlnm3v",
"internal": False
@ -363,7 +356,6 @@ def bitcoind_d_sim_sign(bitcoind):
},
{
"timestamp": "now",
"label": "Coldcard 0f056943",
"active": True,
"desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/0/*))#0qf5gv2y",
"internal": False

View File

@ -6,8 +6,9 @@ from io import BytesIO
try:
from pysecp256k1 import (
ec_seckey_verify, ec_pubkey_create, ec_pubkey_serialize, ec_pubkey_parse,
ec_seckey_tweak_add, ec_pubkey_tweak_add,
ec_seckey_tweak_add, ec_pubkey_tweak_add, tagged_sha256
)
from pysecp256k1.extrakeys import xonly_pubkey_from_pubkey, xonly_pubkey_serialize, xonly_pubkey_tweak_add
except ImportError:
import ecdsa
SECP256k1 = ecdsa.curves.SECP256k1
@ -119,6 +120,10 @@ class PrivateKey(object):
tweaked = ec_seckey_tweak_add(self.k, tweak32)
return PrivateKey(sec_exp=tweaked)
def address(self, compressed: bool = True, chain: str = "BTC",
addr_fmt: str = "p2wpkh") -> str:
return self.K.address(compressed, chain, addr_fmt)
@classmethod
def from_wif(cls, wif_str: str) -> "PrivateKey":
"""
@ -193,8 +198,17 @@ class PublicKey(object):
return self.K.to_string(encoding="compressed" if compressed else "uncompressed")
def tweak_add(self, tweak32: bytes) -> "PublicKey":
assert len(tweak32) == 32
return PublicKey(pub_key=ec_pubkey_tweak_add(self.K, tweak32))
def taptweak(self, tweak32: bytes = None) -> "bytes":
xonly_key, _ = xonly_pubkey_from_pubkey(self.K)
tweak = tweak32 or xonly_pubkey_serialize(xonly_key)
tweak = tagged_sha256(b"TapTweak", tweak)
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_key, tweak)
tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonly_pubkey)
@classmethod
def parse(cls, key_bytes: bytes) -> "PublicKey":
"""
@ -227,7 +241,7 @@ class PublicKey(object):
"""
return hash160(self.sec(compressed=compressed))
def address(self, compressed: bool = True, testnet: bool = False,
def address(self, compressed: bool = True, chain: str = "BTC",
addr_fmt: str = "p2wpkh") -> str:
"""
Generates bitcoin address from public key.
@ -240,18 +254,33 @@ class PublicKey(object):
3. p2wpkh (default)
:return: bitcoin address
"""
if chain == "BTC":
hrp = "bc"
pkh_prefix = b"\x00"
sh_prefix = b"\x05"
else:
pkh_prefix = b"\x6f"
sh_prefix = b"\xc4"
if chain == "XRT":
hrp = "bcrt"
elif chain == "XTN":
hrp = "tb"
else:
assert False
if addr_fmt == "p2tr":
tweaked_xonly = self.taptweak()
return bech32.encode(hrp=hrp, witver=1, witprog=tweaked_xonly)
h160 = self.h160(compressed=compressed)
if addr_fmt == "p2pkh":
prefix = b"\x6f" if testnet else b"\x00"
return encode_base58_checksum(prefix + h160)
return encode_base58_checksum(pkh_prefix + h160)
elif addr_fmt == "p2wpkh":
hrp = "tb" if testnet else "bc"
return bech32.encode(hrp=hrp, witver=0, witprog=h160)
elif addr_fmt == "p2sh-p2wpkh":
scr = b"\x00\x14" + h160 # witversion 0 + pubkey hash
h160 = hash160(scr)
prefix = b"\xc4" if testnet else b"\x05"
return encode_base58_checksum(prefix + h160)
return encode_base58_checksum(sh_prefix + h160)
raise ValueError("Unsupported address type.")
@ -708,6 +737,12 @@ class BIP32Node:
ek = PubKeyNode.parse(extended_key, testnet)
return cls(ek, netcode="XTN" if testnet else "BTC")
@classmethod
def from_chaincode_pubkey(cls, chain_code, pubkey, netcode="XTN"):
node = PubKeyNode(pubkey, chain_code, 0, 0,
False if netcode == "BTC" else True)
return cls(node, netcode=netcode)
def subkey_for_path(self, path):
path_list = str_to_path(path)
node = self.node
@ -730,9 +765,9 @@ class BIP32Node:
def hash160(self, compressed=True):
return self.node.public_key.h160(compressed)
def address(self, compressed=True, netcode="XTN", addr_fmt="p2pkh"):
def address(self, compressed=True, chain="XTN", addr_fmt="p2pkh"):
return self.node.public_key.address(compressed, addr_fmt=addr_fmt,
testnet=False if netcode == "BTC" else True)
chain=chain)
def sec(self, compressed=True):
return self.node.public_key.sec(compressed)

View File

@ -1,9 +1,9 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, pdb
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
from helpers import B2A, U2SAT, hash160, taptweak
from base58 import decode_base58_checksum
from bip32 import BIP32Node
from msg import verify_message
@ -293,26 +293,30 @@ def addr_vs_path(master_xpub):
from bip32 import BIP32Node
from ckcc_protocol.constants import AF_CLASSIC, AFC_PUBKEY, AF_P2WPKH, AFC_SCRIPT
from ckcc_protocol.constants import AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH
from bech32 import bech32_decode, convertbits, Encoding
from bech32 import bech32_decode, convertbits, decode, Encoding
from hashlib import sha256
def doit(given_addr, path=None, addr_fmt=None, script=None, testnet=True):
def doit(given_addr, path=None, addr_fmt=None, script=None, chain="XTN"):
if not script:
try:
# prefer using xpub if we can
mk = BIP32Node.from_wallet_key(master_xpub)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
mk._netcode = chain
sk = mk.subkey_for_path(path)
except:
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
mk._netcode = chain
sk = mk.subkey_for_path(path)
if addr_fmt in {None, AF_CLASSIC}:
if addr_fmt == AF_P2TR:
tweaked_xonly = taptweak(sk.sec()[1:])
decoded = decode(given_addr[:2], given_addr)
assert not given_addr.startswith("bcrt") # regtest
assert tweaked_xonly == bytes(decoded[1])
elif addr_fmt in {None, AF_CLASSIC}:
# easy
assert sk.address(netcode="XTN" if testnet else "BTC") == given_addr
assert sk.address(chain=chain) == given_addr
elif addr_fmt & AFC_PUBKEY:
@ -360,7 +364,6 @@ def addr_vs_path(master_xpub):
return doit
@pytest.fixture(scope='module')
def capture_enabled(sim_eval):
# need to have sim_display imported early, see unix/frozen-modules/ckcc
@ -622,6 +625,12 @@ def get_secrets(sim_execfile):
return doit
@pytest.fixture
def clear_miniscript(unit_test):
def doit():
unit_test('devtest/wipe_miniscript.py')
return doit
@pytest.fixture(scope='module')
def press_select(dev, has_qwerty):
f = functools.partial(_press_select, dev, has_qwerty)
@ -1574,6 +1583,9 @@ def nfc_read(request, needs_nfc):
def nfc_write(request, needs_nfc, is_q1):
# WRITE data into NFC "chip"
def doit_usb(ccfile):
from ckcc.constants import MAX_MSG_LEN
if len(ccfile) >= MAX_MSG_LEN:
pytest.xfail("MAX_MSG_LEN")
sim_exec = request.getfixturevalue('sim_exec')
press_select = request.getfixturevalue('press_select')
rv = sim_exec('list(glob.NFC.big_write(%r))' % ccfile)
@ -1689,7 +1701,7 @@ def load_shared_mod():
return doit
@pytest.fixture
def verify_detached_signature_file(microsd_path, virtdisk_path):
def verify_detached_signature_file(microsd_path, virtdisk_path, garbage_collector):
def doit(fnames, sig_fname, way, addr_fmt=None):
fpaths = []
for fname in fnames:
@ -1698,6 +1710,7 @@ def verify_detached_signature_file(microsd_path, virtdisk_path):
else:
path = virtdisk_path(fname)
fpaths.append(path)
garbage_collector.append(path)
if way == "sd":
sig_path = microsd_path(sig_fname)
@ -1738,9 +1751,7 @@ def verify_detached_signature_file(microsd_path, virtdisk_path):
assert (hashlib.sha256(contents).digest().hex() + fn_addendum) in msg
assert verify_message(address, sig, msg) is True
try:
os.unlink(sig_path)
except: pass
garbage_collector.append(sig_path)
return fcontents[0], address
return doit
@ -1774,10 +1785,10 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache
@pytest.fixture
def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json,
load_export_and_verify_signature, is_q1, press_cancel, press_select, readback_bbqr,
cap_screen_qr):
cap_screen_qr, garbage_collector):
def doit(way, label, is_json, sig_check=True, addr_fmt=AF_CLASSIC, ret_sig_addr=False,
tail_check=None, sd_key=None, vdisk_key=None, nfc_key=None, ret_fname=False,
fpattern=None, qr_key=None):
fpattern=None, qr_key=None, skip_query=False):
s_label = None
if label == "Address summary":
@ -1789,54 +1800,55 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
"nfc": nfc_key or (KEY_NFC if is_q1 else "3"),
"qr": qr_key or (KEY_QR if is_q1 else "4"),
}
time.sleep(0.2)
title, story = cap_story()
if way == "sd":
if f"({key_map['sd']}) to save {s_label if s_label else label} file to SD Card" in story:
need_keypress(key_map['sd'])
if not skip_query:
time.sleep(0.2)
title, story = cap_story()
if way == "sd":
if f"({key_map['sd']}) to save {s_label if s_label else label} file to SD Card" in story:
need_keypress(key_map['sd'])
elif way == "nfc":
if f"{key_map['nfc'] if is_q1 else '(3)'} to share via NFC" not in story:
pytest.skip("NFC disabled")
else:
need_keypress(key_map['nfc'])
time.sleep(0.2)
if is_json:
nfc_export = nfc_read_json()
elif way == "nfc":
if f"{key_map['nfc'] if is_q1 else '(3)'} to share via NFC" not in story:
pytest.skip("NFC disabled")
else:
nfc_export = nfc_read_text()
need_keypress(key_map['nfc'])
time.sleep(0.2)
if is_json:
nfc_export = nfc_read_json()
else:
nfc_export = nfc_read_text()
time.sleep(0.3)
press_cancel() # exit NFC animation
return nfc_export
elif way == "qr":
if 'file written' in story:
assert not is_q1
# mk4 only does QR if fits in normal QR, becaise it can't do BBQr
pytest.skip('no BBQr on Mk4')
need_keypress(key_map["qr"])
time.sleep(0.3)
press_cancel() # exit NFC animation
return nfc_export
elif way == "qr":
if 'file written' in story:
assert not is_q1
# mk4 only does QR if fits in normal QR, becaise it can't do BBQr
pytest.skip('no BBQr on Mk4')
need_keypress(key_map["qr"])
time.sleep(0.3)
try:
file_type, data = readback_bbqr()
if file_type == "J":
return json.loads(data)
elif file_type == "U":
return data.decode('utf-8') if not isinstance(data, str) else data
else:
raise NotImplementedError
except:
raise
res = cap_screen_qr().decode('ascii')
try:
return json.loads(res)
file_type, data = readback_bbqr()
if file_type == "J":
return json.loads(data)
elif file_type == "U":
return data.decode('utf-8') if not isinstance(data, str) else data
else:
raise NotImplementedError
except:
return res
else:
# virtual disk
if f"({key_map['vdisk']}) to save to Virtual Disk" not in story:
pytest.skip("Vdisk disabled")
raise
res = cap_screen_qr().decode('ascii')
try:
return json.loads(res)
except:
return res
else:
need_keypress(key_map['vdisk'])
# virtual disk
if f"({key_map['vdisk']}) to save to Virtual Disk" not in story:
pytest.skip("Vdisk disabled")
else:
need_keypress(key_map['vdisk'])
time.sleep(0.2)
title, story = cap_story()
@ -1865,6 +1877,8 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
if is_json:
export = json.loads(export)
garbage_collector.append(path)
press_select()
if ret_sig_addr and sig_addr:
@ -1909,7 +1923,7 @@ def tapsigner_encrypted_backup(microsd_path, virtdisk_path):
return doit
@pytest.fixture
def choose_by_word_length(need_keypress):
def choose_by_word_length(need_keypress, press_select):
# for use in seed XOR menu system
def doit(num_words):
if num_words == 12:
@ -1917,7 +1931,7 @@ def choose_by_word_length(need_keypress):
elif num_words == 18:
need_keypress("2")
else:
need_keypress("y")
press_select()
return doit
# workaround: need these fixtures to be global so I can call test from a test
@ -2151,6 +2165,9 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
elif af in ("p2wpkh", "p2wsh"):
target = "bc1q" if chain == "BTC" else "tb1q"
assert addr.startswith(target)
elif af == "p2tr":
target = "bc1p" if chain == "BTC" else "tb1p"
assert addr.startswith(target)
elif af in ("p2sh", "p2wpkh-p2sh", "p2wsh-p2sh"):
target = "3" if chain == "BTC" else "2"
assert addr.startswith(target)
@ -2194,6 +2211,30 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
return doit
@pytest.fixture
def validate_address():
# Check whether an address is covered by the given subkey
def doit(addr, sk):
if addr[0] in '1mn':
chain = "XTN" if addr[0] != "1" else "BTC"
assert addr == sk.address(addr_fmt="p2pkh", chain=chain)
elif addr[0:4] in {'bc1q', 'tb1q'}:
chain = "XTN" if addr[0:4] != 'bc1q' else "BTC"
assert addr == sk.address(addr_fmt="p2wpkh", chain=chain)
elif addr[0:6] == "bcrt1q":
assert addr == sk.address(addr_fmt="p2wpkh", chain="XRT")
elif addr[0:4] in {'bc1p', 'tb1p'}:
chain = "XTN" if addr[0:4] != 'bc1p' else "BTC"
assert addr == sk.address(addr_fmt="p2tr", chain=chain)
elif addr[0:6] == "bcrt1p":
assert addr == sk.address(addr_fmt="p2tr", chain="XRT")
elif addr[0] in '23':
chain = "XTN" if addr[0] != '3' else "BTC"
assert addr == sk.address(addr_fmt="p2sh-p2wpkh", chain=chain)
else:
raise ValueError(addr)
return doit
@pytest.fixture
def skip_if_useless_way(is_q1, nfc_disabled):
@ -2220,7 +2261,8 @@ def dev_core_import_object(dev):
ders = [
("m/44h/1h/0h", AF_CLASSIC),
("m/49h/1h/0h", AF_P2WPKH_P2SH),
("m/84h/1h/0h", AF_P2WPKH)
("m/84h/1h/0h", AF_P2WPKH),
("m/86h/1h/0h", AF_P2TR),
]
descriptors = []
for idx, (path, addr_format) in enumerate(ders):
@ -2239,6 +2281,15 @@ def dev_core_import_object(dev):
return descriptors
@pytest.fixture
def garbage_collector():
to_remove = []
yield to_remove
for pth in to_remove:
try:
os.remove(pth)
except: pass
# useful fixtures
from test_backup import backup_system
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll
@ -2248,6 +2299,7 @@ from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto
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_multisig import import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn
from test_miniscript import offer_minsc_import
from test_multisig import make_ms_address, clear_ms, make_myself_wallet, import_multisig
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
from test_seed_xor import restore_seed_xor

View File

@ -25,9 +25,11 @@ unmap_addr_fmt = {
'p2wsh': AF_P2WSH,
'p2wsh-p2sh': AF_P2WSH_P2SH,
'p2sh-p2wsh': AF_P2WSH_P2SH,
"p2tr": AF_P2TR,
}
msg_sign_unmap_addr_fmt = {
'p2tr': AF_P2TR, # not supported for msg signign tho
'p2pkh': AF_CLASSIC,
'p2wpkh': AF_P2WPKH,
'p2sh-p2wpkh': AF_P2WPKH_P2SH,
@ -35,6 +37,7 @@ msg_sign_unmap_addr_fmt = {
}
addr_fmt_names = {
AF_P2TR: 'p2tr',
AF_CLASSIC: 'p2pkh',
AF_P2SH: 'p2sh',
AF_P2WPKH: 'p2wpkh',
@ -45,10 +48,10 @@ addr_fmt_names = {
# all possible addr types, including multisig/scripts
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh']
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2tr']
# single-signer
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh']
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh', 'p2tr']
# multi signer
ADDR_STYLES_MS = ['p2sh', 'p2wsh', 'p2wsh-p2sh']

Binary file not shown.

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a075701133f173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc3475000000

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757011342173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc34751701aa000000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926315c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e1f80023202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926115c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e123202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6924214022cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094089756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb0000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094289756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb01010000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b093f89756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd430000

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757221602fe349064c98d6e2a853fa3c9b12bd8b304a19c195c60efa7ee2393046d3fa2321900772b2da75600008001000080000000800100000000000000000000

481
testing/descriptor.py Normal file
View File

@ -0,0 +1,481 @@
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# descriptor.py - Bitcoin Core's descriptors and their specialized checksums.
#
import struct
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2TR
MULTI_FMT_TO_SCRIPT = {
AF_P2SH: "sh(%s)",
AF_P2WSH_P2SH: "sh(wsh(%s))",
AF_P2WSH: "wsh(%s)",
AF_P2TR: "tr(%s)",
None: "wsh(%s)",
# hack for tests
"p2sh": "sh(%s)",
"p2sh-p2wsh": "sh(wsh(%s))",
"p2wsh-p2sh": "sh(wsh(%s))",
"p2wsh": "wsh(%s)",
"p2tr": "tr(%s)"
}
SINGLE_FMT_TO_SCRIPT = {
AF_P2WPKH: "wpkh(%s)",
AF_CLASSIC: "pkh(%s)",
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
AF_P2TR: "tr(%s)",
None: "wpkh(%s)",
"p2pkh": "pkh(%s)",
"p2wpkh": "wpkh(%s)",
"p2sh-p2wpkh": "sh(wpkh(%s))",
"p2wpkh-p2sh": "sh(wpkh(%s))",
"p2tr": "tr(%s)",
}
PROVABLY_UNSPENDABLE = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def xfp2str(xfp):
# Standardized way to show an xpub's fingerprint... it's a 4-byte string
# and not really an integer. Used to show as '0x%08x' but that's wrong endian.
return b2a_hex(struct.pack('<I', xfp)).decode().upper()
def str2xfp(txt):
# Inverse of xfp2str
return struct.unpack('<I', a2b_hex(txt))[0]
class WrongCheckSumError(Exception):
pass
def polymod(c, val):
c0 = c >> 35
c = ((c & 0x7ffffffff) << 5) ^ val
if (c0 & 1):
c ^= 0xf5dee51989
if (c0 & 2):
c ^= 0xa9fdca3312
if (c0 & 4):
c ^= 0x1bab10e32d
if (c0 & 8):
c ^= 0x3706b1677a
if (c0 & 16):
c ^= 0x644d626ffd
return c
def descriptor_checksum(desc):
c = 1
cls = 0
clscount = 0
for ch in desc:
pos = INPUT_CHARSET.find(ch)
if pos == -1:
raise ValueError(ch)
c = polymod(c, pos & 31)
cls = cls * 3 + (pos >> 5)
clscount += 1
if clscount == 3:
c = polymod(c, cls)
cls = 0
clscount = 0
if clscount > 0:
c = polymod(c, cls)
for j in range(0, 8):
c = polymod(c, 0)
c ^= 1
rv = ''
for j in range(0, 8):
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
return rv
def append_checksum(desc):
return desc + "#" + descriptor_checksum(desc)
def parse_desc_str(string):
"""Remove comments, empty lines and strip line. Produce single line string"""
res = ""
for l in string.split("\n"):
strip_l = l.strip()
if not strip_l:
continue
if strip_l.startswith("#"):
continue
res += strip_l
return res
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
if addr_fmt == AF_P2WSH_P2SH:
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
elif addr_fmt == AF_P2WSH:
descriptor_template = "wsh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2SH:
descriptor_template = "sh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2TR:
# provably unspendable BIP-0341
descriptor_template = "tr(" + PROVABLY_UNSPENDABLE + ",sortedmulti_a(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
class Descriptor:
__slots__ = (
"keys",
"addr_fmt",
)
def __init__(self, keys, addr_fmt):
self.keys = keys
self.addr_fmt = addr_fmt
@staticmethod
def checksum_check(desc_w_checksum , csum_required=False):
try:
desc, checksum = desc_w_checksum.split("#")
except ValueError:
if csum_required:
raise ValueError("Missing descriptor checksum")
return desc_w_checksum, None
calc_checksum = descriptor_checksum(desc)
if calc_checksum != checksum:
raise WrongCheckSumError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
return desc, checksum
@staticmethod
def parse_key_orig_info(key: str):
# key origin info is required for our MultisigWallet
close_index = key.find("]")
if key[0] != "[" or close_index == -1:
raise ValueError("Key origin info is required for %s" % (key))
key_orig_info = key[1:close_index] # remove brackets
key = key[close_index + 1:]
assert "/" in key_orig_info, "Malformed key derivation info"
return key_orig_info, key
@staticmethod
def parse_key_derivation_info(key: str):
invalid_subderiv_msg = "Invalid subderivation path - only 0/* or <0;1>/* allowed"
slash_split = key.split("/")
assert len(slash_split) > 1, invalid_subderiv_msg
if all(["h" not in elem and "'" not in elem for elem in slash_split[1:]]):
assert slash_split[-1] == "*", invalid_subderiv_msg
assert slash_split[-2] in ["0", "<0;1>", "<1;0>"], invalid_subderiv_msg
assert len(slash_split[1:]) == 2, invalid_subderiv_msg
return slash_split[0]
else:
raise ValueError("Cannot use hardened sub derivation path")
def checksum(self):
return descriptor_checksum(self._serialize())
def serialize_keys(self, internal=False, int_ext=False, keys=None):
to_do = keys if keys is not None else self.keys
result = []
for xfp, deriv, xpub in to_do:
if deriv[0] == "m":
# get rid of 'm'
deriv = deriv[1:]
elif deriv[0] != "/":
# input "84'/0'/0'" would lack slash separtor with xfp
deriv = "/" + deriv
if not isinstance(xfp, str):
xfp = xfp2str(xfp)
koi = xfp + deriv
# normalize xpub to use h for hardened instead of '
key_str = "[%s]%s" % (koi.lower(), xpub)
if int_ext:
key_str = key_str + "/" + "<0;1>" + "/" + "*"
else:
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
result.append(key_str.replace("'", "h"))
return result
def _serialize(self, internal=False, int_ext=False):
"""Serialize without checksum"""
assert len(self.keys) == 1 # "Multiple keys for single signature script"
desc_base = SINGLE_FMT_TO_SCRIPT[self.addr_fmt]
inner = self.serialize_keys(internal=internal, int_ext=int_ext)[0]
return desc_base % (inner)
def serialize(self, internal=False, int_ext=False):
"""Serialize with checksum"""
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
@classmethod
def parse(cls, desc_w_checksum):
# remove garbage
desc_w_checksum = parse_desc_str(desc_w_checksum)
# check correct checksum
desc, checksum = cls.checksum_check(desc_w_checksum)
# legacy
if desc.startswith("pkh("):
addr_fmt = AF_CLASSIC
tmp_desc = desc.replace("pkh(", "")
tmp_desc = tmp_desc.rstrip(")")
# native segwit
elif desc.startswith("wpkh("):
addr_fmt = AF_P2WPKH
tmp_desc = desc.replace("wpkh(", "")
tmp_desc = tmp_desc.rstrip(")")
# wrapped segwit
elif desc.startswith("sh(wpkh("):
addr_fmt = AF_P2WPKH_P2SH
tmp_desc = desc.replace("sh(wpkh(", "")
tmp_desc = tmp_desc.rstrip("))")
# wrapped segwit
elif desc.startswith("tr("):
addr_fmt = AF_P2TR
tmp_desc = desc.replace("tr(", "")
tmp_desc = tmp_desc.rstrip(")")
else:
raise ValueError("Unsupported descriptor. Supported: pkh(, wpkh(, sh(wpkh(.")
koi, key = cls.parse_key_orig_info(tmp_desc)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
@classmethod
def is_descriptor(cls, desc_str):
# Quick method to guess whether this is a descriptor
try:
temp = parse_desc_str(desc_str)
except:
return False
for prefix in ("pk(", "pkh(", "wpkh(", "tr(", "addr(", "raw(", "rawtr(", "combo(",
"sh(", "wsh(", "multi(", "sortedmulti(", "multi_a(", "sortedmulti_a("):
if temp.startswith(prefix):
return True
if prefix in temp:
# weaker case - needed for JSON wrapped imports
# if descriptor is invalid or unsuitable for our purpose
# we fail later (in parsing)
return True
return False
def bitcoin_core_serialize(self, external_label=None):
# this will become legacy one day
# instead use <0;1> descriptor format
res = []
for internal in [False, True]:
desc_obj = {
"desc": self.serialize(internal=internal),
"active": True,
"timestamp": "now",
"internal": internal,
"range": [0, 100],
}
if internal is False and external_label:
desc_obj["label"] = external_label
res.append(desc_obj)
return res
class MultisigDescriptor(Descriptor):
# only supprt with key derivation info
# only xpubs
# can be extended when needed
__slots__ = (
"M",
"N",
"internal_key",
"keys",
"addr_fmt",
"is_sorted" # whether to use sortedmulti() or multi()
)
def __init__(self, M, N, keys, addr_fmt, internal_key=None, is_sorted=True):
self.M = M
self.N = N
self.internal_key = is_sorted
self.is_sorted = is_sorted
super().__init__(keys, addr_fmt)
@classmethod
def parse(cls, desc_w_checksum):
internal_key = None
# remove garbage
desc_w_checksum = parse_desc_str(desc_w_checksum)
# check correct checksum
desc, checksum = cls.checksum_check(desc_w_checksum)
is_sorted = "sortedmulti(" in desc
rplc = "sortedmulti(" if is_sorted else "multi("
# wrapped segwit
if desc.startswith("sh(wsh("+rplc):
addr_fmt = AF_P2WSH_P2SH
tmp_desc = desc.replace("sh(wsh("+rplc, "")
tmp_desc = tmp_desc.rstrip(")))")
# native segwit
elif desc.startswith("wsh("+rplc):
addr_fmt = AF_P2WSH
tmp_desc = desc.replace("wsh("+rplc, "")
tmp_desc = tmp_desc.rstrip("))")
# legacy
elif desc.startswith("sh("+rplc):
addr_fmt = AF_P2SH
tmp_desc = desc.replace("sh("+rplc, "")
tmp_desc = tmp_desc.rstrip("))")
elif desc.startswith("tr("):
addr_fmt = AF_P2TR
tmp_desc = desc.replace("tr(", "")
tmp_desc = tmp_desc.rstrip(")")
internal_key, tmp_desc = tmp_desc.split(",", 1)
tmp_desc = tmp_desc.replace(rplc + "_a(", "")
tmp_desc = tmp_desc.rstrip(")")
try:
koi, key = cls.parse_key_orig_info(internal_key)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
internal_key = (xfp, origin_deriv, xpub)
except ValueError:
# https://github.com/BlockstreamResearch/secp256k1-zkp/blob/11af7015de624b010424273be3d91f117f172c82/src/modules/rangeproof/main_impl.h#L16
# H = lift_x(0x0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0)
# if internal_key == PROVABLY_UNSPENDABLE:
# # unspendable H as defined in BIP-0341
# pass
# else:
# assert "r=" in internal_key
# _, r = internal_key.split("=")
# if r == "@":
# # 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 = a2b_hex(PROVABLY_UNSPENDABLE)
# H_xo = ngu.secp256k1.xonly_pubkey(H)
# internal_key = H_xo.tweak_add(kp.xonly_pubkey().to_bytes())
# internal_key = b2a_hex(internal_key.to_bytes()).decode()
pass
else:
raise ValueError("Unsupported descriptor. Supported: sh(), sh(wsh()), wsh().")
splitted = tmp_desc.split(",")
M, keys = int(splitted[0]), splitted[1:]
N = int(len(keys))
if M > N:
raise ValueError("M must be <= N: got M=%d and N=%d" % (M, N))
res_keys = []
for key in keys:
koi, key = cls.parse_key_orig_info(key)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
res_keys.append((xfp, origin_deriv, xpub))
return cls(M=M, N=N, keys=res_keys, addr_fmt=addr_fmt,
internal_key=internal_key,is_sorted=is_sorted)
def _serialize(self, internal=False, int_ext=False):
"""Serialize without checksum"""
desc_base = MULTI_FMT_TO_SCRIPT[self.addr_fmt]
if self.addr_fmt == AF_P2TR:
if isinstance(self.internal_key, str):
desc_base = desc_base % (self.internal_key + ",sortedmulti_a(%s)")
else:
ik_ser = self.serialize_keys(keys=[self.internal_key])[0]
desc_base = desc_base % (ik_ser + ",sortedmulti_a(%s)")
_type = "sortedmulti" if self.is_sorted else "multi"
_type += "(%s)"
desc_base = desc_base % _type
assert len(self.keys) == self.N
inner = str(self.M) + "," + ",".join(
self.serialize_keys(internal=internal, int_ext=int_ext))
return desc_base % (inner)
def pretty_serialize(self):
"""Serialize in pretty and human-readable format"""
_type = "sortedmulti" if self.is_sorted else "multi"
res = "# Coldcard descriptor export\n"
if self.is_sorted:
res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n"
else:
res += ("# !!! DANGER: order of keys in descriptor MUST be preserved. "
"Correct order of keys is required to compose valid redeem/witness script.\n")
if self.addr_fmt == AF_P2SH:
res += "# bare multisig - p2sh\n"
res += "sh("+_type+"(\n%s\n))"
# native segwit
elif self.addr_fmt == AF_P2WSH:
res += "# native segwit - p2wsh\n"
res += "wsh("+_type+"(\n%s\n))"
# wrapped segwit
elif self.addr_fmt == AF_P2WSH_P2SH:
res += "# wrapped segwit - p2sh-p2wsh\n"
res += "sh(wsh(" + _type + "(\n%s\n)))"
elif self.addr_fmt == AF_P2TR:
inner_ident = 2
res += "# taproot multisig - p2tr\n"
res += "tr(\n"
if isinstance(self.internal_key, str):
res += "\t" + "# internal key (provably unspendable)\n"
res += "\t" + self.internal_key + ",\n"
res += "\t" + _type + "_a(\n%s\n))"
else:
ik_ser = self.serialize_keys(keys=[self.internal_key])[0]
res += "\t" + "# internal key\n"
res += "\t" + ik_ser + ",\n"
res += "\t" + _type + "_a(\n%s\n))"
else:
raise ValueError("Malformed descriptor")
assert len(self.keys) == self.N
inner = "\t" + "# %d of %d (%s)\n" % (
self.M, self.N,
"requires all participants to sign" if self.M == self.N else "threshold")
inner += "\t" + str(self.M) + ",\n"
ser_keys = self.serialize_keys()
for i, key_str in enumerate(ser_keys, start=1):
if i == self.N:
inner += "\t" + key_str
else:
inner += "\t" + key_str + ",\n"
checksum = self.serialize().split("#")[1]
return (res % inner) + "#" + checksum
# EOF

View File

@ -23,6 +23,7 @@ if not pa.is_secret_blank():
pa.login()
assert pa.is_secret_blank()
settings.blank()
SettingsObject.master_sv_data = {}
SettingsObject.master_nvram_key = None

View File

@ -0,0 +1,13 @@
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# quickly clear all miniscript wallets installed
from glob import settings
from ux import restore_menu
if settings.get('miniscript'):
del settings.current['miniscript']
settings.save()
print("cleared miniscript")
restore_menu()

View File

@ -24,13 +24,15 @@ def prandom(count):
return bytes(random.randint(0, 255) for i in range(count))
def taptweak(internal_key, tweak=None):
tweak = internal_key if tweak is None else internal_key + tweak
assert len(internal_key) == 32, "not xonly-pubkey (len!=32)"
if tweak is not None:
assert len(tweak) == 32, "tweak (len!=32)"
tweak = internal_key if tweak is None else internal_key + tweak
xonly_pubkey = xonly_pubkey_parse(internal_key)
tweak = tagged_sha256(b"TapTweak", tweak)
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_pubkey, tweak)
tweaked_xonnly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonnly_pubkey)
tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonly_pubkey)
def fake_dest_addr(style='p2pkh'):
# Make a plausible output address, but it's random garbage. Cant use for change outs

View File

@ -123,6 +123,9 @@ class BasicPSBTInput(PSBTSection):
self.taproot_bip32_paths = {}
self.taproot_internal_key = None
self.taproot_key_sig = None
self.taproot_merkle_root = None
self.taproot_scripts = {}
self.taproot_script_sigs = {}
self.redeem_script = None
self.witness_script = None
self.previous_txid = None # v2
@ -147,6 +150,9 @@ class BasicPSBTInput(PSBTSection):
a.taproot_key_sig == b.taproot_key_sig and \
a.taproot_bip32_paths == b.taproot_bip32_paths and \
a.taproot_internal_key == b.taproot_internal_key and \
a.taproot_merkle_root == b.taproot_merkle_root and \
a.taproot_scripts == b.taproot_scripts and \
a.taproot_script_sigs == b.taproot_script_sigs and \
sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys()) and \
a.previous_txid == b.previous_txid and \
a.prevout_idx == b.prevout_idx and \
@ -189,7 +195,7 @@ class BasicPSBTInput(PSBTSection):
self.others[kt] = val
elif kt == PSBT_IN_TAP_BIP32_DERIVATION:
self.taproot_bip32_paths[key] = val
elif kt == PSBT_OUT_TAP_INTERNAL_KEY:
elif kt == PSBT_IN_TAP_INTERNAL_KEY:
self.taproot_internal_key = val
elif kt == PSBT_IN_TAP_KEY_SIG:
self.taproot_key_sig = val
@ -203,6 +209,21 @@ class BasicPSBTInput(PSBTSection):
self.req_time_locktime = struct.unpack("<I", val)[0]
elif kt == PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
self.req_height_locktime = struct.unpack("<I", val)[0]
elif kt == PSBT_IN_TAP_SCRIPT_SIG:
assert len(key) == 64, "PSBT_IN_TAP_SCRIPT_SIG key length != 64"
assert len(val) in (64, 65), "PSBT_IN_TAP_SCRIPT_SIG signature length != 64 or 65"
xonly_pubkey, script_hash = key[:32], key[32:]
self.taproot_script_sigs[(xonly_pubkey, script_hash)] = val
elif kt == PSBT_IN_TAP_LEAF_SCRIPT:
assert len(key) > 32, "PSBT_IN_TAP_LEAF_SCRIPT control block is too short"
assert (len(key) - 1) % 32 == 0, "PSBT_IN_TAP_LEAF_SCRIPT control block is not valid"
assert len(val) != 0, "PSBT_IN_TAP_LEAF_SCRIPT cannot be empty"
leaf_script = (val[:-1], int(val[-1]))
if leaf_script not in self.taproot_scripts:
self.taproot_scripts[leaf_script] = set()
self.taproot_scripts[leaf_script].add(key)
elif kt == PSBT_IN_TAP_MERKLE_ROOT:
self.taproot_merkle_root = val
else:
self.unknown[bytes([kt]) + key] = val
@ -236,6 +257,16 @@ class BasicPSBTInput(PSBTSection):
if self.taproot_key_sig:
wr(PSBT_IN_TAP_KEY_SIG, self.taproot_key_sig)
if self.taproot_merkle_root:
wr(PSBT_IN_TAP_MERKLE_ROOT, self.taproot_merkle_root)
if self.taproot_scripts:
for (script, leaf_ver), control_blocks in self.taproot_scripts.items():
for control_block in control_blocks:
wr(PSBT_IN_TAP_LEAF_SCRIPT, script + struct.pack("B", leaf_ver), control_block)
if self.taproot_script_sigs:
for (xonly, leaf_hash), sig in self.taproot_script_sigs.items():
wr(PSBT_IN_TAP_SCRIPT_SIG, sig, xonly + leaf_hash)
if v2:
if self.previous_txid is not None:
wr(PSBT_IN_PREVIOUS_TXID, self.previous_txid)
@ -267,6 +298,7 @@ class BasicPSBTOutput(PSBTSection):
self.bip32_paths = {}
self.taproot_bip32_paths = {}
self.taproot_internal_key = None
self.taproot_tree = None
self.script = None # v2
self.amount = None # v2
self.proprietary = {}
@ -282,6 +314,7 @@ class BasicPSBTOutput(PSBTSection):
a.taproot_bip32_paths == b.taproot_bip32_paths and \
a.taproot_internal_key == b.taproot_internal_key and \
a.proprietary == b.proprietary and \
a.taproot_tree == b.taproot_tree and \
a.unknown == b.unknown
def parse_kv(self, kt, key, val):
@ -297,6 +330,18 @@ class BasicPSBTOutput(PSBTSection):
self.taproot_bip32_paths[key] = val
elif kt == PSBT_OUT_TAP_INTERNAL_KEY:
self.taproot_internal_key = val
elif kt == PSBT_OUT_TAP_TREE:
res = []
reader = io.BytesIO(val)
while True:
depth = reader.read(1)
if not depth:
break
leaf_version = reader.read(1)[0]
script_len = deser_compact_size(reader)
script = reader.read(script_len)
res.append((depth[0], leaf_version, script))
self.taproot_tree = res
elif kt == PSBT_OUT_SCRIPT:
self.script = val
elif kt == PSBT_OUT_AMOUNT:
@ -319,6 +364,11 @@ class BasicPSBTOutput(PSBTSection):
wr(PSBT_OUT_TAP_BIP32_DERIVATION, self.taproot_bip32_paths[k], k)
if self.taproot_internal_key:
wr(PSBT_OUT_TAP_INTERNAL_KEY, self.taproot_internal_key)
if self.taproot_tree:
res = b''
for depth, leaf_version, script in self.taproot_tree:
res += bytes([depth, leaf_version]) + ser_compact_size(len(script)) + script
wr(PSBT_OUT_TAP_TREE, res)
if v2 and self.script is not None:
wr(PSBT_OUT_SCRIPT, self.script)
if v2 and self.amount is not None:

View File

@ -12,7 +12,7 @@ from charcodes import KEY_QR
from constants import msg_sign_unmap_addr_fmt
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'"])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR ])
def test_show_addr_usb(dev, press_select, addr_vs_path, path, addr_fmt, is_simulator):
addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
@ -27,7 +27,7 @@ def test_show_addr_usb(dev, press_select, addr_vs_path, path, addr_fmt, is_simul
@pytest.mark.qrcode
@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 ])
@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,
press_cancel, is_q1):
@ -60,25 +60,40 @@ def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
assert qr == addr or qr == addr.upper()
@pytest.mark.bitcoind
def test_addr_vs_bitcoind(use_regtest, press_select, dev, bitcoind_d_sim_sign):
@pytest.mark.parametrize("addr_fmt", [
(AF_CLASSIC, "legacy"),
(AF_P2WPKH_P2SH, "p2sh-segwit"),
(AF_P2WPKH, "bech32"),
(AF_P2TR, "bech32m")
])
def test_addr_vs_bitcoind(addr_fmt, use_regtest, press_select, dev, bitcoind_d_sim_sign):
# check our p2wpkh wrapped in p2sh is right
use_regtest()
addr_fmt, addr_fmt_bitcoind = addr_fmt
for i in range(5):
core_addr = bitcoind_d_sim_sign.getnewaddress(f"{i}-addr", "p2sh-segwit")
assert core_addr[0] == '2'
core_addr = bitcoind_d_sim_sign.getnewaddress(f"{i}-addr", addr_fmt_bitcoind)
resp = bitcoind_d_sim_sign.getaddressinfo(core_addr)
assert resp['embedded']['iswitness'] == True
assert resp['isscript'] == True
assert resp["ismine"] is True
if addr_fmt in (AF_P2TR, AF_P2WPKH):
wit_ver = resp["witness_version"]
if addr_fmt == AF_P2TR:
assert wit_ver == 1
else:
assert wit_ver == 0
assert resp["iswitness"] is True
if addr_fmt == AF_P2WPKH_P2SH:
assert resp['embedded']['iswitness'] is True
assert resp['isscript'] is True
assert resp['embedded']['witness_version'] == 0
path = resp['hdkeypath']
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2WPKH_P2SH), timeout=None)
addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
press_select()
assert addr == core_addr
@pytest.mark.parametrize("body_err", [
("m\np2wsh", "Invalid address format: 'p2wsh'"),
("m\np2sh-p2wsh", "Invalid address format: 'p2sh-p2wsh'"),
("m\np2tr", "Invalid address format: 'p2tr'"),
("m\np2wsh", "Unsupported address format: 'p2wsh'"),
("m\np2sh-p2wsh", "Unsupported address format: 'p2sh-p2wsh'"),
("m/0/0/0/0/0/0/0/0/0/0/0/0/0\np2pkh", "too deep"),
("m/0/0/0/0/0/q/0/0/0\np2pkh", "invalid characters"),
])
@ -94,7 +109,7 @@ def test_show_addr_nfc_invalid(body_err, goto_home, pick_menu_item, nfc_write_te
assert err in story
@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"])
@pytest.mark.parametrize("str_addr_fmt", ["p2pkh", "", "p2wpkh", "p2wpkh-p2sh", "p2sh-p2wpkh", "p2tr"])
def test_show_addr_nfc(path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_menu_item,
goto_home, cap_story, press_nfc, addr_vs_path, press_select, is_q1,
cap_screen):
@ -142,4 +157,59 @@ def test_show_addr_nfc(path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_m
assert story_addr == addr
addr_vs_path(addr, path, addr_fmt)
# EOF
def test_bip86(dev, set_seed_words, use_mainnet, need_keypress):
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
set_seed_words(mnemonic)
use_mainnet()
path = "m/86'/0'/0'"
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk"
xpub = "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"
assert xp == xpub
# Account 0, first receiving
path = "m/86'/0'/0'/0/0"
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2TR), timeout=None)
need_keypress('y')
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xprvA449goEeU9okwCzzZaxiy475EQGQzBkc65su82nXEvcwzfSskb2hAt2WymrjyRL6kpbVTGL3cKtp9herYXSjjQ1j4stsXXiRF7kXkCacK3T"
xpub = "xpub6H3W6JmYJXN49h5TfcVjLC3onS6uPeUTTJoVvRC8oG9vsTn2J8LwigLzq5tHbrwAzH9DGo6ThGUdWsqce8dGfwHVBxSbixjDADGGdzF7t2B"
# internal_key = "cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"
# output_key = "a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"
# scriptPubKey = "5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"
address = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
assert xp == xpub
assert addr == address
# Account 0, second receiving
path = "m/86'/0'/0'/0/1"
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2TR), timeout=None)
need_keypress('y')
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xxprvA449goEeU9okyiF1LmKiDaTgeXvmh87DVyRd35VPbsSop8n8uALpbtrUhUXByPFKK7C2yuqrB1FrhiDkEMC4RGmA5KTwsE1aB5jRu9zHsuQ"
xpub = "xpub6H3W6JmYJXN4CCKUSnriaiQRCZmG6aq4sCMDqTu1ACyngw7HShf59hAxYjXgKDuuHThVEUzdHrc3aXCr9kfvQvZPit5dnD3K9xVRBzjK3rX"
# internal_key = "83dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145"
# output_key = "a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"
# scriptPubKey = "5120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"
address = "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
assert xp == xpub
assert addr == address
# Account 0, first change
path = "m/86'/0'/0'/1/0"
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2TR), timeout=None)
need_keypress('y')
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xprvA3Ln3Gt3aphvUgzgEDT8vE2cYqb4PjFfpmbiFKphxLg1FjXQpkAk5M1ZKDY15bmCAHA35jTiawbFuwGtbDZogKF1WfjwxML4gK7WfYW5JRP"
xpub = "xpub6GL8SnQwRCGDhB59LEz9HMyM6sRYoByXBzXK3iEKWgCz8XrZNHUzd9L3AUBELW5NzA7dEFvMas1F84TuPH3xqdUA5tumaGWFgihJzWytXe3"
# internal_key = "399f1b2f4393f29a18c937859c5dd8a77350103157eb880f02e8c08214277cef"
# output_key = "882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"
# scriptPubKey = "5120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"
address = "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"
assert xp == xpub
assert addr == address
# EOF

View File

@ -24,9 +24,10 @@ def mk_common_derivations():
# Removed in v4.1.3: ( "m/{change}/{idx}", AF_CLASSIC ),
#( "m/{account}'/{change}'/{idx}'", AF_CLASSIC ),
#( "m/{account}'/{change}'/{idx}'", AF_P2WPKH ),
( "m/44h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_CLASSIC ),
( "m/49h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH_P2SH ),
( "m/84h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH )
("m/44h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_CLASSIC),
("m/49h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH_P2SH),
("m/84h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH),
("m/86h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2TR),
]
return doit
@ -57,32 +58,14 @@ def parse_display_screen(cap_story, is_mark3):
return d
return doit
@pytest.fixture
def validate_address():
# Check whether an address is covered by the given subkey
def doit(addr, sk):
if addr[0] in '1mn':
assert addr == sk.address()
elif addr[0:3] in { 'bc1', 'tb1' }:
h20 = sk.hash160()
assert addr == bech32.encode(addr[0:2], 0, h20)
elif addr[0:5] == "bcrt1":
h20 = sk.hash160()
assert addr == bech32.encode(addr[0:4], 0, h20)
elif addr[0] in '23':
h20 = hash160(b'\x00\x14' + sk.hash160())
assert h20 == decode_base58_checksum(addr)[1:]
else:
raise ValueError(addr)
return doit
@pytest.fixture
def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, microsd_path,
virtdisk_path, nfc_read_text, load_export_and_verify_signature,
press_select, press_nfc):
press_select, press_nfc, load_export):
# Generates the address file through the simulator, reads the file and
# returns a list of tuples of the form (subpath, address)
def doit(start_idx=0, way="sd", change=False, is_custom_single=False):
def doit(start_idx=0, way="sd", change=False, is_custom_single=False, is_p2tr=False):
expected_qty = 250 if way != "nfc" else 10
if (start_idx + expected_qty) > MAX_BIP32_IDX:
expected_qty = (MAX_BIP32_IDX - start_idx) + 1
@ -92,7 +75,8 @@ def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, mic
if change and not is_custom_single:
need_keypress("0")
if way == "sd":
need_keypress('1')
if "Press (1)" in story:
need_keypress('1')
elif way == "vdisk":
if "save to Virtual Disk" not in story:
raise pytest.skip("Vdisk disabled")
@ -110,9 +94,16 @@ def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, mic
assert len(addresses.split("\n")) == expected_qty
raise pytest.xfail("PASSED - different export format for NFC")
time.sleep(.5) # always long enough to write the file?
title, body = cap_story()
contents, sig_addr = load_export_and_verify_signature(body, way, label="Address summary")
if is_p2tr:
# p2tr - no signature file
contents = load_export(way, label="Address summary", is_json=False,
sig_check=False, skip_query=True)
sig_addr = None
else:
time.sleep(.5) # always long enough to write the file?
title, body = cap_story()
contents, sig_addr = load_export_and_verify_signature(body, way, label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
@ -120,7 +111,8 @@ def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, mic
for n, (idx, addr, deriv) in enumerate(cc, start=start_idx):
assert int(idx) == n
if n == start_idx:
assert sig_addr == addr
if sig_addr:
assert sig_addr == addr
if not is_custom_single:
assert ('/%s' % idx) in deriv
@ -272,7 +264,7 @@ def test_address_display(goto_address_explorer, parse_display_screen, mk_common_
press_cancel() # back
@pytest.mark.parametrize('click_idx', ["Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH"])
@pytest.mark.parametrize('click_idx', ["Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH", 'Taproot P2TR'])
@pytest.mark.parametrize("change", [True, False])
@pytest.mark.parametrize("start_idx", [MAX_BIP32_IDX, 80965, 0])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
@ -289,7 +281,8 @@ def test_dump_addresses(way, change, generate_addresses_file, mk_common_derivati
set_addr_exp_start_idx(start_idx)
pick_menu_item(click_idx)
# Generate the addresses file and get each line in a list
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx, change=change):
is_p2tr = click_idx == 'Taproot P2TR'
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx, change=change, is_p2tr=is_p2tr):
# derive the subkey and validate the corresponding address
assert subpath.split("/")[-2] == "1" if change else "0"
sk = node_prv.subkey_for_path(subpath)
@ -336,7 +329,7 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
# derive index=0 address
assert '{account}' in path
subpath = path.format(account=account_num, change=0, idx=start_idx) # e.g. "m/44'/1'/X'/0/0"
subpath = path.format(account=account_num, change=0, idx=start_idx, is_p2tr=addr_format==AF_P2TR) # e.g. "m/44'/1'/X'/0/0"
sk = node_prv.subkey_for_path(subpath)
# capture full index=0 address from display screen & validate it
@ -357,7 +350,7 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
assert expected_addr.startswith(start)
assert expected_addr.endswith(end)
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx):
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx,is_p2tr=addr_format==AF_P2TR):
assert subpath.split('/')[-3] == str(account_num)+"h"
sk = node_prv.subkey_for_path(subpath)
validate_address(addr, sk)
@ -378,7 +371,7 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
("m/1/2/3/4/5", MAX_BIP32_IDX),
("m/1h/2h/3h/4h/5h", 0),
])
@pytest.mark.parametrize('which_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ])
@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,
@ -445,12 +438,14 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
m = cap_menu()
assert m[0] == 'Classic P2PKH'
assert m[1] == 'Segwit P2WPKH'
assert m[2] == 'P2SH-Segwit'
assert m[2] == 'Taproot P2TR'
assert m[3] == 'P2SH-Segwit'
fmts = {
AF_CLASSIC: 'Classic P2PKH',
AF_P2WPKH: 'Segwit P2WPKH',
AF_P2WPKH_P2SH: 'P2SH-Segwit',
AF_P2TR: 'Taproot P2TR',
}
pick_menu_item(fmts[which_fmt])
@ -479,7 +474,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
need_keypress(KEY_QR if is_q1 else '4')
qr = cap_screen_qr().decode('ascii')
if which_fmt == AF_P2WPKH:
if which_fmt in (AF_P2WPKH, AF_P2TR):
assert qr == addr.upper()
else:
assert qr == addr
@ -501,7 +496,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
# remove QR from screen
press_cancel()
addr_gen = generate_addresses_file(change=False, is_custom_single=True)
addr_gen = generate_addresses_file(change=False, is_custom_single=True, is_p2tr=which_fmt == AF_P2TR)
f_path, f_addr = next(addr_gen)
assert f_path == path
assert f_addr == addr
@ -529,7 +524,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
need_keypress(KEY_QR if is_q1 else '4')
for i in range(n):
qr = cap_screen_qr().decode('ascii')
if which_fmt == AF_P2WPKH:
if which_fmt in (AF_P2WPKH, AF_P2TR):
qr = qr.lower()
qr_addr_list.append(qr)
need_keypress(KEY_RIGHT if is_q1 else "9")
@ -542,11 +537,92 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
assert sorted(qr_addr_list) == sorted(addr_dict.values())
addr_gen = generate_addresses_file(start_idx=start_idx, change=False)
addr_gen = generate_addresses_file(start_idx=start_idx, change=False, is_p2tr=which_fmt==AF_P2TR)
assert addr_dict == {p: a for i,(p, a) in enumerate(addr_gen) if i < n}
# check the rest of file export
for p, a in addr_gen:
addr_vs_path(a, p, addr_fmt=which_fmt)
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC, AF_P2TR])
@pytest.mark.parametrize("acct_num", [None, "999"])
def test_bitcoind_descriptor_address(addr_fmt, acct_num, bitcoind, goto_home, pick_menu_item, cap_story,
use_regtest, need_keypress, microsd_path, generate_addresses_file,
bitcoind_d_wallet_w_sk, load_export, settings_set, cap_menu,
goto_address_explorer, press_cancel, press_select, enter_number):
# export single sig descriptors (external, internal)
# export addressses from address explorer
# derive addresses from descriptor with bitcoind
# compare bitcoind derived addressses with those exported from address explorer
bitcoind = bitcoind_d_wallet_w_sk
use_regtest()
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Export Wallet")
pick_menu_item("Descriptor")
time.sleep(.1)
_, story = cap_story()
assert "This saves a ranged xpub descriptor" in story
assert "Press (1) to enter a non-zero account number" in story
assert "sensitive--in terms of privacy" in story
assert "not compromise your funds directly" in story
if isinstance(acct_num, str):
need_keypress("1") # chosse account number
for ch in acct_num:
need_keypress(ch) # input num
press_select() # confirm selection
else:
press_select() # confirm story
time.sleep(.1)
_, story = cap_story()
assert "press (1) to export receiving and change descriptors separately" in story
need_keypress("1")
sig_check = True
if addr_fmt == AF_P2WPKH:
menu_item = "Segwit P2WPKH"
desc_prefix = "wpkh("
elif addr_fmt == AF_P2WPKH_P2SH:
menu_item = "P2SH-Segwit"
desc_prefix = "sh(wpkh("
elif addr_fmt == AF_P2TR:
menu_item = "Taproot P2TR"
desc_prefix = "tr("
sig_check = False
else:
# addr_fmt == AF_CLASSIC:
menu_item = "Classic P2PKH"
desc_prefix = "pkh("
pick_menu_item(menu_item)
contents = load_export("sd", label="Descriptor", is_json=False, addr_fmt=addr_fmt,
sig_check=sig_check)
descriptors = contents.strip()
ext_desc, int_desc = descriptors.split("\n")
assert ext_desc.startswith(desc_prefix)
assert int_desc.startswith(desc_prefix)
# check both external and internal
for chng in [False, True]:
goto_address_explorer()
if acct_num:
menu = cap_menu()
# can be "Account number" or "Account: N"
mi = [m for m in menu if "Account" in m]
assert len(mi) == 1
pick_menu_item(mi[0])
enter_number(acct_num)
desc = int_desc if chng else ext_desc
settings_set("axi", 0)
pick_menu_item(menu_item)
cc_addrs_gen = generate_addresses_file(change=chng, is_p2tr=addr_fmt == AF_P2TR)
cc_addrs = [addr for deriv, addr in cc_addrs_gen]
bitcoind_addrs = bitcoind.deriveaddresses(desc, [0, 249])
assert cc_addrs == bitcoind_addrs
# EOF

1667
testing/test_bsms.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,20 +14,24 @@ wordlist = Mnemonic('english').wordlist
@pytest.fixture
def try_decode(sim_exec):
def doit(arg):
def doit(arg, ):
cmd = "from decoders import decode_qr_result; " + \
f"RV.write(repr(decode_qr_result({arg!r})))"
result = sim_exec(cmd)
if 'Traceback' in result:
raise RuntimeError(result)
if '<' in result:
# objects, like "<HexStreamer..."
result = result.replace('<', "'").replace('>', "'")
try:
return eval(result)
except SyntaxError:
if '<' in result:
# objects, like "<HexStreamer..."
result = result.replace('<', "'").replace('>', "'")
return eval(result)
raise
return eval(result)
return doit
@pytest.mark.parametrize('fname,expect', [
@ -145,7 +149,6 @@ def test_urldecode(url, sim_exec):
@pytest.mark.parametrize('config', [
'wsh(sortedmulti(2,[0f056943/48h/1h/0h/2h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[6ba6cfd0/48h/1h/0h/2h]tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm/0/*,[747b698e/48h/1h/0h/2h]tpubDExj5FnaUnPAn7sHGUeBqD3buoNH5dqmjAT6884vbDpH1iDYWigb7kFo2cA97dc8EHb54u13TRcZxC4kgRS9gc3Ey2xc8c5urytEzTcp3ac/0/*,[7bb026be/48h/1h/0h/2h]tpubDFiuHYSJhNbHcbLJoxWdbjtUcbKR6PvLq53qC1Xq6t93CrRx78W3wcng8vJyQnY3giMJZEgNCRVzTojLb8RqPFpW5Ms2dYpjcJYofN1joyu/0/*))#al5z7mcj',
'0f056943: tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n6ba6cfd0: tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
'0f056943: xpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n6ba6cfd0: tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
' 0F056943 : tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n 6BA6CFD0 : tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
@ -163,6 +166,18 @@ def test_multisig(config, try_decode):
assert ft == "multi"
assert vals[0] == config
@pytest.mark.parametrize('desc', [
'wsh(sortedmulti(2,[0f056943/48h/1h/0h/2h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[6ba6cfd0/48h/1h/0h/2h]tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm/0/*,[747b698e/48h/1h/0h/2h]tpubDExj5FnaUnPAn7sHGUeBqD3buoNH5dqmjAT6884vbDpH1iDYWigb7kFo2cA97dc8EHb54u13TRcZxC4kgRS9gc3Ey2xc8c5urytEzTcp3ac/0/*,[7bb026be/48h/1h/0h/2h]tpubDFiuHYSJhNbHcbLJoxWdbjtUcbKR6PvLq53qC1Xq6t93CrRx78W3wcng8vJyQnY3giMJZEgNCRVzTojLb8RqPFpW5Ms2dYpjcJYofN1joyu/0/*))#al5z7mcj',
'wsh(or_d(pk([5155f1fa/44h/1h/0h]tpubDCtts5PqRUpJZaRegaWEGTULHp9XbFVsmrxQ38bAXf291HfmnTuDdeeXgyi59ywvRzaAmE8hiFZMVEv7KyGnH5YVBK3SDK625Huv4uoTsWZ/<0;1>/*),and_v(v:pkh([0f056943/84h/0h/0h]tpubDCx8y86cKonoPyTtj3f9NZLpBYoBNkbAzUdafMHhggjxkhF8Dny2aekWfDafywEMZEQaQjkK9Gxn7aN7usLRUQdYbvDgcnmYRf72khPEouL/<0;1>/*),older(5))))#sraf9nwn',
'wsh(or_d(pk([d7beb757/44h/1h/0h]tpubDCKMUppLh1DJkSgbp9dmKaMwHyBQwrmLzxgwz8J7obXnFEaWneGyMZymyLra1PBjDyqBUE9JmPVyn33QCgXwkeAniz3LCXXTpw8YFe6edjk/0/*),and_v(v:pkh([0f056943/84h/0h/0h]tpubDCx8y86cKonoPyTtj3f9NZLpBYoBNkbAzUdafMHhggjxkhF8Dny2aekWfDafywEMZEQaQjkK9Gxn7aN7usLRUQdYbvDgcnmYRf72khPEouL/<0;1>/*),older(5))))',
'{"name":"a","desc":"wsh(or_d(pk([d7beb757/44h/1h/0h]tpubDCKMUppLh1DJkSgbp9dmKaMwHyBQwrmLzxgwz8J7obXnFEaWneGyMZymyLra1PBjDyqBUE9JmPVyn33QCgXwkeAniz3LCXXTpw8YFe6edjk/0/*),and_v(v:pkh([0f056943/84h/0h/0h]tpubDCx8y86cKonoPyTtj3f9NZLpBYoBNkbAzUdafMHhggjxkhF8Dny2aekWfDafywEMZEQaQjkK9Gxn7aN7usLRUQdYbvDgcnmYRf72khPEouL/<0;1>/*),older(5))))"}',
])
def test_miniscript_descriptors(desc, try_decode):
# includes multisig
ft, vals = try_decode(desc)
assert ft == "minisc"
assert vals[0] == desc
@pytest.mark.parametrize('data', [
('5J9Gfy2FNTw2EpkkQu41S9CTBBVij123kYPkbYAnaQkUHtMuv2Q', False, False),
('L2TgtddYM9ueK2auJVkNaNEF3egMMK1MTMkng5RBAcBWXnCMnxcb', True, False),

View File

@ -256,7 +256,7 @@ def confirm_tmp_seed(need_keypress, cap_story, press_select):
@pytest.fixture
def seed_vault_delete(pick_menu_item, need_keypress, cap_menu, cap_story,
goto_home, press_select):
goto_home, press_select, settings_get):
def doit(xfp, wipe=True):
# delete it from records
goto_home()
@ -276,12 +276,17 @@ def seed_vault_delete(pick_menu_item, need_keypress, cap_menu, cap_story,
title, story = cap_story()
assert "Remove" in story
assert xfp in title
assert "press (1)" in story
if wipe:
press_select()
else:
# preserve settings - remove just from seed vaul
need_keypress("1")
if xfp2str(settings_get("xfp")) == xfp:
assert "press (1)" not in story
press_select() # will NOT wipe settings
else:
assert "press (1)" in story
# preserve settings - remove just from seed vaul
need_keypress("1")
time.sleep(.1)
goto_home()
@ -1117,16 +1122,21 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item
m = cap_menu()
assert m[0] == "AAA"
pick_menu_item("Delete")
time.sleep(.1)
title, story = cap_story()
# current active does not offer to purge the slot, only to remove from Seed Vault
assert "delete its settings?" not in story
press_select()
time.sleep(.1)
goto_home()
m = cap_menu()
# after we delete from seed vault together with its settings
# we're back to master secret
assert m[0] == "Ready To Sign"
# still in tmp mode
assert m[0] != "Ready To Sign"
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
assert len(m) == 2
# Ignore Add Current and Restore Master (only SV items are numbered with colon)
assert len([mi for mi in m if ":" in mi]) == 2
press_down()
press_select()
@ -1146,7 +1156,10 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item
assert "Delete" in m
pick_menu_item("Delete")
need_keypress("1") # only delete from seed vault
time.sleep(.1)
_, story = cap_story()
assert "delete its settings?" not in story
press_select() # only delete from seed vault, no other option provided
time.sleep(.1)
m = cap_menu()
assert len(m) == 3
@ -1164,6 +1177,25 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item
# still in ephemeral
assert title == m[0]
restore_main_seed()
pick_menu_item("Seed Vault")
press_select()
time.sleep(.1)
m = cap_menu()
assert "Rename" in m
assert "Use This Seed" in m
assert "Delete" in m
pick_menu_item("Delete")
time.sleep(.1)
_, story = cap_story()
assert "delete its settings?" in story
need_keypress("1") # only remove from seed vault, keep settings
time.sleep(.1)
m = cap_menu()
assert all([":" not in mi for mi in m])
assert "(none saved yet)" in m
def test_xfp_collision(reset_seed_words, settings_set, import_ephemeral_xprv,
cap_story, press_cancel, pick_menu_item, cap_menu,
@ -1473,4 +1505,21 @@ def test_home_menu_xfp(goto_home, pick_menu_item, press_select, cap_story, cap_m
m = cap_menu()
assert m[0] == "Ready To Sign"
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
reset_seed_words()
goto_eph_seed_menu()
ephemeral_seed_disabled()
e_seed_words = generate_ephemeral_words(num_words=12, dice=False,
from_main=True, seed_vault=False)
verify_ephemeral_secret_ui(mnemonic=e_seed_words, seed_vault=False)
goto_home()
pick_menu_item("Advanced/Tools")
m = cap_menu()
assert "Seed Vault" not in m
# EOF

View File

@ -4,12 +4,10 @@
#
# Start simulator with: simulator.py --eff --set nfc=1
#
import sys
sys.path.append("../shared")
from descriptor import Descriptor
from mnemonic import Mnemonic
import pytest, time, os, json, io, bech32
from bip32 import BIP32Node
from descriptor import Descriptor
from mnemonic import Mnemonic
from ckcc_protocol.constants import *
from helpers import xfp2str, slip132undo
from conftest import simulator_fixed_xfp, simulator_fixed_tprv, simulator_fixed_words, simulator_fixed_xprv
@ -85,7 +83,12 @@ def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home,
addrs = []
imm_js = None
imd_js = None
imd_js_tr = None
tr = False
for ln in fp:
if ln.startswith("p2tr:"):
tr = True
if 'importmulti' in ln:
# PLAN: this will become obsolete
assert ln.startswith("importmulti '")
@ -93,20 +96,26 @@ def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home,
assert not imm_js, "dup importmulti lines"
imm_js = ln[13:-2]
elif "importdescriptors '" in ln:
ln = ln.strip()
assert ln.startswith("importdescriptors '")
assert ln.endswith("'\n")
assert not imd_js, "dup importdesc lines"
imd_js = ln[19:-2]
if tr:
imd_js_tr = ln[19:-1]
tr = False
else:
imd_js = ln[19:-1]
elif '=>' in ln:
path, addr = ln.strip().split(' => ', 1)
assert path.startswith(f"m/84h/1h/{acct_num}h/0")
assert addr.startswith('bcrt1q') # TODO here we should differentiate if testnet or smthg
sk = BIP32Node.from_wallet_key(simulator_fixed_tprv).subkey_for_path(path)
h20 = sk.hash160()
assert addr == bech32.encode(addr[0:4], 0, h20) # TODO here we should differentiate if testnet or smthg
if path.startswith(f"m/86h/1h/{acct_num}h/0"):
assert addr.startswith('bcrt1p')
assert addr == sk.address(addr_fmt="p2tr", chain="XRT")
else:
assert path.startswith(f"m/84h/1h/{acct_num}h/0")
assert addr.startswith("bcrt1q")
assert addr == sk.address(addr_fmt="p2wpkh", chain="XRT")
addrs.append(addr)
assert len(addrs) == 3
assert len(addrs) == 6
xfp = xfp2str(simulator_fixed_xfp).lower()
@ -140,14 +149,9 @@ def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home,
x = bitcoind_wallet.getaddressinfo(addrs[-1])
pprint(x)
assert x['address'] == addrs[-1]
if 'label' in x:
# pre 0.21.?
assert x['label'] == 'testcase'
else:
assert x['labels'] == ['testcase']
assert x['iswatchonly'] == True
assert x['iswitness'] == True
assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
# assert x['iswatchonly'] == True
assert x['iswitness'] is True
# assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
# importdescriptors -- its better
assert imd_js
@ -168,26 +172,49 @@ def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home,
assert expect in desc
assert expect+f'/{n}/*' in desc
assert 'label' not in d
res = bitcoind_d_wallet.importdescriptors(obj)
assert res[0]["success"]
assert res[1]["success"]
x = bitcoind_d_wallet.getaddressinfo(addrs[2])
pprint(x)
assert x['address'] == addrs[2]
assert x['iswatchonly'] == False
assert x['iswitness'] == True
assert x['solvable'] == True
assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
assert x['hdkeypath'].replace("'", "h") == f"m/84h/1h/{acct_num}h/0/%d" % 2
assert imd_js_tr
obj = json.loads(imd_js_tr)
for n, here in enumerate(obj):
assert here['timestamp'] == 'now'
assert here['internal'] == bool(n)
d = here['desc']
desc, chk = d.split('#', 1)
assert len(chk) == 8
assert desc.startswith(f'tr([{xfp}/86h/1h/{acct_num}h]')
expect = BIP32Node.from_wallet_key(simulator_fixed_tprv) \
.subkey_for_path(f"m/86h/1h/{acct_num}h").hwif()
assert expect in desc
assert expect + f'/{n}/*' in desc
# test against bitcoind -- needs a "descriptor native" wallet
res = bitcoind_d_wallet.importdescriptors(obj)
assert res[0]["success"]
assert res[1]["success"]
core_gen = []
for i in range(3):
core_gen.append(bitcoind_d_wallet.getnewaddress())
assert core_gen == addrs
x = bitcoind_d_wallet.getaddressinfo(addrs[-1])
pprint(x)
assert x['address'] == addrs[-1]
assert x['iswatchonly'] == False
assert x['iswitness'] == True
# assert x['ismine'] == True # TODO we have imported pubkeys - it has no idea if it is ours or solvable
# assert x['solvable'] == True
# assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
#assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
assert x['iswatchonly'] is False
assert x['iswitness'] is True
assert x['solvable'] is True
assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
assert x['hdkeypath'].replace("'", "h") == f"m/86h/1h/{acct_num}h/0/%d" % 2
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@ -305,7 +332,7 @@ def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, ca
@pytest.mark.parametrize('acct_num', [ None, '99', '1236'])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
@pytest.mark.parametrize('app', [
# no need to run them all - just name check differs
("Generic JSON", "Generic Export"),
@ -317,12 +344,12 @@ def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, ca
])
def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap_story, need_keypress,
microsd_path, nfc_read_json, virtdisk_path, addr_vs_path, enter_number,
load_export, testnet, use_mainnet, press_select,
load_export, chain, use_mainnet, press_select,
skip_if_useless_way, expect_acctnum_captured):
skip_if_useless_way(way)
if not testnet:
if chain == "BTC":
use_mainnet()
export_mi, app_f_name = app
@ -377,8 +404,8 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap
addr = v.get('first', None)
if fn == 'bip44':
assert first.address(netcode="XTN" if testnet else "BTC") == v['first']
addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, testnet=testnet)
assert first.address(chain=chain) == v['first']
addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, chain=chain)
elif ('bip48_' in fn) or (fn == 'bip45'):
# multisig: cant do addrs
assert addr == None
@ -389,11 +416,11 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap
h20 = first.hash160()
if fn == 'bip84':
assert addr == bech32.encode(addr[0:2], 0, h20)
addr_vs_path(addr, v['deriv'] + '/0/0', AF_P2WPKH, testnet=testnet)
addr_vs_path(addr, v['deriv'] + '/0/0', AF_P2WPKH, chain=chain)
elif fn == 'bip49':
# don't have test logic for verifying these addrs
# - need to make script, and bleh
assert first.address(addr_fmt="p2sh-p2wpkh", netcode="XTN" if testnet else "BTC") == v['first']
assert first.address(addr_fmt="p2sh-p2wpkh", chain=chain) == v['first']
else:
assert False
@ -455,15 +482,14 @@ def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_k
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, microsd_path,
addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_mainnet,
load_export, testnet, skip_if_useless_way):
addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_testnet,
load_export, chain, skip_if_useless_way):
# test UX and values produced.
skip_if_useless_way(way)
if not testnet:
use_mainnet()
use_testnet(chain == "XTN")
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('File Management')
@ -481,7 +507,7 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi
xfp = xfp2str(simulator_fixed_xfp).upper()
ek = simulator_fixed_tprv if testnet else simulator_fixed_xprv
ek = simulator_fixed_tprv if chain == "XTN" else simulator_fixed_xprv
root = BIP32Node.from_wallet_key(ek)
for ln in fp:
@ -508,14 +534,16 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi
if not f:
if rhs[0] in '1mn':
f = AF_CLASSIC
elif rhs[0:3] in ['tb1', "bc1"]:
elif rhs[0:4] in ['tb1q', "bc1q"]:
f = AF_P2WPKH
elif rhs[0:4] in ['tb1p', "bc1p"]:
f = AF_P2TR
elif rhs[0] in '23':
f = AF_P2WPKH_P2SH
else:
raise ValueError(rhs)
addr_vs_path(rhs, path=lhs, addr_fmt=f, testnet=testnet)
addr_vs_path(rhs, path=lhs, addr_fmt=f, chain=chain)
@pytest.mark.qrcode
@ -538,6 +566,8 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
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"
elif '-44' in m:
expect = "m/44h/0h/{acct}h"
elif '49' in m:
@ -603,14 +633,13 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
@pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC, AF_P2TR])
@pytest.mark.parametrize("acct_num", [None, 0, 1, (2 ** 31) - 1])
@pytest.mark.parametrize("int_ext", [True, False])
def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
settings_set, need_keypress, expect_acctnum_captured, OK,
pick_menu_item, way, cap_story, cap_menu, int_ext, settings_get,
virtdisk_path, load_export, press_select, skip_if_useless_way):
skip_if_useless_way(way)
virtdisk_path, load_export, press_select):
settings_set('chain', chain)
chain_num = 1 if chain in ["XTN", "XRT"] else 0
@ -651,6 +680,10 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
menu_item = "P2SH-Segwit"
desc_prefix = "sh(wpkh("
bip44_purpose = 49
elif addr_fmt == AF_P2TR:
menu_item = "Taproot P2TR"
desc_prefix = "tr("
bip44_purpose = 86
else:
# addr_fmt == AF_CLASSIC:
menu_item = "Classic P2PKH"
@ -662,7 +695,11 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
expect_acctnum_captured(acct_num)
contents = load_export(way, label="Descriptor", is_json=False, addr_fmt=addr_fmt)
sig_check = True
if addr_fmt == AF_P2TR:
sig_check = False
contents = load_export(way, label="Descriptor", is_json=False, addr_fmt=addr_fmt,
sig_check=sig_check)
descriptor = contents.strip()
if int_ext is False:

View File

@ -206,7 +206,7 @@ def hsm_reset(dev, sim_exec):
# wallets
(DICT(rules=[dict(wallet='1')]),
'(non multisig)'),
'(singlesig only)'),
# users
(DICT(rules=[dict(users=USERS)]),
@ -570,7 +570,7 @@ def test_named_wallets(dev, start_hsm, tweak_rule, make_myself_wallet, hsm_statu
# simple p2pkh should fail
psbt = fake_txn(1, 2, dev.master_xpub, outvals=[amount, 1E8-amount], change_outputs=[1], fee=0)
attempt_psbt(psbt, "not multisig")
attempt_psbt(psbt, "singlesig only")
# but txn w/ multisig wallet should work
psbt = fake_ms_txn(1, 2, M, keys, fee=0, outvals=[amount, 1E8-amount], outstyles=['p2wsh'],
@ -579,7 +579,119 @@ def test_named_wallets(dev, start_hsm, tweak_rule, make_myself_wallet, hsm_statu
# check ms txn not accepted when rule spec's a single signer
tweak_rule(0, dict(wallet='1'))
attempt_psbt(psbt, 'wrong wallet')
attempt_psbt(psbt, 'wrong multisig wallet')
@pytest.mark.bitcoind
def test_named_wallets_miniscript(dev, start_hsm, tweak_rule, make_myself_wallet,
hsm_status, attempt_psbt, fake_txn, bitcoind,
offer_minsc_import, need_keypress, pick_menu_item,
load_export, goto_home):
stat = hsm_status()
assert not stat.active
from test_miniscript import CHANGE_BASED_DESCS
for i, desc in enumerate(CHANGE_BASED_DESCS):
name = f"hsm_msc{i}"
xd = json.dumps({"name": name, "desc": desc})
title, story = offer_minsc_import(xd)
assert "Create new miniscript wallet?" in story
assert name in story
need_keypress("y")
time.sleep(.2)
core_wallets = []
for i in range(len(CHANGE_BASED_DESCS)):
name = f"hsm_msc{i}"
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")
pick_menu_item(name)
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"]
af = "bech32"
if i > 1:
af = "bech32m"
addr = wo.getnewaddress("", af)
bitcoind.supply_wallet.sendtoaddress(addr, 1.0)
core_wallets.append(wo)
# mine above txns
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
for w in core_wallets:
assert len(w.listunspent()) > 0, "nu funds"
stat = hsm_status()
for i in range(len(CHANGE_BASED_DESCS)):
assert f"hsm_msc{i}" in stat.wallets
# policy: only allow miniscript 0
wname = "hsm_msc0"
policy = DICT(share_addrs=["any"], rules=[dict(wallet=wname)])
stat = start_hsm(policy)
assert 'Any amount from miniscript wallet' in stat.summary
assert wname in stat.summary
assert 'wallets' not in stat
# simple p2pkh should fail
psbt = fake_txn(1, 2, outvals=[5E6, 1E8-5E6], change_outputs=[1], fee=0)
attempt_psbt(psbt, "singlesig only")
# but txn from target miniscript wallet 0 must work
wal0 = core_wallets[0]
psbt_res = wal0.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.2}], 0, {"fee_rate": 20})
attempt_psbt(base64.b64decode(psbt_res["psbt"]))
# WRONG
wal2 = core_wallets[2]
psbt_res = wal2.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.2}], 0, {"fee_rate": 18})
attempt_psbt(base64.b64decode(psbt_res["psbt"]), 'wrong miniscript wallet')
wal1 = core_wallets[1]
psbt_res = wal1.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.2}], 0, {"fee_rate": 12})
attempt_psbt(base64.b64decode(psbt_res["psbt"]), 'wrong miniscript wallet')
# works
psbt_res = wal0.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.3}], 0, {"fee_rate": 15})
attempt_psbt(base64.b64decode(psbt_res["psbt"]))
wname = "hsm_msc3"
tweak_rule(0, dict(wallet=wname))
# this worked before but now, after tweak, it does not
psbt_res = wal0.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.1}], 0, {"fee_rate": 13})
attempt_psbt(base64.b64decode(psbt_res["psbt"]), 'wrong miniscript wallet')
# correct wallet 3
wal3 = core_wallets[3]
psbt_res = wal3.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.6}], 0, {"fee_rate": 10})
attempt_psbt(base64.b64decode(psbt_res["psbt"]))
psbt_res = wal3.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 0.15}], 0, {"fee_rate": 15})
last_correct = base64.b64decode(psbt_res["psbt"])
attempt_psbt(last_correct)
# check ms txn not accepted when rule spec's a single signer
tweak_rule(0, dict(wallet='1'))
attempt_psbt(last_correct, 'wrong miniscript wallet')
stat = hsm_status()
assert stat.approvals == 4
assert stat.refusals == 5
@pytest.mark.parametrize('with_whitelist_opts', [ False, True])
def test_whitelist_single(dev, start_hsm, tweak_rule, attempt_psbt, fake_txn, with_whitelist_opts, amount=5E6):
@ -594,7 +706,7 @@ def test_whitelist_single(dev, start_hsm, tweak_rule, attempt_psbt, fake_txn, wi
start_hsm(policy)
# try all addr types
for style in ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh']:
for style in ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2tr']:
dests = []
psbt = fake_txn(1, 2, dev.master_xpub,
outstyles=[style, 'p2wpkh'],
@ -1157,6 +1269,31 @@ def test_show_p2sh_addr(dev, hsm_reset, start_hsm, change_hsm, make_myself_walle
M, xfp_paths, scr, addr_fmt=AF_P2WSH))
assert 'Not allowed in HSM mode' in str(ee)
def test_show_miniscript_addr(dev, offer_minsc_import, start_hsm,
change_hsm, need_keypress, clear_miniscript):
clear_miniscript()
from test_miniscript import CHANGE_BASED_DESCS
name = "hsm_msc_msas"
xd = json.dumps({"name": name, "desc": CHANGE_BASED_DESCS[0]})
title, story = offer_minsc_import(xd)
assert "Create new miniscript wallet?" in story
assert name in story
need_keypress("y")
time.sleep(.2)
policy = DICT(share_addrs=["any", "p2sh"], rules=[dict(wallet=name)])
start_hsm(policy)
with pytest.raises(CCProtoError) as ee:
dev.send_recv(CCProtocolPacker.miniscript_address(name, False, 0))
assert "Not allowed in HSM mode" in ee.value.args[0]
# change policy to allow miniscript address show
policy = DICT(share_addrs=["any", "p2sh", "msas"], rules=[dict(wallet=name)])
change_hsm(policy)
addr = dev.send_recv(CCProtocolPacker.miniscript_address(name, False, 0))
assert addr[2:4] == "1q"
def test_xpub_sharing(dev, start_hsm, change_hsm, addr_fmt=AF_CLASSIC):
# xpub sharing, but only at certain derivations
# - note 'm' is always shared
@ -1537,7 +1674,8 @@ def test_op_return_output_local(op_return_data, start_hsm, attempt_psbt, fake_tx
def test_op_return_output_bitcoind(op_return_data, start_hsm, attempt_psbt, bitcoind_d_sim_watch, bitcoind, hsm_reset):
cc = bitcoind_d_sim_watch
dest_address = cc.getnewaddress()
bitcoind.supply_wallet.generatetoaddress(101, dest_address)
bitcoind.supply_wallet.sendtoaddress(dest_address, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt = cc.walletcreatefundedpsbt([], [{dest_address: 1.0}, {"data": op_return_data.hex()}], 0, {"fee_rate": 20})["psbt"]
policy = DICT(rules=[dict(max_amount=10)])
start_hsm(policy)

3011
testing/test_miniscript.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,6 @@
#
# py.test test_multisig.py -m ms_danger --ms-danger
#
import sys
sys.path.append("../shared")
from descriptor import MultisigDescriptor, append_checksum, MULTI_FMT_TO_SCRIPT, parse_desc_str
import time, pytest, os, random, json, shutil, pdb, io, base64, struct, bech32, itertools, re
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
from ckcc.protocol import CCProtocolPacker, MAX_TXN_LEN
@ -24,6 +21,7 @@ from ctransaction import CTransaction, CTxOut, CTxIn, COutPoint, uint256_from_st
from io import BytesIO
from hashlib import sha256
from bbqr import split_qrs
from descriptor import MULTI_FMT_TO_SCRIPT, MultisigDescriptor, parse_desc_str
from charcodes import KEY_QR
@ -100,11 +98,11 @@ def make_multisig(dev, sim_execfile):
# default is BIP-45: m/45'/... (but no co-signer idx)
# - but can provide str format for deriviation, use {idx} for cosigner idx
def doit(M, N, unique=0, deriv=None, dev_key=False):
def doit(M, N, unique=0, deriv=None, dev_key=False, chain="XTN"):
keys = []
for i in range(N-1):
pk = BIP32Node.from_master_secret(b'CSW is a fraud %d - %d' % (i, unique), 'XTN')
pk = BIP32Node.from_master_secret(b'CSW is a fraud %d - %d' % (i, unique), chain)
xfp = unpack("<I", pk.fingerprint())[0]
@ -126,7 +124,7 @@ def make_multisig(dev, sim_execfile):
xfp_bytes = pk.fingerprint()
xfp = swab32(struct.unpack('>I', xfp_bytes)[0])
else:
pk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
pk = BIP32Node.from_wallet_key(simulator_fixed_tprv if chain == "XTN" else simulator_fixed_xprv)
xfp = simulator_fixed_xfp
if not deriv:
@ -258,7 +256,7 @@ def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select,
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None,
keys=None, do_import=True, derivs=None, descriptor=False,
int_ext_desc=False, dev_key=False, way=None, bip67=True,
force_unsort_ms=True):
force_unsort_ms=True, chain="XTN"):
# param: bip67 if false, only usable together with descriptor=True
if not bip67:
assert descriptor, "needs descriptor=True"
@ -267,7 +265,8 @@ def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select,
settings_set("unsort_ms", 1)
keys = keys or make_multisig(M, N, unique=unique, dev_key=dev_key,
deriv=common or (derivs[0] if derivs else None))
deriv=common or (derivs[0] if derivs else None),
chain=chain)
name = name or f'test-{M}-{N}'
if not do_import:
@ -391,6 +390,7 @@ def test_ms_import_variations(N, make_multisig, offer_ms_import, press_cancel, i
# the different addr formats
for af in unmap_addr_fmt.keys():
if af == "p2tr": continue
config = f'format: {af}\n'
config += '\n'.join(sk.hwif(as_private=False) for xfp,m,sk in keys)
title, story = offer_ms_import(config)
@ -497,7 +497,7 @@ def make_ms_address(M, keys, idx=0, is_change=0, addr_fmt=AF_P2SH, testnet=1,
@pytest.fixture
def test_ms_show_addr(dev, cap_story, press_select, addr_vs_path, bitcoind_p2sh,
has_ms_checks, is_q1):
def doit(M, keys, addr_fmt=AF_P2SH, bip45=True, **make_redeem_args):
def doit(M, keys, addr_fmt=AF_P2SH, bip45=True, chain="XTN", **make_redeem_args):
# test we are showing addresses correctly
# - verifies against bitcoind as well
addr_fmt = unmap_addr_fmt.get(addr_fmt, addr_fmt)
@ -530,7 +530,7 @@ def test_ms_show_addr(dev, cap_story, press_select, addr_vs_path, bitcoind_p2sh,
press_select()
# check expected addr was generated based on my math
addr_vs_path(got_addr, addr_fmt=addr_fmt, script=scr)
addr_vs_path(got_addr, addr_fmt=addr_fmt, script=scr, chain=chain)
# also check against bitcoind
core_addr, core_scr = bitcoind_p2sh(M, pubkeys, addr_fmt)
@ -554,7 +554,7 @@ def test_import_ranges(m_of_n, use_regtest, addr_fmt, clear_ms, import_ms_wallet
try:
# test an address that should be in that wallet.
time.sleep(.1)
test_ms_show_addr(M, keys, addr_fmt=addr_fmt)
test_ms_show_addr(M, keys, addr_fmt=addr_fmt, chain="XRT")
finally:
clear_ms()
@ -1051,8 +1051,8 @@ def test_import_dup_safe(N, clear_ms, make_multisig, offer_ms_import,
menu = cap_menu()
assert f'{M}/{N}: {name}' in menu
# depending if NFC enabled or not, and if Q (has QR)
assert (len(menu) - num_wallets) in [6, 7, 8]
# depending if NFC enabled or not, and if Q (has QR) or whether EDGE
assert (len(menu) - num_wallets) in [6, 7, 8, 9]
title, story = offer_ms_import(make_named('xxx-orig'))
assert 'Create new multisig wallet' in story
@ -1484,7 +1484,7 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev
# IMPORTANT: wont work if you start simulator with --ms flag. Use no args
all_out_styles = list(unmap_addr_fmt.keys())
all_out_styles = [af for af in unmap_addr_fmt.keys() if af != "p2tr"]
num_outs = len(all_out_styles)
clear_ms()
@ -2001,43 +2001,6 @@ def test_ms_change_fraud(case, pk_num, num_ins, dev, addr_fmt, clear_ms, incl_xp
assert len(story.split(':')[-1].strip()), story
@pytest.mark.parametrize('repeat', range(2) )
def test_iss6743(repeat, set_seed_words, sim_execfile, try_sign):
# from SomberNight <https://github.com/spesmilo/electrum/issues/6743#issuecomment-729965813>
psbt_b4 = bytes.fromhex('70736274ff0100520200000001bde05be36069e2e0fe44793c68ad8244bb1a52cc37f152e0fa5b75e40169d7f70000000000fdffffff018b1e000000000000160014ed5180f05c7b1dc980732602c50cda40530e00ad4de11c004f01024289ef0000000000000000007dd565da7ee1cf05c516e89a608968fed4a2450633a00c7b922df66b27afd2e1033a0a4fa4b0a997738ac2f142a395c1f02afcb31d7ffd46a90a0c927a4c411fd704094ef7844f01024289ef0431fcbdcc8000000112d4aaea7292e7870c7eeb3565fa1c1fa8f957fa7c4c24b411d5b4f5710d359a023e63d1e54063525bea286ccb2a0ad7b14560aa31ec4be826afa883141dfe1d53145c9e228d300000800100008000000080010000804f01024289ef04e44b38f1800000014a1960f3a3c86ba355a16a66a548cfb62eeb25663311f7cd662a192896f3777e038cc595159a395e4ec35e477c9523a1512f873e74d303fb03fc9a1503b1ba45271434652fae30000080010000800000008001000080000100df02000000000101d1a321707660769c7f8604d04c9ae2db58cf1ec7a01f4f285cdcbb25ce14bdfd0300000000fdffffff02401f00000000000017a914bfd0b8471a3706c1e17870a4d39b0354bcea57b687c864000000000000160014e608b171d63ec24d9fa252d5c1e45624b14e44700247304402205133eb96df167b895f657cce31c6882840a403013682d9d4651aed2730a7dad502202aaacc045d85d9c711af0c84e7f355cc18bf2f8e6d91774d42ba24de8418a39e012103a58d8eb325abb412eaf927cf11d8b7641c4a468ce412057e47892ca2d13ed6144de11c000104220020a3c65c4e376d82fb3ca45596feee5b08313ad64f38590c1b08bc530d1c0bbfea010569522102ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da621034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef6381921039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f53ae220602ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da60c094ef78400000000030000002206034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef638191c5c9e228d3000008001000080000000800100008000000000030000002206039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f1c34652fae3000008001000080000000800100008000000000030000000000')
# pre 3.2.0 result
psbt_wrong = bytes.fromhex('70736274ff0100520200000001bde05be36069e2e0fe44793c68ad8244bb1a52cc37f152e0fa5b75e40169d7f70000000000fdffffff018b1e000000000000160014ed5180f05c7b1dc980732602c50cda40530e00ad4de11c004f01024289ef0000000000000000007dd565da7ee1cf05c516e89a608968fed4a2450633a00c7b922df66b27afd2e1033a0a4fa4b0a997738ac2f142a395c1f02afcb31d7ffd46a90a0c927a4c411fd704094ef7844f01024289ef0431fcbdcc8000000112d4aaea7292e7870c7eeb3565fa1c1fa8f957fa7c4c24b411d5b4f5710d359a023e63d1e54063525bea286ccb2a0ad7b14560aa31ec4be826afa883141dfe1d53145c9e228d300000800100008000000080010000804f01024289ef04e44b38f1800000014a1960f3a3c86ba355a16a66a548cfb62eeb25663311f7cd662a192896f3777e038cc595159a395e4ec35e477c9523a1512f873e74d303fb03fc9a1503b1ba45271434652fae30000080010000800000008001000080000100df02000000000101d1a321707660769c7f8604d04c9ae2db58cf1ec7a01f4f285cdcbb25ce14bdfd0300000000fdffffff02401f00000000000017a914bfd0b8471a3706c1e17870a4d39b0354bcea57b687c864000000000000160014e608b171d63ec24d9fa252d5c1e45624b14e44700247304402205133eb96df167b895f657cce31c6882840a403013682d9d4651aed2730a7dad502202aaacc045d85d9c711af0c84e7f355cc18bf2f8e6d91774d42ba24de8418a39e012103a58d8eb325abb412eaf927cf11d8b7641c4a468ce412057e47892ca2d13ed6144de11c002202034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef63819483045022100a85d08eef6675803fe2b58dda11a553641080e07da36a2f3e116f1224201931b022071b0ba83ef920d49b520c37993c039d13ae508a1adbd47eb4b329713fcc8baef01010304010000002206034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef638191c5c9e228d3000008001000080000000800100008000000000030000002206039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f1c34652fae300000800100008000000080010000800000000003000000220602ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da60c094ef78400000000030000000104220020a3c65c4e376d82fb3ca45596feee5b08313ad64f38590c1b08bc530d1c0bbfea010569522102ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da621034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef6381921039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f53ae0000')
# psbt_right = bytes.fromhex('70736274ff0100520200000001bde05be36069e2e0fe44793c68ad8244bb1a52cc37f152e0fa5b75e40169d7f70000000000fdffffff018b1e000000000000160014ed5180f05c7b1dc980732602c50cda40530e00ad4de11c004f01024289ef0000000000000000007dd565da7ee1cf05c516e89a608968fed4a2450633a00c7b922df66b27afd2e1033a0a4fa4b0a997738ac2f142a395c1f02afcb31d7ffd46a90a0c927a4c411fd704094ef7844f01024289ef0431fcbdcc8000000112d4aaea7292e7870c7eeb3565fa1c1fa8f957fa7c4c24b411d5b4f5710d359a023e63d1e54063525bea286ccb2a0ad7b14560aa31ec4be826afa883141dfe1d53145c9e228d300000800100008000000080010000804f01024289ef04e44b38f1800000014a1960f3a3c86ba355a16a66a548cfb62eeb25663311f7cd662a192896f3777e038cc595159a395e4ec35e477c9523a1512f873e74d303fb03fc9a1503b1ba45271434652fae30000080010000800000008001000080000100df02000000000101d1a321707660769c7f8604d04c9ae2db58cf1ec7a01f4f285cdcbb25ce14bdfd0300000000fdffffff02401f00000000000017a914bfd0b8471a3706c1e17870a4d39b0354bcea57b687c864000000000000160014e608b171d63ec24d9fa252d5c1e45624b14e44700247304402205133eb96df167b895f657cce31c6882840a403013682d9d4651aed2730a7dad502202aaacc045d85d9c711af0c84e7f355cc18bf2f8e6d91774d42ba24de8418a39e012103a58d8eb325abb412eaf927cf11d8b7641c4a468ce412057e47892ca2d13ed6144de11c002202034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef63819483045022100ae90a7e4c350389816b03af0af46df59a2f53da04cc95a2abd81c0bbc5950c1d02202f9471d6b0664b7a46e81da62d149f688adc7ba2b3413372d26fa618a8460eba01010304010000002206034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef638191c5c9e228d3000008001000080000000800100008000000000030000002206039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f1c34652fae300000800100008000000080010000800000000003000000220602ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da60c094ef78400000000030000000104220020a3c65c4e376d82fb3ca45596feee5b08313ad64f38590c1b08bc530d1c0bbfea010569522102ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da621034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef6381921039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f53ae0000')
# changed with with introduction of signature grinding
psbt_right = bytes.fromhex('70736274ff0100520200000001bde05be36069e2e0fe44793c68ad8244bb1a52cc37f152e0fa5b75e40169d7f70000000000fdffffff018b1e000000000000160014ed5180f05c7b1dc980732602c50cda40530e00ad4de11c004f01024289ef0000000000000000007dd565da7ee1cf05c516e89a608968fed4a2450633a00c7b922df66b27afd2e1033a0a4fa4b0a997738ac2f142a395c1f02afcb31d7ffd46a90a0c927a4c411fd704094ef7844f01024289ef0431fcbdcc8000000112d4aaea7292e7870c7eeb3565fa1c1fa8f957fa7c4c24b411d5b4f5710d359a023e63d1e54063525bea286ccb2a0ad7b14560aa31ec4be826afa883141dfe1d53145c9e228d300000800100008000000080010000804f01024289ef04e44b38f1800000014a1960f3a3c86ba355a16a66a548cfb62eeb25663311f7cd662a192896f3777e038cc595159a395e4ec35e477c9523a1512f873e74d303fb03fc9a1503b1ba45271434652fae30000080010000800000008001000080000100df02000000000101d1a321707660769c7f8604d04c9ae2db58cf1ec7a01f4f285cdcbb25ce14bdfd0300000000fdffffff02401f00000000000017a914bfd0b8471a3706c1e17870a4d39b0354bcea57b687c864000000000000160014e608b171d63ec24d9fa252d5c1e45624b14e44700247304402205133eb96df167b895f657cce31c6882840a403013682d9d4651aed2730a7dad502202aaacc045d85d9c711af0c84e7f355cc18bf2f8e6d91774d42ba24de8418a39e012103a58d8eb325abb412eaf927cf11d8b7641c4a468ce412057e47892ca2d13ed6144de11c002202034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef6381947304402201008b084f53d3064ee381dfb3ff4373b29d6ae765b2af15a4e217e8d5d049c650220576af95d79b8fc686627da8a534141208b225ceb6085cd93fcaffb153ac016ea01010304010000002206034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef638191c5c9e228d3000008001000080000000800100008000000000030000002206039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f1c34652fae300000800100008000000080010000800000000003000000220602ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da60c094ef78400000000030000000104220020a3c65c4e376d82fb3ca45596feee5b08313ad64f38590c1b08bc530d1c0bbfea010569522102ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da621034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef6381921039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f53ae0000')
seed_words = 'all all all all all all all all all all all all'
expect_xfp = swab32(int('5c9e228d', 16))
assert xfp2str(expect_xfp) == '5c9e228d'.upper()
# load specific private key
xfp = set_seed_words(seed_words)
assert xfp == expect_xfp
# check Coldcard derives expected Upub
derivation = "m/48h/1h/0h/1h" # part of devtest/unit_iss6743.py
expect_xpub = 'Upub5SJWbuhs5tM4mkJST69tnpGGaf8dDTqByx3BLSocWFpq5YLh1fky4DQTFGQVG6nCSqZfUiAAeStdxSQteUcfMsWjDkhniZx4GdwpB18Tnbq'
pub = sim_execfile('devtest/unit_iss6743.py')
assert pub == expect_xpub
# verify psbt globals section
tp = BasicPSBT().parse(psbt_b4)
(hdr_xpub, hdr_path), = [(v,k) for v,k in tp.xpubs if k[0:4] == pack('<I', expect_xfp)]
assert expect_xpub == encode_base58_checksum(hdr_xpub)
assert derivation == path_to_str(unpack('<%dI' % (len(hdr_path) // 4),hdr_path))
# sign a multisig, with xpubs in globals
_, out_psbt = try_sign(psbt_b4, accept=True, accept_ms_import=True)
assert out_psbt != psbt_wrong
assert out_psbt == psbt_right
open('debug/i6.psbt', 'wt').write(out_psbt.hex())
@pytest.mark.parametrize('N', [ 3, 15])
@pytest.mark.parametrize('xderiv', [ None, 'any', 'unknown', '*', '', 'none'])
def test_ms_import_nopath(N, xderiv, make_multisig, clear_ms, offer_ms_import):
@ -2209,11 +2172,12 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
# once change is selected - do not offer this option again
assert "change addresses." not in story
assert "(0)" not in story
# unwrap text a bit
if change:
story = story.replace("=>\n", "=> ").replace('1/0]\n =>', "1/0 =>")
story = story.replace("=>\n", "=> ").replace('1/0]\n =>', "1/0] =>")
else:
story = story.replace("=>\n", "=> ").replace('0/0]\n =>', "0/0 =>")
story = story.replace("=>\n", "=> ").replace('0/0]\n =>', "0/0] =>")
maps = []
for ln in story.split('\n'):
@ -2222,8 +2186,9 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
path,chk,addr = ln.split()
assert chk == '=>'
assert '/' in path
path = path.replace("[", "").replace("]", "")
maps.append( (path, addr) )
maps.append((path, addr))
if start_idx <= 2147483638:
assert len(maps) == 10
@ -2238,6 +2203,7 @@ def test_ms_addr_explorer(change, M_N, addr_fmt, start_idx, clear_ms, cap_menu,
path_mapper=path_mapper, bip67=bip67)
assert int(subpath.split('/')[-1]) == idx
assert int(subpath.split('/')[-2]) == chng_idx
#print('../0/%s => \n %s' % (idx, B2A(script)))
start, end = detruncate_address(addr)
@ -2421,12 +2387,134 @@ def test_bitcoind_ms_address(change, M_N, addr_fmt, clear_ms, goto_home, need_ke
bitcoind_addrs = bitcoind.deriveaddresses(desc_export, addr_range)
for idx, cc_item in enumerate(cc_addrs):
cc_item = cc_item.split(",")
partial_address = cc_item[part_addr_index]
_start, _end = partial_address.split("___")
address = cc_item[part_addr_index]
if way != "nfc":
_start, _end = _start[1:], _end[:-1]
assert bitcoind_addrs[idx].startswith(_start)
assert bitcoind_addrs[idx].endswith(_end)
address = address[1:-1]
assert bitcoind_addrs[idx] == address
@pytest.fixture
def bitcoind_multisig(bitcoind, bitcoind_d_sim_watch, need_keypress, cap_story, load_export, pick_menu_item, goto_home,
cap_menu, microsd_path, use_regtest, press_select):
def doit(M, N, script_type, cc_account=0, funded=True):
use_regtest()
bitcoind_signers = [
bitcoind.create_wallet(wallet_name=f"bitcoind--signer{i}", disable_private_keys=False, blank=False,
passphrase=None, avoid_reuse=False, descriptors=True)
for i in range(N - 1)
]
for signer in bitcoind_signers:
signer.keypoolrefill(10)
# watch only wallet where multisig descriptor will be imported
ms = bitcoind.create_wallet(
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("sd", label="Multisig XPUB", is_json=True, sig_check=False)
template = xpub_obj[script_type +"_desc"]
# get keys from bitcoind signers
bitcoind_signers_xpubs = []
for signer in bitcoind_signers:
target_desc = ""
bitcoind_descriptors = signer.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]
bitcoind_signers_xpubs.append(core_key)
desc = template.replace("M", str(M), 1).replace("...", ",".join(bitcoind_signers_xpubs))
if script_type == 'p2wsh':
name = f"core{M}of{N}_native.txt"
elif script_type == "p2sh_p2wsh":
name = f"core{M}of{N}_wrapped.txt"
else:
name = f"core{M}of{N}_legacy.txt"
with open(microsd_path(name), "w") as f:
f.write(desc + "\n")
goto_home()
pick_menu_item('Settings')
pick_menu_item('Multisig Wallets')
pick_menu_item('Import from File')
time.sleep(0.3)
_, story = cap_story()
if "Press (1) to import multisig wallet file from SD Card" in story:
# in case Vdisk is enabled
need_keypress("1")
time.sleep(0.5)
pick_menu_item(name)
_, story = cap_story()
assert "Create new multisig wallet?" in story
assert name.split(".")[0] in story
assert f"{M} of {N}" in story
if M == N:
assert f"All {N} co-signers must approve spends" in story
else:
assert f"{M} signatures, from {N} possible" in story
if script_type == "p2wsh":
assert "P2WSH" in story
elif script_type == "p2sh":
assert "P2SH" in story
else:
assert "P2SH-P2WSH" in story
assert "Derivation:\n Varies (2)" 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)
assert res[0]["success"]
assert res[1]["success"]
if funded:
if script_type == "p2wsh":
addr_type = "bech32"
elif script_type == "p2tr":
addr_type = "bech32m"
elif script_type == "p2sh":
addr_type = "legacy"
else:
addr_type = "p2sh-segwit"
addr = ms.getnewaddress("", addr_type)
if script_type == "p2wsh":
sw = "bcrt1q"
elif script_type == "p2tr":
sw = "bcrt1p"
else:
sw = "2"
assert addr.startswith(sw)
# get some coins and fund above multisig address
bitcoind.supply_wallet.sendtoaddress(addr, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # mine above
return ms, bitcoind_signers
return doit
@pytest.mark.bitcoind
@ -2802,17 +2890,16 @@ def test_bitcoind_MofN_tutorial(m_n, desc_type, clear_ms, goto_home, need_keypre
@pytest.mark.parametrize("desc", [
# lack of checksum is now legal
# ("Missing descriptor checksum", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))"),
("Wrong checksum", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#gs2fqgl7"),
("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"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#fy9mm8dt"),
("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"),
("Malformed key derivation info", "wsh(sortedmulti(2,[0f056943]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#nhjvt4wd"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#gs2fqgl6"),
("Invalid subderivation path - only 0/* or <0;1>/* allowed", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0))#s487stua"),
("xpub depth", "wsh(sortedmulti(2,[0f056943]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M))#nhjvt4wd"),
("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"),
# ("Unsupported descriptor", "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))#t2zpj2eu"),
("Unsupported descriptor", "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)#ml40v0wf"),
("M must be <= N", "wsh(sortedmulti(3,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0/*))#uueddtsy"),
])
def test_exotic_descriptors(desc, clear_ms, goto_home, need_keypress, pick_menu_item, cap_menu,
@ -2894,7 +2981,7 @@ def test_ms_xpub_ordering(descriptor, m_n, clear_ms, make_multisig, import_ms_wa
@pytest.mark.parametrize('cmn_pth_from_root', [True, False])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5), (15, 15)])
@pytest.mark.parametrize('M_N', [(2, 3), (3, 5), (15, 15)])
@pytest.mark.parametrize('desc', ["multi", "sortedmulti"])
@pytest.mark.parametrize('addr_fmt', [AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH])
def test_multisig_descriptor_export(M_N, way, addr_fmt, cmn_pth_from_root, clear_ms, make_multisig,
@ -2986,6 +3073,82 @@ def test_multisig_descriptor_export(M_N, way, addr_fmt, cmn_pth_from_root, clear
clear_ms()
def test_chain_switching(use_mainnet, use_regtest, settings_get, settings_set,
clear_ms, goto_home, cap_menu, pick_menu_item,
need_keypress, import_ms_wallet):
clear_ms()
use_regtest()
# cannot import XPUBS when testnet/regtest enabled
with pytest.raises(Exception):
import_ms_wallet(3, 3, addr_fmt="p2wsh", accept=1, descriptor=True, chain="BTC")
import_ms_wallet(2, 2, addr_fmt="p2wsh", accept=1, descriptor=True, chain="XTN")
# assert that wallets created at XRT always store XTN anywas (key_chain)
res = settings_get("multisig")
assert len(res) == 1
assert res[0][-1]["ch"] == "XTN"
goto_home()
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
time.sleep(0.1)
m = cap_menu()
assert "(none setup yet)" not in m
assert "2/2:" in m[0]
goto_home()
settings_set("chain", "BTC")
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
time.sleep(0.1)
m = cap_menu()
# asterisk hints that some wallets are already stored
# but not on current active chain
assert "(none setup yet)*" in m
import_ms_wallet(3, 3, addr_fmt="p2wsh", accept=1, descriptor=True, chain="BTC")
goto_home()
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
time.sleep(0.1)
m = cap_menu()
assert "3/3:" in m[0]
for mi in m:
assert not mi.startswith("2/2:")
goto_home()
settings_set("chain", "XTN")
import_ms_wallet(4, 4, addr_fmt="p2wsh", accept=1, descriptor=True, chain="XTN")
pick_menu_item("Settings")
pick_menu_item("Multisig Wallets")
time.sleep(0.1)
m = cap_menu()
assert "(none setup yet)" not in m
assert "2/2:" in m[0]
assert "4/4:" in m[1]
for mi in m:
assert not mi.startswith("3/3:")
@pytest.mark.parametrize("desc", [
("wsh(sortedmulti(2,"
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*,"
"[0f056943/84'/1'/9']tpubDC7jGaaSE66QBAcX8TUD3JKWari1zmGH4gNyKZcrfq6NwCofKujNF2kyeVXgKshotxw5Yib8UxLrmmCmWd8NVPVTAL8rGfMdc7TsAKqsy6y/<0;1>/*"
"))"),
("wsh(sortedmulti(2,"
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*,"
"[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<2;3>/*"
"))"),
])
def test_same_key_account_based_multisig(goto_home, need_keypress, pick_menu_item, cap_story,
clear_ms, microsd_path, load_export, desc,
offer_ms_import):
clear_ms()
try:
_, story = offer_ms_import(desc)
except Exception as e:
assert "my key included more than once" in str(e)
def test_multisig_name_validation(microsd_path, offer_ms_import):
with open("data/multisig/export-p2wsh-myself.txt", "r") as f:
config = f.read()

View File

@ -2,12 +2,12 @@
#
# Address ownership tests.
#
import pytest, time, io, csv
import pytest, time, io, csv, json
from txn import fake_address
from base58 import encode_base58_checksum
from helpers import hash160
from helpers import hash160, taptweak
from bip32 import BIP32Node
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
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
@pytest.fixture
@ -23,7 +23,7 @@ def wipe_cache(sim_exec):
[14, 8, 26, 1, 7, 19]
'''
@pytest.mark.parametrize('addr_fmt', [
AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
])
@pytest.mark.parametrize('testnet', [ False, True] )
def test_negative(addr_fmt, testnet, sim_exec):
@ -36,24 +36,26 @@ def test_negative(addr_fmt, testnet, sim_exec):
assert 'Explained' in lst
@pytest.mark.parametrize('addr_fmt, testnet', [
(AF_CLASSIC, True),
(AF_CLASSIC, False),
(AF_P2WPKH, True),
(AF_P2WPKH, False),
(AF_P2WPKH_P2SH, True),
(AF_P2WPKH_P2SH, False),
@pytest.mark.parametrize('addr_fmt, chain', [
(AF_CLASSIC, "XTN"),
(AF_CLASSIC, "BTC"),
(AF_P2WPKH, "XTN"),
(AF_P2WPKH, "BTC"),
(AF_P2WPKH_P2SH, "XTN"),
(AF_P2WPKH_P2SH, "BTC"),
(AF_P2TR, "XTN"),
(AF_P2TR, "BTC"),
# multisig - testnet only
(AF_P2WSH, True),
(AF_P2SH, True),
(AF_P2WSH_P2SH,True),
(AF_P2WSH, "XTN"),
(AF_P2SH, "XTN"),
(AF_P2WSH_P2SH, "XTN"),
])
@pytest.mark.parametrize('offset', [ 3, 760] )
@pytest.mark.parametrize('subaccount', [ 0, 34] )
@pytest.mark.parametrize('change_idx', [ 0, 1] )
@pytest.mark.parametrize('from_empty', [ True, False] )
def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
enter_number, press_cancel, settings_set, import_ms_wallet, clear_ms
):
@ -61,17 +63,23 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
# API/Unit test, limited UX
if not testnet and addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
# multisig jigs assume testnet
raise pytest.skip('testnet only')
if chain == "BTC":
use_testnet(False)
testnet = False
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
# multisig jigs assume testnet
raise pytest.skip('testnet only')
coin_type = 0
if chain == "XTN":
use_testnet(True)
coin_type = 1
testnet = True
use_testnet(testnet)
if from_empty:
wipe_cache() # very different codepaths
settings_set('accts', [])
coin_type = 1 if testnet else 0
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
from test_multisig import make_ms_address, HARD
M, N = 1, 3
@ -99,6 +107,9 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
elif addr_fmt == AF_P2WPKH:
menu_item = expect_name = 'Segwit P2WPKH'
path = "m/84h/{ct}h/{acc}h"
elif addr_fmt == AF_P2TR:
menu_item = expect_name = 'Taproot P2TR'
path = "m/86h/{ct}h/{acc}h"
else:
raise ValueError(addr_fmt)
@ -108,14 +119,18 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
# see addr_vs_path
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
sk = mk.subkey_for_path(path[2:].replace('h', "'"))
sk = mk.subkey_for_path(path)
if addr_fmt == AF_CLASSIC:
addr = sk.address(netcode="XTN" if testnet else "BTC")
addr = sk.address(chain=chain)
elif addr_fmt == AF_P2WPKH_P2SH:
pkh = sk.hash160()
digest = hash160(b'\x00\x14' + pkh)
addr = encode_base58_checksum(bytes([196 if testnet else 5]) + digest)
elif addr_fmt == AF_P2TR:
from bech32 import encode
tweked_xonly = taptweak(sk.sec()[1:])
addr = encode("tb" if testnet else "bc", 1, tweked_xonly)
else:
pkh = sk.hash160()
addr = bech32_encode('tb' if testnet else 'bc', 0, pkh)
@ -166,7 +181,7 @@ def test_ux(valid, testnet, method,
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(netcode="XTN" if testnet else "BTC")
addr = sk.address(chain="XTN" if testnet else "BTC")
else:
addr = fake_address(addr_fmt, testnet)
@ -220,20 +235,26 @@ def test_ux(valid, testnet, method,
assert 'Searched ' in story
assert 'candidates without finding a match' in story
@pytest.mark.parametrize("af", ["P2SH-Segwit", "Segwit P2WPKH", "Classic P2PKH", "ms0"])
@pytest.mark.parametrize("af", ["P2SH-Segwit", "Segwit P2WPKH", "Classic P2PKH", "Taproot P2TR", "ms0", "msc0", "msc2"])
def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explorer,
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):
cap_story, load_export, offer_minsc_import):
goto_home()
wipe_cache()
settings_set('accts', [])
if af == "ms0":
clear_ms()
import_ms_wallet(2,3, name=af)
import_ms_wallet(2, 3, name=af)
press_select() # accept ms import
elif "msc" in af:
from test_miniscript import CHANGE_BASED_DESCS
which = int(af[-1])
title, story = offer_minsc_import(json.dumps({"name": af, "desc": CHANGE_BASED_DESCS[which]}))
assert "Create new miniscript wallet?" in story
press_select() # accept
goto_address_explorer()
pick_menu_item(af)
@ -245,17 +266,19 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
lst = eval(lst)
assert lst
if af == "ms0":
return # multisig addresses are blanked
title, body = cap_story()
contents, sig_addr = load_export_and_verify_signature(body, "sd", label="Address summary")
if af in ("Taproot P2TR", "ms0", "msc0", "msc2"):
# p2tr - no signature file
contents = load_export("sd", label="Address summary", is_json=False, sig_check=False)
else:
contents, _ = load_export_and_verify_signature(body, "sd", label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
assert hdr == ['Index', 'Payment Address', 'Derivation']
addr = None
for n, (idx, addr, deriv) in enumerate(cc, start=0):
assert hdr[:2] == ['Index', 'Payment Address']
for n, (idx, addr, *_) in enumerate(cc, start=0):
assert int(idx) == n
if idx == 200:
addr = addr
@ -279,7 +302,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
assert addr in story
assert title == 'Verified Address'
assert 'Found in wallet' in story
assert 'Derivation path' in story
# assert 'Derivation path' in story
if af == "P2SH-Segwit":
assert "P2WPKH-in-P2SH" in story
elif af == "Segwit P2WPKH":

View File

@ -6,19 +6,19 @@
# This module can and should be run with `-l` and without it.
#
import pytest, time, os, shutil, re, random
import pytest, time, os, shutil, re, random, json
from binascii import a2b_hex
from hashlib import sha256
from bip32 import PrivateKey
from ckcc_protocol.constants import *
@pytest.mark.parametrize('mode', ["classic", 'segwit'])
@pytest.mark.parametrize('mode', ["classic", 'segwit', 'taproot'])
@pytest.mark.parametrize('pdf', [False, True])
@pytest.mark.parametrize('netcode', ["XTN", "BTC"])
@pytest.mark.parametrize('netcode', ["XRT", "BTC", "XTN"])
def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home, cap_story,
need_keypress, microsd_path, verify_detached_signature_file, settings_set,
press_select):
press_select, validate_address, bitcoind):
# test UX and operation of the 'bitcoin core' wallet export
mx = "Don't make PDF"
@ -26,10 +26,7 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
goto_home()
pick_menu_item('Advanced/Tools')
try:
pick_menu_item('Paper Wallets')
except:
raise pytest.skip('Feature absent')
pick_menu_item('Paper Wallets')
time.sleep(0.1)
title, story = cap_story()
@ -45,6 +42,11 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
pick_menu_item('Segwit P2WPKH')
time.sleep(0.5)
if mode == 'taproot':
pick_menu_item('Classic P2PKH')
pick_menu_item('Taproot P2TR')
time.sleep(0.5)
if pdf:
assert mx in cap_menu()
shutil.copy('../docs/paperwallet.pdf', microsd_path('paperwallet.pdf'))
@ -58,7 +60,7 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
time.sleep(0.1)
title, story = cap_story()
if "Press (1) to save paper wallet file to SD Card" in story:
if "Press (1)" in story:
need_keypress("1")
time.sleep(0.2)
title, story = cap_story()
@ -68,20 +70,32 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
story = [i for i in story.split('\n') if i]
sig_file = story[-1]
if not pdf:
fname = story[-2]
fnames = [fname]
if mode == "taproot":
fname = story[-1]
else:
fname = story[-2]
fnames = [fname]
else:
fname = story[-3]
pdf_name = story[-2]
fnames = [fname, pdf_name]
if mode == "taproot":
fname = story[-2]
pdf_name = story[-1]
else:
fname = story[-3]
pdf_name = story[-2]
fnames = [fname, pdf_name]
assert pdf_name.endswith('.pdf')
assert fname.endswith('.txt')
assert sig_file.endswith(".sig")
verify_detached_signature_file(fnames, sig_file, "sd",
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
if mode != 'taproot':
assert sig_file.endswith(".sig")
verify_detached_signature_file(fnames, sig_file, "sd",
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
path = microsd_path(fname)
_wif = None
_sk = None
_addr = None
_idesc = None
with open(path, 'rt') as fp:
hdr = None
for ln in fp:
@ -98,27 +112,46 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
val = ln.strip()
if 'Deposit address' in hdr:
assert val == fname.split('.', 1)[0].split('-', 1)[0]
txt_addr = val
addr = val
_addr = val
elif hdr == 'Private key:': # for QR case
assert val == wif
assert val == _wif
elif 'Private key' in hdr and 'WIF=Wallet' in hdr:
wif = val
k1 = PrivateKey.from_wif(val)
_wif = val
elif 'Private key' in hdr and 'Hex, 32 bytes' in hdr:
k2 = PrivateKey(sec_exp=a2b_hex(val))
_sk = val
elif 'Bitcoin Core command':
assert wif in val
assert 'importmulti' in val or 'importprivkey' in val
assert _wif in val
if 'importdescriptors' in val:
_idesc = val
assert 'importprivkey' in val or 'importdescriptors' in val
else:
print(f'{hdr} => {val}')
raise ValueError(hdr)
assert k1.K.sec() == k2.K.sec()
assert addr == k1.K.address(addr_fmt="p2wpkh" if mode == "segwit" else "p2pkh",
testnet=True if netcode == "XTN" else False)
if netcode != "XRT":
from bip32 import PrivateKey
k1 = PrivateKey.from_wif(_wif)
k2 = PrivateKey.parse(a2b_hex(_sk))
assert k1 == k2
validate_address(_addr, k1)
else:
if mode == "segwit":
assert _addr.startswith("bcrt1q")
elif mode == "taproot":
assert _addr.startswith("bcrt1p")
else:
assert _addr[0] in "mn"
os.unlink(path)
# bitcoind on regtest
conn = bitcoind.create_wallet(wallet_name="paper", disable_private_keys=False, blank=True,
passphrase=None, avoid_reuse=False, descriptors=True)
desc_obj_s, desc_obj_e = _idesc.find("["), _idesc.find("]") + 1
desc_obj = json.loads(_idesc[desc_obj_s:desc_obj_e])
desc = desc_obj[0]["desc"]
res = conn.importdescriptors(desc_obj)
assert res[0]["success"]
assert _addr == conn.deriveaddresses(desc)[0]
bitcoind.delete_wallet_files()
if not pdf: return
@ -126,8 +159,8 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
with open(path, 'rb') as fp:
d = fp.read()
assert wif.encode('ascii') in d
assert txt_addr.encode('ascii') in d
assert _wif.encode('ascii') in d
assert _addr.encode('ascii') in d
os.unlink(path)
@ -276,7 +309,7 @@ def test_dice_generate(rolls, testnet, dev, cap_menu, pick_menu_item, goto_home,
val, = hx
k2 = PrivateKey(sec_exp=a2b_hex(val))
assert addr == k2.K.address(testnet=testnet, addr_fmt="p2pkh")
assert addr == k2.K.address(chain="XTN" if testnet else "BTC", addr_fmt="p2pkh")
assert val == sha256(rolls.encode('ascii')).hexdigest()

View File

@ -12,7 +12,7 @@ from pprint import pprint, pformat
from decimal import Decimal
from base64 import b64encode, b64decode
from base58 import encode_base58_checksum
from helpers import B2A, U2SAT, prandom, fake_dest_addr, make_change_addr, parse_change_back
from helpers import B2A, fake_dest_addr, parse_change_back
from helpers import xfp2str, seconds2human_readable, hash160
from msg import verify_message
from bip32 import BIP32Node
@ -133,8 +133,8 @@ def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec):
assert oo == rb
@pytest.mark.unfinalized
def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
press_select):
@pytest.mark.parametrize("taproot", [True, False])
def test_speed_test(dev, taproot, fake_txn, is_mark3, is_mark4, start_sign, end_sign, press_select):
# measure time to sign a larger txn
if is_mark4:
# Mk4: expect
@ -149,7 +149,10 @@ def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
num_in = 9
num_out = 100
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=True)
if taproot:
psbt = fake_txn(num_in, num_out, dev.master_xpub, taproot_in=True)
else:
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=True)
open('debug/speed.psbt', 'wb').write(psbt)
dt = time.time()
@ -191,8 +194,9 @@ if 0:
@pytest.mark.bitcoind
@pytest.mark.veryslow
@pytest.mark.parametrize('segwit', [True, False])
def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
start_sign, end_sign, dev, segwit, accept = True):
@pytest.mark.parametrize('taproot', [True, False])
def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, is_mark3, is_mark4,
start_sign, end_sign, dev, segwit, taproot, accept=True):
# try a bunch of different bigger sized txns
# - important to test on real device, due to it's limited memory
@ -209,7 +213,8 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
num_in = 250
num_out = 2000
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit, outstyles=ADDR_STYLES)
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit,
taproot_in=taproot, outstyles=ADDR_STYLES)
open('debug/last.psbt', 'wb').write(psbt)
@ -262,11 +267,13 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
@pytest.mark.bitcoind
@pytest.mark.parametrize('num_ins', [ 2, 7, 15 ])
@pytest.mark.parametrize('segwit', [True, False])
def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit, decode_with_bitcoind):
@pytest.mark.parametrize('taproot', [True, False])
def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit,taproot,
decode_with_bitcoind):
# create a TXN using actual addresses that are correct for DUT
xp = dev.master_xpub
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit)
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit, taproot_in=taproot)
open('debug/real-%d.psbt' % num_ins, 'wb').write(psbt)
_, txn = try_sign(psbt, accept=True, finalize=True)
@ -908,16 +915,17 @@ def test_network_fee_unlimited(fake_txn, start_sign, end_sign, dev, settings_set
@pytest.mark.parametrize('num_outs', [ 2, 7, 15 ])
@pytest.mark.parametrize('act_outs', [ 2, 1, -1])
@pytest.mark.parametrize('segwit', [True, False])
@pytest.mark.parametrize('taproot', [True, False])
@pytest.mark.parametrize('add_xpub', [True, False])
@pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE)
@pytest.mark.parametrize('visualized', [0, STXN_VISUALIZE, STXN_VISUALIZE|STXN_SIGNED])
def test_change_outs(fake_txn, start_sign, end_sign, cap_story, dev, num_outs, master_xpub,
act_outs, segwit, out_style, visualized, add_xpub, num_ins=3):
act_outs, segwit, taproot, out_style, visualized, add_xpub, num_ins=3):
# create a TXN which has change outputs, which shouldn't be shown to user, and also not fail.
xp = dev.master_xpub
couts = num_outs if act_outs == -1 else num_ins-act_outs
psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit,
psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit, taproot_in=taproot,
outstyles=[out_style], change_outputs=range(couts), add_xpub=add_xpub)
open('debug/change.psbt', 'wb').write(psbt)
@ -1083,8 +1091,8 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind
("45'/1'/0'/1/5", 'diff path prefix'),
("44'/2'/0'/1/5", 'diff path prefix'),
("44'/1'/1'/1/5", 'diff path prefix'),
("44'/1'/0'/3000/5", '2nd last component'),
("44'/1'/0'/3/5", '2nd last component'),
# ("44'/1'/0'/3000/5", '2nd last component'),
# ("44'/1'/0'/3/5", '2nd last component'),
])
def test_change_troublesome(dev, start_sign, cap_story, try_path, expect):
# NOTE: out#1 is change:
@ -1209,8 +1217,10 @@ def hist_count(sim_exec):
@pytest.mark.parametrize('num_utxo', [9, 100])
@pytest.mark.parametrize('segwit_in', [False, True])
def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, settings_set,
settings_get, cap_story, sim_exec, hist_count):
@pytest.mark.parametrize('taproot_in', [False, True])
def test_bip143_attack_data_capture(num_utxo, segwit_in, taproot_in, try_sign, fake_txn,
settings_set, settings_get, cap_story, sim_exec,
hist_count):
# cleanup prev runs, if very first time thru
sim_exec('import history; history.OutptValueCache.clear()')
@ -1219,12 +1229,13 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set
# make a txn, capture the outputs of that as inputs for another txn
psbt = fake_txn(1, num_utxo+3, segwit_in=segwit_in, change_outputs=range(num_utxo+2),
outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh'])
taproot_in=taproot_in,
outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh'])
_, txn = try_sign(psbt, accept=True, finalize=True)
open('debug/funding.psbt', 'wb').write(psbt)
num_inp_utxo = (1 if segwit_in else 0)
num_inp_utxo = (1 if (segwit_in or taproot_in) else 0)
time.sleep(.1)
title, story = cap_story()
@ -1268,12 +1279,15 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set
@pytest.mark.parametrize('segwit', [False, True])
@pytest.mark.parametrize('taproot', [False, True])
@pytest.mark.parametrize('num_ins', [1, 17])
def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind, cap_story):
@pytest.mark.parametrize('num_outs', [1, 17])
def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind,
cap_story, taproot, num_outs):
# verify correct txid for transactions is being calculated
xp = dev.master_xpub
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit)
psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit, taproot_in=taproot)
_, txn = try_sign(psbt, accept=True, finalize=True)
@ -1320,7 +1334,7 @@ def test_sdcard_signing(encoding, num_outs, del_after, partial, try_sign_microsd
pp = psbt.inputs[0].bip32_paths[pk]
psbt.inputs[0].bip32_paths[pk] = b'what' + pp[4:]
psbt = fake_txn(2, num_outs, xp, segwit_in=True, psbt_hacker=hack)
psbt = fake_txn(3, num_outs, xp, segwit_in=True, taproot_in=True, psbt_hacker=hack)
_, txn, txid = try_sign_microsd(psbt, finalize=not partial,
encoding=encoding, del_after=del_after)
@ -1352,7 +1366,8 @@ def test_payjoin_signing(num_ins, num_outs, fake_txn, try_sign, start_sign, end_
txn = end_sign(True, finalize=False)
@pytest.mark.parametrize('segwit', [False, True])
def test_fully_unsigned(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('taproot', [False, True])
def test_fully_unsigned(fake_txn, try_sign, segwit, taproot):
# A PSBT which is unsigned but all inputs lack keypaths
@ -1360,8 +1375,9 @@ def test_fully_unsigned(fake_txn, try_sign, segwit):
# change all inputs to be "not ours" ... but with utxo details
for i in psbt.inputs:
i.bip32_paths.clear()
i.taproot_bip32_paths.clear()
psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack)
psbt = fake_txn(7, 2, segwit_in=segwit, taproot_in=taproot, psbt_hacker=hack)
with pytest.raises(CCProtoError) as ee:
orig, result = try_sign(psbt, accept=True)
@ -1369,7 +1385,8 @@ def test_fully_unsigned(fake_txn, try_sign, segwit):
assert 'does not contain any key path information' in str(ee)
@pytest.mark.parametrize('segwit', [False, True])
def test_wrong_xfp(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('taproot', [False, True])
def test_wrong_xfp(fake_txn, try_sign, segwit, taproot):
# A PSBT which is unsigned and doesn't involve our XFP value
@ -1380,8 +1397,10 @@ def test_wrong_xfp(fake_txn, try_sign, segwit):
for i in psbt.inputs:
for pubkey in i.bip32_paths:
i.bip32_paths[pubkey] = wrong_xfp + i.bip32_paths[pubkey][4:]
for xonly_pubkey in i.taproot_bip32_paths:
i.taproot_bip32_paths[xonly_pubkey] = b"\x00" + wrong_xfp + i.taproot_bip32_paths[xonly_pubkey][5:]
psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack)
psbt = fake_txn(7, 2, segwit_in=segwit, taproot_in=taproot, psbt_hacker=hack)
with pytest.raises(CCProtoError) as ee:
orig, result = try_sign(psbt, accept=True)
@ -1390,7 +1409,8 @@ def test_wrong_xfp(fake_txn, try_sign, segwit):
assert 'found 12345678' in str(ee)
@pytest.mark.parametrize('segwit', [False, True])
def test_wrong_xfp_multi(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('taproot', [False, True])
def test_wrong_xfp_multi(fake_txn, try_sign, segwit, taproot):
# A PSBT which is unsigned and doesn't involve our XFP value
# - but multiple wrong XFP values
@ -1404,8 +1424,12 @@ def test_wrong_xfp_multi(fake_txn, try_sign, segwit):
here = struct.pack('<I', idx+73)
i.bip32_paths[pubkey] = here + i.bip32_paths[pubkey][4:]
wrongs.add(xfp2str(idx+73))
for xonly_pubkey in i.taproot_bip32_paths:
here = struct.pack('<I', idx + 73)
i.taproot_bip32_paths[xonly_pubkey] = b"\x00" + here + i.taproot_bip32_paths[xonly_pubkey][5:]
wrongs.add(xfp2str(idx + 73))
psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack)
psbt = fake_txn(7, 2, segwit_in=segwit, taproot_in=taproot, psbt_hacker=hack)
open('debug/wrong-xfp.psbt', 'wb').write(psbt)
with pytest.raises(CCProtoError) as ee:
@ -1420,15 +1444,16 @@ def test_wrong_xfp_multi(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE)
@pytest.mark.parametrize('segwit', [False, True])
@pytest.mark.parametrize('taproot', [False, True])
@pytest.mark.parametrize('outval', ['.5', '.788888', '0.92640866'])
def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, dev):
def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, dev, taproot):
# check how we render the value of outputs
# - works on simulator and connected USB real-device
xp = dev.master_xpub
oi = int(Decimal(outval) * int(1E8))
psbt = fake_txn(1, 2, dev.master_xpub, segwit_in=segwit, outvals=[oi, int(1E8-oi)],
outstyles=[out_style], change_outputs=[1])
taproot_in=taproot, outstyles=[out_style], change_outputs=[1])
open('debug/render.psbt', 'wb').write(psbt)
@ -1461,6 +1486,8 @@ def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign,
elif out_style == 'p2wpkh-p2sh':
assert len(set(i[0] for i in addrs)) == 1
assert addrs[0][0] in {'2', '3'}
elif out_style == 'p2tr':
assert all(i.startswith(("bc1p", "tb1p", "bcrt1p")) for i in addrs)
def test_negative_fee(dev, fake_txn, try_sign):
@ -1643,7 +1670,8 @@ def test_zero_xfp(dev, start_sign, end_sign, fake_txn, cap_story):
@pytest.mark.parametrize("segwit_in", [True, False])
@pytest.mark.parametrize('num_not_ours', [1, 3, 4])
def test_foreign_utxo_missing(segwit_in, num_not_ours, dev, fake_txn, start_sign, cap_story, end_sign):
def test_foreign_utxo_missing(segwit_in, num_not_ours, dev, fake_txn, start_sign,
cap_story, end_sign):
def hack(psbt):
# change first input to not be ours
for i in range(num_not_ours):
@ -1666,16 +1694,17 @@ def test_foreign_utxo_missing(segwit_in, num_not_ours, dev, fake_txn, start_sign
assert signed != psbt
@pytest.mark.parametrize("segwit_in", [True, False])
@pytest.mark.parametrize("taproot_in", [True, False])
@pytest.mark.parametrize("num_missing", [1, 3, 4])
def test_own_utxo_missing(segwit_in, num_missing, dev, fake_txn, start_sign, cap_story, end_sign,
press_cancel):
press_cancel, taproot_in):
def hack(psbt):
for i in range(num_missing):
# no utxo provided for our input
psbt.inputs[i].utxo = None
psbt.inputs[i].witness_utxo = None
psbt = fake_txn(5, 2, dev.master_xpub, segwit_in=segwit_in, psbt_hacker=hack)
psbt = fake_txn(5, 2, dev.master_xpub, segwit_in=segwit_in, taproot_in=taproot_in, psbt_hacker=hack)
start_sign(psbt)
time.sleep(.1)
title, story = cap_story()
@ -1693,17 +1722,22 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
alice = bitcoind.create_wallet(wallet_name="alice")
bob = bitcoind.create_wallet(wallet_name="bob")
cc = bitcoind_d_sim_watch
tap_dave = bitcoind.create_wallet(wallet_name="tap_dave")
alice_addr = alice.getnewaddress()
alice_pubkey = alice.getaddressinfo(alice_addr)["pubkey"]
bob_addr = bob.getnewaddress()
bob_pubkey = bob.getaddressinfo(bob_addr)["pubkey"]
cc_addr = cc.getnewaddress()
cc_pubkey = cc.getaddressinfo(cc_addr)["pubkey"]
tap_dave_addr = tap_dave.getnewaddress("", "bech32m")
# fund all addresses
for addr in (alice_addr, bob_addr, cc_addr):
bitcoind.supply_wallet.generatetoaddress(101, addr)
for addr in (alice_addr, bob_addr, cc_addr, tap_dave_addr):
bitcoind.supply_wallet.sendtoaddress(addr, 2)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt_list = []
for w in (alice, bob, cc):
for w in (alice, bob, cc, tap_dave):
assert w.listunspent()
psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
psbt_list.append(psbt)
@ -1718,6 +1752,9 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
assert pk.hex() in (alice_pubkey, bob_pubkey)
inp.utxo = None
inp.witness_utxo = None
for xo_pk, _ in inp.taproot_bip32_paths.items():
inp.utxo = None
inp.witness_utxo = None
psbt0 = the_psbt_obj.as_bytes()
orig, res = try_sign(psbt0, accept=True)
@ -1726,10 +1763,12 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
# lets sign with bob first - bobs wallet will ignore missing alice UTXO but will supply his UTXO
psbt1 = bob.walletprocesspsbt(base64.b64encode(res).decode(), True, "ALL")["psbt"]
# finally sign with alice
res = alice.walletprocesspsbt(psbt1, True, "ALL")
res = alice.walletprocesspsbt(psbt1, True)
psbt2 = res["psbt"]
res = tap_dave.walletprocesspsbt(psbt2, True)
psbt3 = res["psbt"]
assert res["complete"] is True
tx = alice.finalizepsbt(psbt2)["hex"]
tx = alice.finalizepsbt(psbt3)["hex"]
assert alice.testmempoolaccept([tx])[0]["allowed"] is True
tx_id = alice.sendrawtransaction(tx)
assert isinstance(tx_id, str) and len(tx_id) == 64
@ -1747,7 +1786,8 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, bitcoind, start_sign, end_sign, cap_story):
cc = bitcoind_d_sim_watch
dest_address = cc.getnewaddress()
bitcoind.supply_wallet.generatetoaddress(101, dest_address)
bitcoind.supply_wallet.sendtoaddress(dest_address, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt = cc.walletcreatefundedpsbt([], [{dest_address: 1.0}, {"data": op_return_data.hex()}], 0, {"fee_rate": 20})["psbt"]
start_sign(base64.b64decode(psbt))
time.sleep(.1)
@ -1855,7 +1895,7 @@ def test_duplicate_unknow_values_in_psbt(dev, start_sign, end_sign, fake_txn):
def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
bitcoind, bitcoind_d_dev_watch, settings_set, finalize_v2_v0_convert):
def doit(addr_fmt, sighash, num_inputs=2, num_outputs=2, consolidation=False, sh_checks=False,
psbt_v2=False, tx_check=True):
psbt_v2=False):
from decimal import Decimal, ROUND_DOWN
@ -1874,6 +1914,8 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
not_all_ALL = any(sh != "ALL" for sh in sighash)
stranger = bitcoind.create_wallet(f"{os.urandom(10).hex()}")
bitcoind_d_dev_watch.keypoolrefill(num_inputs + num_outputs)
input_val = bitcoind.supply_wallet.getbalance() / num_inputs
cc_dest = [
@ -1892,7 +1934,7 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
unspent = bitcoind_d_dev_watch.listunspent()
output_val = bitcoind_d_dev_watch.getbalance() / num_outputs
# consolidation or not?
dest_wal = bitcoind_d_dev_watch if consolidation else bitcoind.supply_wallet
dest_wal = bitcoind_d_dev_watch if consolidation else stranger # using stranger here as supply+wallet is legacy and has no tr addresses
destinations = [
{dest_wal.getnewaddress("", addr_fmt): Decimal(output_val).quantize(Decimal('.0000001'), rounding=ROUND_DOWN)}
for _ in range(num_outputs)
@ -1985,11 +2027,13 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
assert resp["complete"] is True
tx_hex = resp["hex"]
if tx_check:
# sign and get finalized tx ready for broadcast out
start_sign(psbt_sh_bytes, finalize=True)
cc_tx_hex = end_sign(accept=True, finalize=True)
assert tx_hex == cc_tx_hex.hex()
# sign again - this time get finalized tx ready for broadcast out
start_sign(psbt_sh_bytes, finalize=True)
cc_tx_hex = end_sign(accept=True, finalize=True).hex()
if addr_fmt != "bech32m":
# schnorr signatures are not deterministic
# any subsequent sign will produce different witness
assert tx_hex == cc_tx_hex
if psbt_v2:
# check txn_modifiable properly set
@ -2015,16 +2059,16 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
assert mod & 4 == 0
# for PSBTv2 here we check if we correctly finalize
res = bitcoind.supply_wallet.testmempoolaccept([tx_hex])
res = bitcoind.supply_wallet.testmempoolaccept([cc_tx_hex])
assert res[0]["allowed"]
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
txn_id = bitcoind.supply_wallet.sendrawtransaction(cc_tx_hex)
assert txn_id
return doit
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32", "bech32m"])
@pytest.mark.parametrize("sighash", [sh for sh in SIGHASH_MAP if sh != 'ALL'])
@pytest.mark.parametrize("num_outs", [1, 3, 5])
@pytest.mark.parametrize("num_ins", [2, 5])
@ -2036,7 +2080,7 @@ def test_sighash_same(addr_fmt, sighash, num_ins, num_outs, psbt_v2, _test_singl
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32", "bech32m"])
@pytest.mark.parametrize("sighash", list(itertools.combinations(SIGHASH_MAP.keys(), 2)))
@pytest.mark.parametrize("num_outs", [2, 3, 5])
@pytest.mark.parametrize("psbt_v2", [True, False])
@ -2047,7 +2091,7 @@ def test_sighash_different(addr_fmt, sighash, num_outs, psbt_v2, _test_single_si
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32", "bech32m"])
@pytest.mark.parametrize("num_outs", [5, 8])
@pytest.mark.parametrize("psbt_v2", [True, False])
def test_sighash_fullmix(addr_fmt, num_outs, psbt_v2, _test_single_sig_sighash):
@ -2114,7 +2158,7 @@ def test_send2taproot_addresss(fake_txn , start_sign, end_sign, cap_story, use_t
assert title == "OK TO SEND?"
# we do not understand change in taproot (taproot not supported)
assert "Consolidating" not in story
assert "Change back" not in story
assert "Change back" in story
# but we should show address
assert "to script" not in story
assert "tb1p" in story
@ -2929,10 +2973,11 @@ def test_sorting_outputs_by_size(fake_txn, start_sign, cap_story, use_testnet,
@pytest.mark.parametrize("chain", ["BTC", "XTN"])
@pytest.mark.parametrize("data", [
# (out_style, amount, is_change)
[("p2tr", 999999, 1)] + [("p2tr", 888888, 0)] * 12,
[("p2pkh", 1000000, 0)] * 99,
[("p2wpkh", 1000000, 1),("p2wpkh-p2sh", 800000, 1)] * 27,
[("p2wpkh", 1000000, 1),("p2wpkh-p2sh", 800000, 1), ("p2tr", 600000, 1)] * 27,
[("p2pkh", 1000000, 1)] * 11 + [("p2wpkh", 50000000, 0)] * 16,
[("p2pkh", 1000000, 1), ("p2wpkh", 50000000, 0), ("p2wpkh-p2sh", 800000, 1)] * 11,
[("p2pkh", 1000000, 1), ("p2wpkh", 50000000, 0), ("p2wpkh-p2sh", 800000, 1), ("p2tr", 100000, 0)] * 11,
])
def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign,
settings_set, txout_explorer, cap_story):
@ -3033,3 +3078,266 @@ def test_null_data_op_return(fake_txn, start_sign, end_sign, reset_seed_words):
assert "OP_RETURN" in story
# EOF
@pytest.mark.bitcoind
def test_taproot_keyspend(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, microsd_path, cap_story, goto_home,
press_select, pick_menu_item, bitcoind):
use_regtest()
sim = bitcoind_d_sim_watch
sim.keypoolrefill(10)
addr = sim.getnewaddress("", "bech32m")
bitcoind.supply_wallet.sendtoaddress(addr, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
dest_addr = sim.getnewaddress("", "bech32m") # self-spend
psbt_resp = sim.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr.psbt"
open('debug/last.psbt', 'w').write(psbt)
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert 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:]
assert len(addrs) == 2
for addr in addrs:
assert addr.startswith("bcrt1p")
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
open('debug/last.psbt', 'w').write(psbt)
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
addr_segwit = sim.getnewaddress("", "bech32")
sim.generatetoaddress(1, addr_segwit) # mine transaction sent and also new coins to p2wpkh
addr_nested_segwit = sim.getnewaddress("", "p2sh-segwit")
sim.generatetoaddress(1, addr_nested_segwit)
addr_legacy = sim.getnewaddress("", "legacy")
sim.generatetoaddress(1, addr_legacy)
# try to sign tx with all input types (legacy, nested segwit, native segwit, taproot)
all_of_it = sim.getbalance()
dest_addr0 = sim.getnewaddress("", "bech32m") # self-spend
dest_addr1 = sim.getnewaddress("", "bech32m") # self-spend
dest_addr2 = sim.getnewaddress("", "bech32m") # self-spend
chunk = round(all_of_it / 3, 6)
psbt_resp = sim.walletcreatefundedpsbt([], [{dest_addr0: chunk}, {dest_addr1: chunk}, {dest_addr2: chunk}],
0, {'subtractFeeFromOutputs': [0], "fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr-all.psbt"
open('debug/last.psbt', 'w').write(psbt)
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" in story # self-spend
assert " 2 inputs\n 3 outputs" in story
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
open('debug/last.psbt', 'w').write(psbt)
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
# multi p2tr output consolidation
addr_segwit = sim.getnewaddress("", "bech32")
sim.generatetoaddress(1, addr_segwit) # mine transaction sent and also new coins to p2wpkh
all_of_it = sim.getbalance()
dest_addr = sim.getnewaddress("", "bech32m")
psbt_resp = sim.walletcreatefundedpsbt([], [{dest_addr: all_of_it}],
0, {'subtractFeeFromOutputs': [0], "fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr-multi-out-consolidation.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" in story # self-spend
assert " 3 inputs\n 1 output" in story
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
open('debug/last.psbt', 'w').write(psbt)
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
# send it all to bob, he's a good guy
bob_w = bitcoind.create_wallet("bob")
dst = bob_w.getnewaddress("", "bech32m")
all_of_it = sim.getbalance()
psbt_resp = sim.walletcreatefundedpsbt([], [{dst: all_of_it}],
0, {'subtractFeeFromOutputs': [0], "fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr2bob.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" not in story # NOT a self-spend
assert "to address" in story
assert dst in story
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
@pytest.mark.parametrize('fn_err_msg', [
('data/taproot/in_internal_key_len.psbt', 'PSBT_IN_TAP_INTERNAL_KEY length != 32'),
('data/taproot/in_key_pth_sig_len.psbt', 'PSBT_IN_TAP_KEY_SIG length != 64 or 65'),
('data/taproot/in_key_pth_sig_len1.psbt', 'PSBT_IN_TAP_KEY_SIG length != 64 or 65'),
('data/taproot/in_tr_deriv_key_len.psbt', 'PSBT_IN_TAP_BIP32_DERIVATION xonly-pubkey length != 32'),
('data/taproot/in_script_sig_key_len.psbt', 'PSBT_IN_TAP_SCRIPT_SIG key length != 64'),
('data/taproot/in_script_sig_sig_len.psbt', 'PSBT_IN_TAP_SCRIPT_SIG signature length != 64 or 65'),
('data/taproot/in_script_sig_sig_len1.psbt', 'PSBT_IN_TAP_SCRIPT_SIG signature length != 64 or 65'),
('data/taproot/in_leaf_script_cb_len.psbt', 'PSBT_IN_TAP_LEAF_SCRIPT control block is not valid'),
('data/taproot/in_leaf_script_cb_len1.psbt', 'PSBT_IN_TAP_LEAF_SCRIPT control block is not valid'),
])
def test_invalid_input_taproot_psbt(start_sign, fn_err_msg, cap_story):
fn, err_msg = fn_err_msg
start_sign(fn)
title, story = cap_story()
assert title == "Failure"
assert 'Invalid PSBT' in story
# error messages are disabled to save some space - problem file line is still included
# assert err_msg in story
def test_invalid_output_tapproot_psbt(fake_txn, start_sign, cap_story, dev):
psbt = fake_txn(3, 2, master_xpub=dev.master_xpub, taproot_in=True, outstyles=["p2tr"], change_outputs=[1])
# invalid internal key length
psbt_obj = BasicPSBT().parse(psbt)
for o in psbt_obj.outputs:
o.taproot_internal_key = b"\x03" + b"a" * 32
psbt0 = BytesIO()
psbt_obj.serialize(psbt0)
start_sign(psbt0.getvalue())
title, story = cap_story()
assert title == "Failure"
assert 'Invalid PSBT' in story
# error messages are disabled to save some space - problem file line is still included
# assert "PSBT_OUT_TAP_INTERNAL_KEY length != 32" in story
# invalid internal key length in bip32 taproot paths
psbt_obj = BasicPSBT().parse(psbt)
for o in psbt_obj.outputs:
o.taproot_bip32_paths = {b"\x03" + b"a" * 32: 12 * b"1"}
psbt0 = BytesIO()
psbt_obj.serialize(psbt0)
start_sign(psbt0.getvalue())
title, story = cap_story()
assert title == "Failure"
assert 'Invalid PSBT' in story
# error messages are disabled to save some space - problem file line is still included
# assert "PSBT_IN_TAP_BIP32_DERIVATION xonly-pubkey length != 32" in story

View File

@ -4,7 +4,7 @@
#
import pytest, os, shutil
from helpers import B2A
from helpers import B2A, taptweak
def test_remote_exec(sim_exec):
@ -71,9 +71,13 @@ def test_public(sim_execfile):
assert sk.hwif() == result
elif result[0] in '1mn':
assert result == sk.address()
elif result[0:3] in { 'bc1', 'tb1' }:
elif result[0:4] in {'bc1q', 'tb1q'}:
h20 = sk.hash160()
assert result == bech32.encode(result[0:2], 0, h20)
elif result[0:4] in {'bc1p', 'tb1p'}:
from bech32 import encode
tweked_xonly = taptweak(sk.sec()[1:])
assert result == encode(result[:2], 1, tweked_xonly)
elif result[0] in '23':
h20 = hash160(b'\x00\x14' + sk.hash160())
assert h20 == decode_base58_checksum(result)[1:]

View File

@ -6,7 +6,7 @@ import pytest, struct
from ckcc_protocol.protocol import MAX_TXN_LEN
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
from io import BytesIO
from helpers import fake_dest_addr, make_change_addr, hash160
from helpers import fake_dest_addr, make_change_addr, hash160, taptweak
from base58 import decode_base58
from bip32 import BIP32Node
from constants import ADDR_STYLES, simulator_fixed_tprv
@ -24,7 +24,7 @@ def fake_txn(dev, pytestconfig):
invals=None, outvals=None, segwit_in=False, wrapped=False,
outstyles=['p2pkh'], psbt_hacker=None, change_outputs=[],
capture_scripts=None, add_xpub=None, op_return=None,
psbt_v2=None, input_amount=1E8):
psbt_v2=None, input_amount=1E8, taproot_in=False):
psbt = BasicPSBT()
@ -61,7 +61,40 @@ def fake_txn(dev, pytestconfig):
assert len(sec) == 33, "expect compressed"
assert subpath[0:2] == '0/'
psbt.inputs[i].bip32_paths[sec] = xfp + struct.pack('<II', 0, i)
if taproot_in:
tweaked_xonly = taptweak(sec[1:])
if segwit_in and taproot_in:
# if both specified:
# even is segwit v0
# odd is segvit v1 (taproot)
if i % 2 == 0:
psbt.inputs[i].bip32_paths[sec] = xfp + struct.pack('<II', 0, i)
scr = bytes([0x00, 0x14]) + subkey.hash160()
if wrapped:
# p2sh-p2wpkh
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
else:
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + xfp + struct.pack('<II', 0, i)
scr = bytes([81, 32]) + tweaked_xonly
# UTXO that provides the funding for to-be-signed txn
elif taproot_in:
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + xfp + struct.pack('<II', 0, i)
scr = bytes([81, 32]) + tweaked_xonly
else:
psbt.inputs[i].bip32_paths[sec] = xfp + struct.pack('<II', 0, i)
if segwit_in:
# p2wpkh
scr = bytes([0x00, 0x14]) + subkey.hash160()
if wrapped:
# p2sh-p2wpkh
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
else:
# p2pkh
scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes([0x88, 0xac])
# UTXO that provides the funding for to-be-signed txn
supply = CTransaction()
@ -72,17 +105,6 @@ def fake_txn(dev, pytestconfig):
)
supply.vin = [CTxIn(out_point, nSequence=0xffffffff)]
if segwit_in:
# p2wpkh
scr = bytes([0x00, 0x14]) + subkey.hash160()
if wrapped:
# p2sh-p2wpkh
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
else:
# p2pkh
scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes([0x88, 0xac])
supply.vout.append(CTxOut(int(input_amount if not invals else invals[i]), scr))
if segwit_in: