Compare commits
53 Commits
master
...
edge634_fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bfb239faf | ||
|
|
39c6618f98 | ||
|
|
f6e2061949 | ||
|
|
bdfe55586b | ||
|
|
97af165b2f | ||
|
|
56db1def61 | ||
|
|
87be155bc2 | ||
|
|
1d27562d6e | ||
|
|
407bf7b227 | ||
|
|
832787e1e6 | ||
|
|
bac2f52251 | ||
|
|
8311ff73ec | ||
|
|
e02b4bf745 | ||
|
|
69b21353a9 | ||
|
|
dec95d165f | ||
|
|
ae4dfa6c12 | ||
|
|
2c7b10e6a2 | ||
|
|
aa154e1d8c | ||
|
|
128658e1e6 | ||
|
|
282e8fbc28 | ||
|
|
2efa9e7f27 | ||
|
|
a76408b335 | ||
|
|
205b26f392 | ||
|
|
1cdb0900b7 | ||
|
|
053c9165bb | ||
|
|
b07fb8a6b3 | ||
|
|
35c16c1491 | ||
|
|
49de639bac | ||
|
|
1a41164271 | ||
|
|
e344bac322 | ||
|
|
e3f75619d5 | ||
|
|
99ab403f66 | ||
|
|
9b4cc260aa | ||
|
|
fce554257f | ||
|
|
06dde5af49 | ||
|
|
ef2c5a7f1f | ||
|
|
b1fe5e194d | ||
|
|
427cf89975 | ||
|
|
bb87cdd59a | ||
|
|
5b2772a4b0 | ||
|
|
e7f8a1a71e | ||
|
|
f7f41fe6e3 | ||
|
|
47754d1785 | ||
|
|
bf67c5ad7e | ||
|
|
a18938cefd | ||
|
|
f618af12d1 | ||
|
|
d2920d1c60 | ||
|
|
a798e96de0 | ||
|
|
89405e819a | ||
|
|
eef1f6d561 | ||
|
|
98db85f2e2 | ||
|
|
e1ff15bab4 | ||
|
|
991965ac60 |
@ -319,7 +319,7 @@ def doit(keydir, outfn=None, build_dir=None, high_water=False,
|
|||||||
pubkey_num=pubkey_num,
|
pubkey_num=pubkey_num,
|
||||||
timestamp=timestamp(backdate) )
|
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:
|
if hw_compat & MK_3_OK:
|
||||||
# actual file length limited by size of SPI flash area reserved to txn data/uploads
|
# actual file length limited by size of SPI flash area reserved to txn data/uploads
|
||||||
|
|||||||
@ -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
|
- `p2wsh-p2sh`: _redeemScript_ (which is: `0x00 + 0x20 + sha256(witnessScript)`), and
|
||||||
_witnessScript_ (which contains the multisig script)
|
_witnessScript_ (which contains the multisig script)
|
||||||
- `p2wsh`: only _witnessScript_ (which contains the actual 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
|
# Derivation Paths
|
||||||
|
|
||||||
|
|||||||
27
docs/miniscript.md
Normal file
27
docs/miniscript.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Miniscript
|
||||||
|
|
||||||
|
**COLDCARD<sup>®</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
75
docs/taproot.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Taproot
|
||||||
|
|
||||||
|
**COLDCARD<sup>®</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
45
releases/EdgeChangeLog.md
Normal 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
73
releases/History-Edge.md
Normal 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
|
||||||
@ -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
|
# 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
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
- tbd
|
## 5.4.1 - 2024-??-??
|
||||||
|
|
||||||
|
|
||||||
## 5.4.? - 2024-??-??
|
|
||||||
|
|
||||||
- tbd
|
|
||||||
|
|
||||||
|
- Enhancement: Export single sig descriptor with simple QR
|
||||||
|
|
||||||
|
|
||||||
# Q Specific Changes
|
# 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.
|
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||||
|
|||||||
@ -2,104 +2,33 @@
|
|||||||
Hash: SHA256
|
Hash: SHA256
|
||||||
|
|
||||||
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
||||||
97107b5be1c8b65efa4bd36b7d1798e4ed15917861bd2d40784d66302a61d335 Next-ChangeLog.md
|
1421e5c7a275a6b1e585460e5358292adfc6c0660315b318a1e86d2d6fdad9a3 Next-ChangeLog.md
|
||||||
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
|
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
|
||||||
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
|
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
|
||||||
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
||||||
|
d7738a68e64215ed512854cfb7daf52302047bb3683a2a9b6620bc51a292a65f History-Edge.md
|
||||||
|
e5fbd8b5384b2afd1522d6d2d362b482be3e66123dd68ecb1033d1aa57d0b5e6 EdgeChangeLog.md
|
||||||
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce ChangeLog.md
|
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce ChangeLog.md
|
||||||
237cfcb3fdf9217550eae1d9ea6fc828c1c8d09470bd60c9f72f9b00a3bb2d11 2024-09-12T1734-v5.4.0-mk4-coldcard.dfu
|
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
|
||||||
6d1178f07d543e1777dbbdca41d872b00ca9c40e0c0c1ffb8ef96e19c51daa52 2024-09-12T1734-v5.4.0-mk4-coldcard-factory.dfu
|
73f31fbcb064a6b763d50852aafcdff01d7ec72906b5cb0af6cf28328fd80a89 2024-12-18T1413-v6.3.4X-mk4-coldcard-factory.dfu
|
||||||
d840fa4e83ebc7b0f961f30f68d795bed61271e2314dda4ab0eb0b8bfe7192f4 2024-09-12T1733-v1.3.0Q-q1-coldcard.dfu
|
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
|
||||||
4db89ecffa1376bfc68a37110c2041a29afe52b005d527ecde701131168fc19c 2024-09-12T1733-v1.3.0Q-q1-coldcard-factory.dfu
|
7e284bcead1f9c2f468230a588ddf62064014682772a552d05f453d91d55b6ae 2024-12-18T1407-v6.3.4QX-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
|
|
||||||
a9d0b416c3cb4f122f2826283fce82bbc5fe4464817b601a3a5787b1f8aaba20 2024-01-18T1507-v6.2.2X-mk4-coldcard.dfu
|
a9d0b416c3cb4f122f2826283fce82bbc5fe4464817b601a3a5787b1f8aaba20 2024-01-18T1507-v6.2.2X-mk4-coldcard.dfu
|
||||||
4651fb81dc04ac07ae53535f4246ef7f32611c50853de9edaefa68f3c64e1fac 2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
|
cc93209e800bc05386b5613969e62c27b9acd4388e3a922686525da90a505778 2024-01-18T1507-v6.2.2X-mk4-coldcard-factory.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
|
|
||||||
f4457dc44d08cbed9517e6260aa7163ecc254457276d3cdb0c2611af0f49ba9b 2023-10-26T1343-v6.2.1X-mk4-coldcard.dfu
|
f4457dc44d08cbed9517e6260aa7163ecc254457276d3cdb0c2611af0f49ba9b 2023-10-26T1343-v6.2.1X-mk4-coldcard.dfu
|
||||||
1dcfb450f81883afe8f655239f06e238de7bae51e740cd4aa5ae6a0541772ad8 2023-10-26T1343-v6.2.1X-mk4-coldcard-factory.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
|
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
|
2e8ed970f518a476d0b34752ecbad75bab246669aa65de8f43801364c6f5753e 2023-05-12T1316-v6.0.0X-mk4-coldcard.dfu
|
||||||
7aefd5bcce533f15337e83618ebbd42925d336792c82a5ca19a430b209b30b8a 2023-04-07T1330-v5.1.2-mk4-coldcard.dfu
|
8dd5ff029bb2b08c857604f0c9b5773931f6683ee331ecbc35d9ab4c460b745f 2023-05-12T1316-v6.0.0X-mk4-coldcard-factory.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
|
|
||||||
-----BEGIN PGP SIGNATURE-----
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmbjJicACgkQo6MbrVoq
|
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmdi2IQACgkQo6MbrVoq
|
||||||
WxAnMwf/e2kR1aK6AJiriRa1n3XDomw8ivaUQXUApmK0kawBhVBDLKw5aa3lvTcS
|
WxBj5Af+PoIWKBXGjtO9OfgPJ7H4gqjs159ql1iF8k20R2BYQRLgspHcJeJP7PNc
|
||||||
dg80wnenzNdE/QxctL+FkaZzKYsKbFpstkBEbZKcgbHVcinypKJJfICrhIBVVyZw
|
rhM0NeGcpbljIUgNOZ7I1ZdyJZtuYQAep6/6rOsDz9aCDNcY+E5d8bUHlWMX2qaw
|
||||||
wdhJMGOLEyWMysqfaYMtYJQPkg5nIn0rRxn4yWXIeXAQLcFgdlWzVykqfGZW1xYr
|
lUY+FWN+faZ5SdVg/mlKdiP65Ca0KpY5xd8Nptlgl5U9on+5nwnwBw5TTgXiSFIs
|
||||||
CcVvxMqufXfc6c5aRFQzBO/YVHiRYzvK1NGDPztJEjXYU3zxnExAZFxk0vgpxvE3
|
LBF/sNvaff+7/LXUmXsBq5v32hwUM4Jj4JNg9/LC5VeG5TGkMwLhCjP4HnVwI+WK
|
||||||
CahKfSSTNv54u4CTLxYCdHPRq9OM6yL/w3OUyUQFklCizk2PjrObsJQW4szbbjlx
|
oljtYnHhu2Et7we1wZU2lTH8UsPQR3oADy/YOAJ54SOEKPHzd0y1LUGyLSuZWyka
|
||||||
r7+587Pc5cpJCZn73Q0Y5/SWgnqm4g==
|
8q6/Tp4D/5NKJOwqxmY1v9iZ5s6PHg==
|
||||||
=/h9F
|
=XDmY
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----
|
||||||
|
|||||||
@ -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 make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
|
||||||
from export import generate_unchained_export, generate_electrum_wallet
|
from export import generate_unchained_export, generate_electrum_wallet
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
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 glob import settings
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
from menu import start_chooser, MenuSystem, MenuItem
|
from menu import start_chooser, MenuSystem, MenuItem
|
||||||
@ -872,6 +872,14 @@ async def start_login_sequence():
|
|||||||
# is early in boot process
|
# is early in boot process
|
||||||
print("XFP save failed: %s" % exc)
|
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'))
|
dis.draw_status(xfp=settings.get('xfp'))
|
||||||
|
|
||||||
# If HSM policy file is available, offer to start that,
|
# If HSM policy file is available, offer to start that,
|
||||||
@ -889,6 +897,14 @@ async def start_login_sequence():
|
|||||||
await ar.interact()
|
await ar.interact()
|
||||||
except: pass
|
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):
|
if version.has_nfc and settings.get('nfc', 0):
|
||||||
# Maybe allow NFC now
|
# Maybe allow NFC now
|
||||||
import nfc
|
import nfc
|
||||||
@ -1014,7 +1030,7 @@ async def export_xpub(label, _2, item):
|
|||||||
path = "m"
|
path = "m"
|
||||||
addr_fmt = AF_CLASSIC
|
addr_fmt = AF_CLASSIC
|
||||||
else:
|
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, addr_fmt = chains.CommonDerivations[remap]
|
||||||
path = path.format(account='{acct}', coin_type=chain.b44_cointype, change=0, idx=0)[:-4]
|
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):
|
async def ss_descriptor_skeleton(_0, _1, item):
|
||||||
# Export of descriptor data (wallet)
|
# Export of descriptor data (wallet)
|
||||||
int_ext, addition, f_pattern = None, "", "descriptor.txt"
|
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:
|
if item.arg:
|
||||||
int_ext, allowed_af, ll, f_pattern = item.arg
|
int_ext, allowed_af, ll, f_pattern = item.arg
|
||||||
addition = " for " + ll
|
addition = " for " + ll
|
||||||
@ -1392,7 +1408,7 @@ async def wipe_filesystem(*A):
|
|||||||
if not await ux_confirm('''\
|
if not await ux_confirm('''\
|
||||||
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
|
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, \
|
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.'''):
|
Does not affect MicroSD card, if any.'''):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1731,7 +1747,7 @@ async def bless_flash(*a):
|
|||||||
pa.greenlight_firmware()
|
pa.greenlight_firmware()
|
||||||
|
|
||||||
# redraw our screen
|
# redraw our screen
|
||||||
dis.show()
|
dis.busy_bar(False) # includes dis.show()
|
||||||
|
|
||||||
|
|
||||||
def is_psbt(filename):
|
def is_psbt(filename):
|
||||||
|
|||||||
@ -8,27 +8,17 @@ import chains, stash, version
|
|||||||
from ux import ux_show_story, the_ux, ux_enter_bip32_index
|
from ux import ux_show_story, the_ux, ux_enter_bip32_index
|
||||||
from ux import export_prompt_builder, import_export_prompt_decode
|
from ux import export_prompt_builder, import_export_prompt_decode
|
||||||
from menu import MenuSystem, MenuItem
|
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 multisig import MultisigWallet
|
||||||
|
from miniscript import MiniScriptWallet
|
||||||
from uasyncio import sleep_ms
|
from uasyncio import sleep_ms
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from ubinascii import hexlify as b2a_hex
|
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from auth import write_sig_file
|
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_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT
|
||||||
from charcodes import KEY_CANCEL
|
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):
|
class KeypathMenu(MenuSystem):
|
||||||
def __init__(self, path=None, nl=0):
|
def __init__(self, path=None, nl=0):
|
||||||
@ -41,6 +31,7 @@ class KeypathMenu(MenuSystem):
|
|||||||
MenuItem("m/44h/⋯", f=self.deeper),
|
MenuItem("m/44h/⋯", f=self.deeper),
|
||||||
MenuItem("m/49h/⋯", f=self.deeper),
|
MenuItem("m/49h/⋯", f=self.deeper),
|
||||||
MenuItem("m/84h/⋯", f=self.deeper),
|
MenuItem("m/84h/⋯", f=self.deeper),
|
||||||
|
MenuItem("m/86h/⋯", f=self.deeper),
|
||||||
MenuItem("m/0/{idx}", menu=self.done),
|
MenuItem("m/0/{idx}", menu=self.done),
|
||||||
MenuItem("m/{idx}", menu=self.done),
|
MenuItem("m/{idx}", menu=self.done),
|
||||||
MenuItem("m", f=self.done),
|
MenuItem("m", f=self.done),
|
||||||
@ -67,7 +58,7 @@ class KeypathMenu(MenuSystem):
|
|||||||
pl = p[0:p.rfind('/')].rfind('/')
|
pl = p[0:p.rfind('/')].rfind('/')
|
||||||
else:
|
else:
|
||||||
self.prefix = p # displayed on mk4 only
|
self.prefix = p # displayed on mk4 only
|
||||||
pl = len(p)-2
|
pl = len(p)-2
|
||||||
for mi in items:
|
for mi in items:
|
||||||
mi.arg = mi.label
|
mi.arg = mi.label
|
||||||
mi.label = '⋯'+mi.label[pl:]
|
mi.label = '⋯'+mi.label[pl:]
|
||||||
@ -112,9 +103,8 @@ class PickAddrFmtMenu(MenuSystem):
|
|||||||
def __init__(self, path, parent):
|
def __init__(self, path, parent):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
items = [
|
items = [
|
||||||
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
|
MenuItem(addr_fmt_label(af), f=self.done, arg=(path, af))
|
||||||
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
|
for af in [AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH]
|
||||||
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
|
|
||||||
]
|
]
|
||||||
super().__init__(items)
|
super().__init__(items)
|
||||||
if path.startswith("m/84h"):
|
if path.startswith("m/84h"):
|
||||||
@ -213,7 +203,11 @@ class AddressListMenu(MenuSystem):
|
|||||||
# if they have MS wallets, add those next
|
# if they have MS wallets, add those next
|
||||||
for ms in MultisigWallet.iter_wallets():
|
for ms in MultisigWallet.iter_wallets():
|
||||||
if not ms.addr_fmt: continue
|
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:
|
else:
|
||||||
items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))
|
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
|
settings.put('axi', axi) # update last clicked address
|
||||||
await self.show_n_addresses(path, addr_fmt, None)
|
await self.show_n_addresses(path, addr_fmt, None)
|
||||||
|
|
||||||
async def pick_multisig(self, _1, _2, item):
|
async def pick_miniscript(self, _1, _2, item):
|
||||||
ms_wallet = item.arg
|
msc_wallet = item.arg
|
||||||
settings.put('axi', item.label) # update last clicked address
|
settings.put('axi', item.label) # update last clicked address
|
||||||
await self.show_n_addresses(None, None, ms_wallet)
|
await self.show_n_addresses(None, msc_wallet.addr_fmt, msc_wallet)
|
||||||
|
|
||||||
async def make_custom(self, *a):
|
async def make_custom(self, *a):
|
||||||
# picking a custom derivation path: makes a tree of menus, with chance
|
# 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
|
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.
|
# Build message and CTA about export, plus the actual addresses.
|
||||||
if n:
|
if n:
|
||||||
msg = "Addresses %d⋯%d:\n\n" % (start, min(start + n - 1, MAX_BIP32_IDX))
|
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...')
|
dis.fullscreen('Wait...')
|
||||||
|
|
||||||
if ms_wallet:
|
if ms_wallet:
|
||||||
# IMPORTANT safety feature: never show complete address
|
msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change)
|
||||||
# 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)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# single-signer wallets
|
# single-signer wallets
|
||||||
from wallet import MasterSingleSigWallet
|
from wallet import MasterSingleSigWallet
|
||||||
@ -325,10 +304,9 @@ Press (3) if you really understand and accept these risks.
|
|||||||
# export options
|
# export options
|
||||||
k0 = 'to show change addresses' if allow_change and change == 0 else None
|
k0 = 'to show change addresses' if allow_change and change == 0 else None
|
||||||
export_msg, escape = export_prompt_builder('address summary file',
|
export_msg, escape = export_prompt_builder('address summary file',
|
||||||
no_qr=bool(ms_wallet), key0=k0,
|
key0=k0, force_prompt=True)
|
||||||
force_prompt=True)
|
|
||||||
if version.has_qwerty:
|
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:
|
else:
|
||||||
escape += "79"
|
escape += "79"
|
||||||
|
|
||||||
@ -342,8 +320,8 @@ Press (3) if you really understand and accept these risks.
|
|||||||
|
|
||||||
return msg, addrs, escape
|
return msg, addrs, escape
|
||||||
|
|
||||||
msg, addrs, escape = make_msg()
|
|
||||||
change = 0
|
change = 0
|
||||||
|
msg, addrs, escape = make_msg(change, start)
|
||||||
while 1:
|
while 1:
|
||||||
ch = await ux_show_story(msg, escape=escape)
|
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:
|
elif choice == KEY_QR:
|
||||||
# switch into a mode that shows them as QR codes
|
# switch into a mode that shows them as QR codes
|
||||||
if ms_wallet:
|
|
||||||
# requires not multisig
|
|
||||||
continue
|
|
||||||
|
|
||||||
from ux import show_qr_codes
|
from ux import show_qr_codes
|
||||||
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
|
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
|
||||||
await show_qr_codes(addrs, is_alnum, start)
|
await show_qr_codes(addrs, is_alnum, start)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif NFC and (choice == KEY_NFC):
|
elif NFC and (choice == KEY_NFC):
|
||||||
@ -408,7 +381,7 @@ Press (3) if you really understand and accept these risks.
|
|||||||
else:
|
else:
|
||||||
continue # 3 in non-NFC mode
|
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):
|
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, change=0):
|
||||||
# Produce CSV file contents as a generator
|
# 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
|
from ownership import OWNERSHIP
|
||||||
|
|
||||||
if ms_wallet:
|
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):
|
if (start == 0) and (n > 100) and change in (0, 1):
|
||||||
saver = OWNERSHIP.saver(ms_wallet, change, start)
|
saver = OWNERSHIP.saver(ms_wallet, change, start)
|
||||||
else:
|
else:
|
||||||
saver = None
|
saver = None
|
||||||
|
|
||||||
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
|
for line in ms_wallet.generate_address_csv(start, n, change):
|
||||||
if saver:
|
yield line
|
||||||
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
|
|
||||||
|
|
||||||
if saver:
|
if saver:
|
||||||
saver(None) # close file
|
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)
|
dis.progress_sofar(idx, count or 1)
|
||||||
|
|
||||||
sig_nice = None
|
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
|
derive = path.format(account=account_num, change=change, idx=start) # first addr
|
||||||
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
|
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
|
||||||
|
|
||||||
|
|||||||
122
shared/auth.py
122
shared/auth.py
@ -8,7 +8,7 @@ from ubinascii import b2a_base64, a2b_base64
|
|||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import unhexlify as a2b_hex
|
from ubinascii import unhexlify as a2b_hex
|
||||||
from uhashlib import sha256
|
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 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 public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
|
||||||
from sffile import SFFile
|
from sffile import SFFile
|
||||||
@ -310,6 +310,10 @@ class ApproveMessageSign(UserAuthorizedAction):
|
|||||||
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
|
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
|
||||||
self.approved_cb = approved_cb
|
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
|
from glob import dis
|
||||||
dis.fullscreen('Wait...')
|
dis.fullscreen('Wait...')
|
||||||
|
|
||||||
@ -1431,7 +1435,7 @@ class ShowP2SHAddress(ShowAddressBase):
|
|||||||
# calculate all the pubkeys involved.
|
# calculate all the pubkeys involved.
|
||||||
self.subpath_help = ms.validate_script(witdeem_script, xfp_paths=xfp_paths)
|
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):
|
def get_msg(self):
|
||||||
return '''\
|
return '''\
|
||||||
@ -1447,6 +1451,41 @@ Paths:
|
|||||||
{sp}'''.format(addr=self.address, name=self.ms.name,
|
{sp}'''.format(addr=self.address, name=self.ms.name,
|
||||||
M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help))
|
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):
|
def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script):
|
||||||
# Show P2SH address to user, also returns it.
|
# Show P2SH address to user, also returns it.
|
||||||
# - first need to find appropriate multisig wallet associated
|
# - first need to find appropriate multisig wallet associated
|
||||||
@ -1505,40 +1544,77 @@ def usb_show_address(addr_format, subpath):
|
|||||||
return active_request.address
|
return active_request.address
|
||||||
|
|
||||||
|
|
||||||
class NewEnrollRequest(UserAuthorizedAction):
|
class MiniscriptDeleteRequest(UserAuthorizedAction):
|
||||||
def __init__(self, ms):
|
def __init__(self, msc):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.wallet = ms
|
self.wallet = msc
|
||||||
# self.result ... will be re-serialized xpub
|
|
||||||
|
|
||||||
async def interact(self):
|
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
|
ms = self.wallet
|
||||||
try:
|
try:
|
||||||
ch = await ms.confirm_import()
|
ch = await ms.confirm_import()
|
||||||
|
if ch not in ('y'+KEY_ENTER):
|
||||||
if ch != 'y':
|
|
||||||
# they don't want to!
|
# they don't want to!
|
||||||
self.refused = True
|
self.refused = True
|
||||||
await ux_dramatic_pause("Refused.", 2)
|
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')
|
return await self.failure('No space left')
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
self.failed = "Exception"
|
self.failed = "Exception"
|
||||||
sys.print_exception(exc)
|
sys.print_exception(exc)
|
||||||
finally:
|
finally:
|
||||||
UserAuthorizedAction.cleanup() # because no results to store
|
UserAuthorizedAction.cleanup() # because no results to store
|
||||||
self.pop_menu()
|
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 glob import dis
|
||||||
from multisig import MultisigWallet
|
from multisig import MultisigWallet
|
||||||
|
from miniscript import MiniScriptWallet
|
||||||
|
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
dis.fullscreen('Wait...') # needed
|
dis.fullscreen('Wait...')
|
||||||
dis.busy_bar(True)
|
dis.busy_bar(True)
|
||||||
|
|
||||||
try:
|
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
|
# this call will raise on parsing errors, so let them rise up
|
||||||
# and be shown on screen/over usb
|
# 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:
|
if ux_reset:
|
||||||
# for USB case, and import from PSBT
|
# 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
|
from ux import the_ux
|
||||||
the_ux.push(UserAuthorizedAction.active_request)
|
the_ux.push(UserAuthorizedAction.active_request)
|
||||||
finally:
|
finally:
|
||||||
# always finish busy bar
|
|
||||||
dis.busy_bar(False)
|
dis.busy_bar(False)
|
||||||
|
|
||||||
|
|
||||||
class FirmwareUpgradeRequest(UserAuthorizedAction):
|
class FirmwareUpgradeRequest(UserAuthorizedAction):
|
||||||
def __init__(self, hdr, length, hdr_check=False, psram_offset=None):
|
def __init__(self, hdr, length, hdr_check=False, psram_offset=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
@ -280,7 +280,7 @@ async def restore_tmp_from_dict_ll(vals):
|
|||||||
if not k[:8] == "setting.":
|
if not k[:8] == "setting.":
|
||||||
continue
|
continue
|
||||||
key = k[8:]
|
key = k[8:]
|
||||||
if key in ["multisig"]:
|
if key in ["multisig", "miniscript"]:
|
||||||
# whitelist
|
# whitelist
|
||||||
settings.set(k, v)
|
settings.set(k, v)
|
||||||
|
|
||||||
|
|||||||
1092
shared/bsms.py
Normal file
1092
shared/bsms.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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_CLASSIC, AF_P2WPKH, AF_P2TR
|
||||||
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
|
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 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 ucollections import namedtuple
|
||||||
from opcodes import OP_RETURN, OP_1, OP_16
|
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>
|
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
|
||||||
# - also electrum source: electrum/lib/constants.py
|
# - 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:
|
class ChainsBase:
|
||||||
|
|
||||||
curve = 'secp256k1'
|
curve = 'secp256k1'
|
||||||
@ -110,23 +132,30 @@ class ChainsBase:
|
|||||||
# - works only with single-key addresses
|
# - works only with single-key addresses
|
||||||
assert not addr_fmt & AFC_SCRIPT
|
assert not addr_fmt & AFC_SCRIPT
|
||||||
|
|
||||||
keyhash = ngu.hash.hash160(pubkey)
|
if addr_fmt == AF_P2TR:
|
||||||
if addr_fmt == AF_CLASSIC:
|
assert len(pubkey) == 32 # internal
|
||||||
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
script = b'\x51\x20' + taptweak(pubkey)
|
||||||
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:
|
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)
|
return cls.render_address(script)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def address(cls, node, addr_fmt):
|
def address(cls, node, addr_fmt):
|
||||||
# return a human-readable, properly formatted address
|
# 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:
|
if addr_fmt == AF_CLASSIC:
|
||||||
# olde fashioned P2PKH
|
# olde fashioned P2PKH
|
||||||
@ -299,6 +328,7 @@ class BitcoinMain(ChainsBase):
|
|||||||
AF_P2WPKH: Slip132Version(0x04b24746, 0x04b2430c, 'z'),
|
AF_P2WPKH: Slip132Version(0x04b24746, 0x04b2430c, 'z'),
|
||||||
AF_P2WSH_P2SH: Slip132Version(0x0295b43f, 0x0295b005, 'Y'),
|
AF_P2WSH_P2SH: Slip132Version(0x0295b43f, 0x0295b005, 'Y'),
|
||||||
AF_P2WSH: Slip132Version(0x02aa7ed3, 0x02aa7a99, 'Z'),
|
AF_P2WSH: Slip132Version(0x02aa7ed3, 0x02aa7a99, 'Z'),
|
||||||
|
AF_P2TR: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||||
}
|
}
|
||||||
|
|
||||||
bech32_hrp = 'bc'
|
bech32_hrp = 'bc'
|
||||||
@ -320,6 +350,7 @@ class BitcoinTestnet(BitcoinMain):
|
|||||||
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
||||||
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
||||||
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
||||||
|
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
|
||||||
}
|
}
|
||||||
|
|
||||||
bech32_hrp = 'tb'
|
bech32_hrp = 'tb'
|
||||||
@ -342,6 +373,7 @@ class BitcoinRegtest(BitcoinMain):
|
|||||||
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
||||||
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
||||||
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
||||||
|
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
|
||||||
}
|
}
|
||||||
|
|
||||||
bech32_hrp = 'bcrt'
|
bech32_hrp = 'bcrt'
|
||||||
@ -376,6 +408,13 @@ def current_chain():
|
|||||||
|
|
||||||
return get_chain(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.
|
# Overbuilt: will only be testnet and mainchain.
|
||||||
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
|
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
|
||||||
|
|
||||||
@ -403,6 +442,8 @@ CommonDerivations = [
|
|||||||
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
|
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
|
||||||
( 'BIP-84 (Native Segwit P2WPKH)', "m/84h/{coin_type}h/{account}h/{change}/{idx}",
|
( 'BIP-84 (Native Segwit P2WPKH)', "m/84h/{coin_type}h/{account}h/{change}/{idx}",
|
||||||
AF_P2WPKH ), # generates bc1 bech32 addresses
|
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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -194,11 +194,6 @@ def decode_short_text(got):
|
|||||||
# was something else.
|
# was something else.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# multisig descriptor
|
|
||||||
# multi( catches both multi( and sortedmulti(
|
|
||||||
if ("multi(" in got):
|
|
||||||
return 'multi', (got,)
|
|
||||||
|
|
||||||
if ("\n" in got) and ('pub' in got):
|
if ("\n" in got) and ('pub' in got):
|
||||||
# legacy multisig import/export format
|
# legacy multisig import/export format
|
||||||
# [0-9a-fA-F]{8}\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]{107}
|
# [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:
|
if c > 1:
|
||||||
return 'multi', (got,)
|
return 'multi', (got,)
|
||||||
|
|
||||||
|
from descriptor import Descriptor
|
||||||
|
if Descriptor.is_descriptor(got):
|
||||||
|
return 'minisc', (got,)
|
||||||
|
|
||||||
# Things with newlines in them are not URL's
|
# Things with newlines in them are not URL's
|
||||||
# - working URLs are not >4k
|
# - working URLs are not >4k
|
||||||
# - might be a story in text, etc.
|
# - might be a story in text, etc.
|
||||||
|
|||||||
558
shared/desc_utils.py
Normal file
558
shared/desc_utils.py
Normal 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
|
||||||
@ -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
|
import ngu, chains
|
||||||
#
|
from io import BytesIO
|
||||||
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
|
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 = {
|
class DescriptorException(ValueError):
|
||||||
AF_P2WPKH: "wpkh(%s)",
|
pass
|
||||||
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 WrongCheckSumError(Exception):
|
class WrongCheckSumError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def polymod(c, val):
|
class Tapscript:
|
||||||
c0 = c >> 35
|
def __init__(self, tree=None, keys=None, policy=None):
|
||||||
c = ((c & 0x7ffffffff) << 5) ^ val
|
self.tree = tree
|
||||||
if (c0 & 1):
|
self.keys = keys
|
||||||
c ^= 0xf5dee51989
|
self.policy = policy
|
||||||
if (c0 & 2):
|
self._merkle_root = None
|
||||||
c ^= 0xa9fdca3312
|
|
||||||
if (c0 & 4):
|
|
||||||
c ^= 0x1bab10e32d
|
|
||||||
if (c0 & 8):
|
|
||||||
c ^= 0x3706b1677a
|
|
||||||
if (c0 & 16):
|
|
||||||
c ^= 0x644d626ffd
|
|
||||||
|
|
||||||
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):
|
@property
|
||||||
c = 1
|
def merkle_root(self):
|
||||||
cls = 0
|
if not self._merkle_root:
|
||||||
clscount = 0
|
self.process_tree()
|
||||||
for ch in desc:
|
return self._merkle_root
|
||||||
pos = INPUT_CHARSET.find(ch)
|
|
||||||
if pos == -1:
|
|
||||||
raise ValueError(ch)
|
|
||||||
|
|
||||||
c = polymod(c, pos & 31)
|
@staticmethod
|
||||||
cls = cls * 3 + (pos >> 5)
|
def _derive(tree, idx, key_map, change=False):
|
||||||
clscount += 1
|
if isinstance(tree, Miniscript):
|
||||||
if clscount == 3:
|
return tree.derive(idx, key_map, change=change)
|
||||||
c = polymod(c, cls)
|
else:
|
||||||
cls = 0
|
if len(tree) == 1 and isinstance(tree[0], Miniscript):
|
||||||
clscount = 0
|
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:
|
def derive(self, idx=None, change=False):
|
||||||
c = polymod(c, cls)
|
derived_keys = OrderedDict()
|
||||||
for j in range(0, 8):
|
for k in self.keys:
|
||||||
c = polymod(c, 0)
|
derived_keys[k] = k.derive(idx, change=change)
|
||||||
c ^= 1
|
tree = Tapscript._derive(self.tree, idx, derived_keys, change=change)
|
||||||
|
return type(self)(tree, policy=self.policy, keys=list(derived_keys.values()))
|
||||||
|
|
||||||
rv = ''
|
def process_tree(self):
|
||||||
for j in range(0, 8):
|
info, mr = taproot_tree_helper(self.tree)
|
||||||
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
|
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):
|
depth += 1
|
||||||
return desc + "#" + descriptor_checksum(desc)
|
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):
|
s.seek(-1, 1)
|
||||||
"""Remove comments, empty lines and strip line. Produce single line string"""
|
item = Miniscript.read_from(s, taproot=True)
|
||||||
res = ""
|
item.is_sane(taproot=True)
|
||||||
for l in string.split("\n"):
|
item.verify()
|
||||||
strip_l = l.strip()
|
num_leafs += 1
|
||||||
if not strip_l:
|
if itmp is None:
|
||||||
continue
|
tapscript.append(item)
|
||||||
if strip_l.startswith("#"):
|
else:
|
||||||
continue
|
if itmp_p and depth == 4:
|
||||||
res += strip_l
|
itmp[itmp_p][itmp_p].append(item)
|
||||||
return res
|
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):
|
def parse_policy(self):
|
||||||
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
|
self.policy, self.keys = self._parse_policy(self.tree, [])
|
||||||
if addr_fmt == AF_P2WSH_P2SH:
|
orig_keys = OrderedDict()
|
||||||
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
|
for k in self.keys:
|
||||||
elif addr_fmt == AF_P2WSH:
|
if k.origin not in orig_keys:
|
||||||
descriptor_template = "wsh(sortedmulti(M,%s,...))"
|
orig_keys[k.origin] = []
|
||||||
elif addr_fmt == AF_P2SH:
|
orig_keys[k.origin].append(k)
|
||||||
descriptor_template = "sh(sortedmulti(M,%s,...))"
|
for i, k_lst in enumerate(orig_keys.values()):
|
||||||
else:
|
# always keep subderivation in policy string
|
||||||
return None
|
self.policy = self.policy.replace(k_lst[0].to_string(subderiv=False), chr(64) + str(i))
|
||||||
descriptor_template = descriptor_template % key_exp
|
|
||||||
return descriptor_template
|
@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:
|
class Descriptor:
|
||||||
__slots__ = (
|
def __init__(self, miniscript=None, sh=False, wsh=True, key=None, wpkh=True,
|
||||||
"keys",
|
taproot=False, tapscript=None):
|
||||||
"addr_fmt",
|
if key is None and miniscript is None:
|
||||||
)
|
raise DescriptorException("Provide either miniscript or a key")
|
||||||
|
|
||||||
def __init__(self, keys, addr_fmt):
|
self.sh = sh
|
||||||
self.keys = keys
|
self.wsh = wsh
|
||||||
self.addr_fmt = addr_fmt
|
self.key = key
|
||||||
|
self.miniscript = miniscript
|
||||||
|
self.wpkh = wpkh
|
||||||
|
self.taproot = taproot
|
||||||
|
self.tapscript = tapscript
|
||||||
|
|
||||||
@staticmethod
|
if taproot:
|
||||||
def checksum_check(desc_w_checksum , csum_required=False):
|
if self.key:
|
||||||
try:
|
self.key.taproot = True
|
||||||
desc, checksum = desc_w_checksum.split("#")
|
for k in self.keys:
|
||||||
except ValueError:
|
k.taproot = taproot
|
||||||
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 validate(self):
|
||||||
def parse_key_orig_info(key):
|
from glob import settings
|
||||||
# key origin info is required for our MultisigWallet
|
if self.miniscript:
|
||||||
close_index = key.find("]")
|
if self.is_basic_multisig:
|
||||||
if key[0] != "[" or close_index == -1:
|
assert len(self.keys) <= MAX_SIGNERS
|
||||||
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>" + "/" + "*"
|
|
||||||
else:
|
else:
|
||||||
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
|
assert len(self.keys) <= 20
|
||||||
result.append(key_str.replace("'", "h"))
|
self.miniscript.verify()
|
||||||
return result
|
if self.miniscript.type != "B":
|
||||||
|
raise DescriptorException("Top level miniscript should be 'B'")
|
||||||
|
|
||||||
def _serialize(self, internal=False, int_ext=False):
|
has_mine = 0
|
||||||
"""Serialize without checksum"""
|
my_xfp = settings.get('xfp')
|
||||||
assert len(self.keys) == 1 # "Multiple keys for single signature script"
|
to_check = self.keys.copy()
|
||||||
desc_base = SINGLE_FMT_TO_SCRIPT[self.addr_fmt]
|
if self.tapscript:
|
||||||
inner = self.serialize_keys(internal=internal, int_ext=int_ext)[0]
|
assert len(self.keys) <= MAX_TR_SIGNERS
|
||||||
return desc_base % (inner)
|
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):
|
c = chains.current_key_chain().ctype
|
||||||
"""Serialize with checksum"""
|
for k in to_check:
|
||||||
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
|
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
|
assert has_mine != 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
|
||||||
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
|
def storage_policy(self):
|
||||||
elif desc.startswith("wpkh("):
|
if self.tapscript:
|
||||||
addr_fmt = AF_P2WPKH
|
return self.tapscript.policy
|
||||||
tmp_desc = desc.replace("wpkh(", "")
|
|
||||||
tmp_desc = tmp_desc.rstrip(")")
|
|
||||||
|
|
||||||
# wrapped segwit
|
s = self.miniscript.to_string()
|
||||||
elif desc.startswith("sh(wpkh("):
|
orig_keys = OrderedDict()
|
||||||
addr_fmt = AF_P2WPKH_P2SH
|
for k in self.keys:
|
||||||
tmp_desc = desc.replace("sh(wpkh(", "")
|
if k.origin not in orig_keys:
|
||||||
tmp_desc = tmp_desc.rstrip("))")
|
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:
|
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)
|
def script_pubkey(self):
|
||||||
if key[0:4] not in ["tpub", "xpub"]:
|
if self.taproot:
|
||||||
raise ValueError("Only extended public keys are supported")
|
tweak = None
|
||||||
|
if self.tapscript:
|
||||||
xpub = cls.parse_key_derivation_info(key)
|
tweak = self.tapscript.merkle_root
|
||||||
xfp = str2xfp(koi[:8])
|
output_pubkey = chains.taptweak(self.key.serialize(), tweak)
|
||||||
origin_deriv = "m" + koi[8:]
|
return b"\x51\x20" + output_pubkey
|
||||||
|
if self.sh:
|
||||||
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
|
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
|
@classmethod
|
||||||
def is_descriptor(cls, desc_str):
|
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:
|
try:
|
||||||
temp = parse_desc_str(desc_str)
|
temp = parse_desc_str(desc_str)
|
||||||
except:
|
except:
|
||||||
@ -267,142 +472,142 @@ class Descriptor:
|
|||||||
return True
|
return True
|
||||||
return False
|
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
|
# this will become legacy one day
|
||||||
# instead use <0;1> descriptor format
|
# instead use <0;1> descriptor format
|
||||||
res = []
|
res = []
|
||||||
for internal in [False, True]:
|
for external in (True, False):
|
||||||
desc_obj = {
|
desc_obj = {
|
||||||
"desc": self.serialize(internal=internal),
|
"desc": self.to_string(external, not external),
|
||||||
"active": True,
|
"active": True,
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"internal": internal,
|
"internal": not external,
|
||||||
"range": [0, 100],
|
"range": [0, 100],
|
||||||
}
|
}
|
||||||
if internal is False and external_label:
|
|
||||||
desc_obj["label"] = external_label
|
|
||||||
res.append(desc_obj)
|
res.append(desc_obj)
|
||||||
|
|
||||||
return res
|
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
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
import machine, uzlib, ckcc, utime
|
import machine, uzlib, ckcc, utime
|
||||||
from ssd1306 import SSD1306_SPI
|
from ssd1306 import SSD1306_SPI
|
||||||
from version import is_devmode
|
from version import is_devmode, is_edge
|
||||||
import framebuf
|
import framebuf
|
||||||
from graphics_mk4 import Graphics
|
from graphics_mk4 import Graphics
|
||||||
|
|
||||||
@ -146,6 +146,12 @@ class Display:
|
|||||||
self.text(-2, 21, 'D', font=FontTiny, invert=1)
|
self.text(-2, 21, 'D', font=FontTiny, invert=1)
|
||||||
self.text(-2, 28, 'E', font=FontTiny, invert=1)
|
self.text(-2, 28, 'E', font=FontTiny, invert=1)
|
||||||
self.text(-2, 35, 'V', 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):
|
def fullscreen(self, msg, percent=None, line2=None):
|
||||||
# show a simple message "fullscreen".
|
# show a simple message "fullscreen".
|
||||||
|
|||||||
@ -56,32 +56,32 @@ still backed-up.''')
|
|||||||
|
|
||||||
def bip85_derive(picked, index):
|
def bip85_derive(picked, index):
|
||||||
# implement the core step of BIP85 from our master secret
|
# implement the core step of BIP85 from our master secret
|
||||||
|
path = "m/83696968h/"
|
||||||
if picked in (0,1,2):
|
if picked in (0,1,2):
|
||||||
# BIP-39 seed phrases (we only support English)
|
# BIP-39 seed phrases (we only support English)
|
||||||
num_words = stash.SEED_LEN_OPTS[picked]
|
num_words = stash.SEED_LEN_OPTS[picked]
|
||||||
width = (16, 24, 32)[picked] # of bytes
|
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'
|
s_mode = 'words'
|
||||||
elif picked == 3:
|
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'
|
s_mode = 'wif'
|
||||||
path = "m/83696968h/2h/{index}h".format(index=index)
|
path += "2h/%dh" % index
|
||||||
width = 32
|
width = 32
|
||||||
elif picked == 4:
|
elif picked == 4:
|
||||||
# New XPRV
|
# New XPRV
|
||||||
path = "m/83696968h/32h/{index}h".format(index=index)
|
path += "32h/%dh" % index
|
||||||
s_mode = 'xprv'
|
s_mode = 'xprv'
|
||||||
width = 64
|
width = 64
|
||||||
elif picked in (5, 6):
|
elif picked in (5, 6):
|
||||||
width = 32 if picked == 5 else 64
|
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'
|
s_mode = 'hex'
|
||||||
elif picked == 7:
|
elif picked == 7:
|
||||||
width = 64
|
width = 64
|
||||||
# hardcoded width for now
|
# hardcoded width for now
|
||||||
# b"pwd".hex() --> 707764
|
# 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'
|
s_mode = 'pw'
|
||||||
else:
|
else:
|
||||||
raise ValueError(picked)
|
raise ValueError(picked)
|
||||||
|
|||||||
129
shared/export.py
129
shared/export.py
@ -9,7 +9,7 @@ from utils import xfp2str, swab32, chunk_writer
|
|||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from auth import write_sig_file
|
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 charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
|
||||||
from ownership import OWNERSHIP
|
from ownership import OWNERSHIP
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ be needed for different systems.
|
|||||||
|
|
||||||
node = sv.derive_path(hard_sub, register=False)
|
node = sv.derive_path(hard_sub, register=False)
|
||||||
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
|
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" % (
|
yield ("%s => %s ##SLIP-132##\n" % (
|
||||||
hard_sub, chain.serialize_public(node, addr_fmt)))
|
hard_sub, chain.serialize_public(node, addr_fmt)))
|
||||||
|
|
||||||
@ -121,7 +121,8 @@ be needed for different systems.
|
|||||||
yield ('\n\n')
|
yield ('\n\n')
|
||||||
|
|
||||||
from multisig import MultisigWallet
|
from multisig import MultisigWallet
|
||||||
if MultisigWallet.exists():
|
exists, exists_other_chain = MultisigWallet.exists()
|
||||||
|
if exists:
|
||||||
yield '\n# Your Multisig Wallets\n\n'
|
yield '\n# Your Multisig Wallets\n\n'
|
||||||
|
|
||||||
for ms in MultisigWallet.get_all():
|
for ms in MultisigWallet.get_all():
|
||||||
@ -133,14 +134,15 @@ be needed for different systems.
|
|||||||
yield fp.getvalue()
|
yield fp.getvalue()
|
||||||
del fp
|
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.
|
# Export data as a text file.
|
||||||
from glob import dis, NFC
|
from glob import dis, NFC
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
from ux import import_export_prompt
|
from ux import import_export_prompt
|
||||||
|
|
||||||
choice = await import_export_prompt("%s file" % title, is_import=False,
|
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:
|
if choice == KEY_CANCEL:
|
||||||
return
|
return
|
||||||
elif choice == KEY_QR:
|
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:
|
with open(fname, 'wb') as fd:
|
||||||
chunk_writer(fd, body)
|
chunk_writer(fd, body)
|
||||||
|
|
||||||
h = ngu.hash.sha256s(body.encode())
|
sig_nice = None
|
||||||
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
if addr_fmt != AF_P2TR:
|
||||||
|
h = ngu.hash.sha256s(body.encode())
|
||||||
|
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
||||||
|
|
||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
await needs_microsd()
|
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))
|
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = '%s file written:\n\n%s\n\n%s signature file written:\n\n%s' % (title, nice, title,
|
msg = '%s file written:\n\n%s' % (title, nice)
|
||||||
sig_nice)
|
if sig_nice:
|
||||||
|
msg += '\n\n%s signature file written:\n\n%s' % (title, sig_nice)
|
||||||
await ux_show_story(msg)
|
await ux_show_story(msg)
|
||||||
|
|
||||||
async def make_summary_file(fname_pattern='public.txt'):
|
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
|
# make the data
|
||||||
examples = []
|
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_multi = ujson.dumps(imp_multi)
|
||||||
imp_desc = ujson.dumps(imp_desc)
|
imp_desc = ujson.dumps(imp_desc)
|
||||||
|
imp_desc_tr = ujson.dumps(imp_desc_tr)
|
||||||
|
|
||||||
body = '''\
|
body = '''\
|
||||||
# Bitcoin Core Wallet Import File
|
# Bitcoin Core Wallet Import File
|
||||||
@ -214,7 +220,10 @@ Wallet operates on blockchain: {nb}
|
|||||||
The following command can be entered after opening Window -> Console
|
The following command can be entered after opening Window -> Console
|
||||||
in Bitcoin Core, or using bitcoin-cli:
|
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.
|
> **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.
|
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)
|
## 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'.join('%s => %s' % t for t in examples)
|
||||||
|
|
||||||
body += '\n'
|
body += '\n'
|
||||||
|
|
||||||
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
|
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
|
||||||
|
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
|
||||||
|
|
||||||
ch = chains.current_chain()
|
ch = chains.current_chain()
|
||||||
derive = "84h/{coin_type}h/{account}h".format(account=account_num, coin_type=ch.b44_cointype)
|
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):
|
def generate_bitcoin_core_wallet(account_num, example_addrs):
|
||||||
# Generate the data for an RPC command to import keys into Bitcoin Core
|
# Generate the data for an RPC command to import keys into Bitcoin Core
|
||||||
# - yields dicts for json purposes
|
# - yields dicts for json purposes
|
||||||
from descriptor import Descriptor
|
from descriptor import Descriptor, Key
|
||||||
|
|
||||||
chain = chains.current_chain()
|
chain = chains.current_chain()
|
||||||
|
|
||||||
derive = "84h/{coin_type}h/{account}h".format(account=account_num,
|
derive_v0 = "84h/{coin_type}h/{account}h".format(
|
||||||
coin_type=chain.b44_cointype)
|
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:
|
with stash.SensitiveValues() as sv:
|
||||||
prefix = sv.derive_path(derive)
|
prefix = sv.derive_path(derive_v0)
|
||||||
xpub = chain.serialize_public(prefix)
|
xpub_v0 = chain.serialize_public(prefix)
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
sp = '0/%d' % i
|
sp = '0/%d' % i
|
||||||
node = sv.derive_path(sp, master=prefix)
|
node = sv.derive_path(sp, master=prefix)
|
||||||
a = chain.address(node, AF_P2WPKH)
|
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')
|
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_P2WPKH, account_num)
|
||||||
|
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
|
||||||
|
|
||||||
desc_obj = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=AF_P2WPKH)
|
|
||||||
# for importmulti
|
# for importmulti
|
||||||
imm_list = [
|
imm_list = [
|
||||||
{
|
{
|
||||||
'desc': desc_obj.serialize(internal=internal),
|
'desc': desc_v0.to_string(external, internal),
|
||||||
'range': [0, 1000],
|
'range': [0, 1000],
|
||||||
'timestamp': 'now',
|
'timestamp': 'now',
|
||||||
'internal': internal,
|
'internal': internal,
|
||||||
'keypool': True,
|
'keypool': True,
|
||||||
'watchonly': True
|
'watchonly': True
|
||||||
}
|
}
|
||||||
for internal in [False, True]
|
for external, internal in [(True, False), (False, True)]
|
||||||
]
|
]
|
||||||
# for importdescriptors
|
# for importdescriptors
|
||||||
imd_list = desc_obj.bitcoin_core_serialize()
|
imd_list = desc_v0.bitcoin_core_serialize()
|
||||||
return imm_list, imd_list
|
imd_list_v1 = desc_v1.bitcoin_core_serialize()
|
||||||
|
return imm_list, imd_list, imd_list_v1
|
||||||
|
|
||||||
|
|
||||||
def generate_wasabi_wallet():
|
def generate_wasabi_wallet():
|
||||||
# Generate the data for a JSON file which Wasabi can open directly as a new 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):
|
def generate_generic_export(account_num=0):
|
||||||
# Generate data that other programers will use to import Coldcard (single-signer)
|
# 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()
|
chain = chains.current_chain()
|
||||||
master_xfp = settings.get("xfp")
|
master_xfp = settings.get("xfp")
|
||||||
@ -361,12 +394,14 @@ def generate_generic_export(account_num=0):
|
|||||||
with stash.SensitiveValues() as sv:
|
with stash.SensitiveValues() as sv:
|
||||||
# each of these paths would have /{change}/{idx} in usage (not hardened)
|
# each of these paths would have /{change}/{idx} in usage (not hardened)
|
||||||
for name, deriv, fmt, atype, is_ms in [
|
for name, deriv, fmt, atype, is_ms in [
|
||||||
( 'bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False ),
|
('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"
|
('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 ),
|
('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 ),
|
('bip86', "m/86h/{ct}h/{acc}h", AF_P2TR, 'p2tr', False),
|
||||||
( 'bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True ),
|
('bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True),
|
||||||
( 'bip45', "m/45h", AF_P2SH, 'p2sh', 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:
|
if fmt == AF_P2SH and account_num:
|
||||||
continue
|
continue
|
||||||
@ -375,11 +410,14 @@ def generate_generic_export(account_num=0):
|
|||||||
node = sv.derive_path(dd)
|
node = sv.derive_path(dd)
|
||||||
xfp = xfp2str(swab32(node.my_fp()))
|
xfp = xfp2str(swab32(node.my_fp()))
|
||||||
xp = chain.serialize_public(node, AF_CLASSIC)
|
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:
|
if is_ms:
|
||||||
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
|
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
|
||||||
else:
|
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)
|
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,
|
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
|
||||||
fname_pattern="descriptor.txt"):
|
fname_pattern="descriptor.txt"):
|
||||||
from descriptor import Descriptor
|
from descriptor import Descriptor, Key
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
dis.fullscreen('Generating...')
|
dis.fullscreen('Generating...')
|
||||||
@ -520,34 +558,41 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
|||||||
mode = 84
|
mode = 84
|
||||||
elif addr_type == AF_P2WPKH_P2SH:
|
elif addr_type == AF_P2WPKH_P2SH:
|
||||||
mode = 49
|
mode = 49
|
||||||
|
elif addr_type == AF_P2TR:
|
||||||
|
mode = 86
|
||||||
else:
|
else:
|
||||||
raise ValueError(addr_type)
|
raise ValueError(addr_type)
|
||||||
|
|
||||||
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
||||||
|
|
||||||
derive = "m/{mode}h/{coin_type}h/{account}h".format(mode=mode,
|
derive = "m/{mode}h/{coin_type}h/{account}h".format(
|
||||||
account=account_num, coin_type=chain.b44_cointype)
|
mode=mode, account=account_num, coin_type=chain.b44_cointype
|
||||||
|
)
|
||||||
dis.progress_bar_show(0.2)
|
dis.progress_bar_show(0.2)
|
||||||
with stash.SensitiveValues() as sv:
|
with stash.SensitiveValues() as sv:
|
||||||
dis.progress_bar_show(0.3)
|
dis.progress_bar_show(0.3)
|
||||||
xpub = chain.serialize_public(sv.derive_path(derive))
|
xpub = chain.serialize_public(sv.derive_path(derive))
|
||||||
|
|
||||||
dis.progress_bar_show(0.7)
|
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)
|
dis.progress_bar_show(0.8)
|
||||||
if int_ext:
|
if int_ext:
|
||||||
# with <0;1> notation
|
# with <0;1> notation
|
||||||
body = desc.serialize(int_ext=True)
|
body = desc.to_string()
|
||||||
else:
|
else:
|
||||||
# external descriptor
|
# external descriptor
|
||||||
# internal descriptor
|
# internal descriptor
|
||||||
body = "%s\n%s" % (
|
body = "%s\n%s" % (
|
||||||
desc.serialize(internal=False),
|
desc.to_string(internal=False),
|
||||||
desc.serialize(internal=True),
|
desc.to_string(external=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
dis.progress_bar_show(1)
|
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
|
# EOF
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from actions import *
|
|||||||
from choosers import *
|
from choosers import *
|
||||||
from mk4 import dev_enable_repl
|
from mk4 import dev_enable_repl
|
||||||
from multisig import make_multisig_menu, import_multisig_nfc
|
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 seed import make_ephemeral_seed_menu, make_seed_vault_menu, start_b39_pw
|
||||||
from address_explorer import address_explore
|
from address_explorer import address_explore
|
||||||
from drv_entro import drv_entro_start, password_entry
|
from drv_entro import drv_entro_start, password_entry
|
||||||
@ -138,6 +139,8 @@ SettingsMenu = [
|
|||||||
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
|
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
|
||||||
NonDefaultMenuItem('Multisig Wallets', 'multisig',
|
NonDefaultMenuItem('Multisig Wallets', 'multisig',
|
||||||
menu=make_multisig_menu, predicate=has_secrets),
|
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),
|
NonDefaultMenuItem('NFC Push Tx', 'ptxurl', menu=pushtx_setup_menu),
|
||||||
MenuItem('Display Units', chooser=value_resolution_chooser),
|
MenuItem('Display Units', chooser=value_resolution_chooser),
|
||||||
MenuItem('Max Network Fee', chooser=max_fee_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.'''),
|
data or filenames.'''),
|
||||||
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
|
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
|
||||||
story='''When enabled, allows scrolling past menu top/bottom \
|
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'],
|
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
||||||
story=('Forces display of XFP (seed fingerprint) '
|
story=('Forces display of XFP (seed fingerprint) '
|
||||||
'at top of main menu. Normally, XFP is shown only when '
|
'at top of main menu. Normally, XFP is shown only when '
|
||||||
@ -176,6 +179,7 @@ XpubExportMenu = [
|
|||||||
# xxxxxxxxxxxxxxxx
|
# xxxxxxxxxxxxxxxx
|
||||||
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
|
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
|
||||||
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
|
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("P2WPKH/P2SH (49)", f=export_xpub, arg=49),
|
||||||
MenuItem("Master XPUB", f=export_xpub, arg=0),
|
MenuItem("Master XPUB", f=export_xpub, arg=0),
|
||||||
MenuItem("Current XFP", f=export_xpub, arg=-1),
|
MenuItem("Current XFP", f=export_xpub, arg=-1),
|
||||||
@ -291,7 +295,7 @@ DangerZoneMenu = [
|
|||||||
"WARNING: Seed Vault is encrypted (AES-256-CTR) by your seed,"
|
"WARNING: Seed Vault is encrypted (AES-256-CTR) by your seed,"
|
||||||
" but not held directly inside secure elements. Backups are required"
|
" but not held directly inside secure elements. Backups are required"
|
||||||
" after any change to vault! Recommended for experiments or temporary use."),
|
" 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('Perform Selftest', f=start_selftest), # little harmful
|
||||||
MenuItem("Set High-Water", f=set_highwater),
|
MenuItem("Set High-Water", f=set_highwater),
|
||||||
MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available),
|
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('Export Wallet', predicate=has_secrets, menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
||||||
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
|
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
|
||||||
MenuItem("File Management", menu=FileMgmtMenu),
|
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),
|
predicate=version.has_qwerty),
|
||||||
MenuItem('Derive Seed B85' if not version.has_qwerty else 'Derive Seeds (BIP-85)',
|
MenuItem('Derive Seed B85' if not version.has_qwerty else 'Derive Seeds (BIP-85)',
|
||||||
f=drv_entro_start),
|
f=drv_entro_start),
|
||||||
@ -424,7 +428,7 @@ NormalSystem = [
|
|||||||
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
|
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
|
||||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
|
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',
|
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||||
predicate=lambda: settings.get("emu", False) and has_secrets()),
|
predicate=lambda: settings.get("emu", False) and has_secrets()),
|
||||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
||||||
|
|||||||
@ -4,16 +4,15 @@
|
|||||||
#
|
#
|
||||||
# Unattended signing of transactions and messages, subject to a set of rules.
|
# 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
|
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
||||||
from sffile import SFFile
|
|
||||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
||||||
from pincodes import AE_LONG_SECRET_LEN
|
from pincodes import AE_LONG_SECRET_LEN
|
||||||
from stash import blank_object
|
from stash import blank_object
|
||||||
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
|
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
|
||||||
from public_constants import MAX_USERNAME_LEN
|
from public_constants import MAX_USERNAME_LEN
|
||||||
from multisig import MultisigWallet
|
from multisig import MultisigWallet
|
||||||
|
from miniscript import MiniScriptWallet
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import unhexlify as a2b_hex
|
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from ucollections import OrderedDict
|
from ucollections import OrderedDict
|
||||||
from files import CardSlot, CardMissingError
|
from files import CardSlot, CardMissingError
|
||||||
@ -88,13 +87,13 @@ def pop_list(j, fld_name, cleanup_fcn=None):
|
|||||||
else:
|
else:
|
||||||
return []
|
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
|
# expect a list of derivation paths, but also 'any' meaning accept all
|
||||||
# - maybe also 'p2sh' as special value
|
# - maybe also 'p2sh' as special value
|
||||||
# - also, path can have n
|
# - also, path can have n
|
||||||
def cu(s):
|
def cu(s):
|
||||||
if s.lower() == 'any': return s.lower()
|
if extra_vals and s.lower() in extra_vals:
|
||||||
if extra_val and s.lower() == extra_val: return s.lower()
|
return s.lower()
|
||||||
try:
|
try:
|
||||||
return cleanup_deriv_path(s, allow_star=True)
|
return cleanup_deriv_path(s, allow_star=True)
|
||||||
except:
|
except:
|
||||||
@ -195,7 +194,7 @@ class ApprovalRule:
|
|||||||
# - users: list of authorized users
|
# - users: list of authorized users
|
||||||
# - min_users: how many of those are needed to approve
|
# - min_users: how many of those are needed to approve
|
||||||
# - local_conf: local user must also confirm w/ code
|
# - 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
|
# - 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:
|
# - patterns: list of transaction patterns to check for. Valid values:
|
||||||
# * EQ_NUM_INS_OUTS: the number of inputs and outputs must be equal
|
# * EQ_NUM_INS_OUTS: the number of inputs and outputs must be equal
|
||||||
@ -212,6 +211,7 @@ class ApprovalRule:
|
|||||||
return u
|
return u
|
||||||
|
|
||||||
self.index = idx+1
|
self.index = idx+1
|
||||||
|
self.ms_type = "multisig"
|
||||||
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
|
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
|
||||||
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
|
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
|
||||||
self.users = pop_list(j, 'users', check_user)
|
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 specified, 'wallet' must be an existing multisig wallet's name
|
||||||
if self.wallet and self.wallet != '1':
|
if self.wallet and self.wallet != '1':
|
||||||
names = [ms.name for ms in MultisigWallet.get_all()]
|
ms_names = [ms.name for ms in MultisigWallet.get_all()]
|
||||||
assert self.wallet in names, "unknown MS wallet: "+self.wallet
|
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
|
# patterns must be valid
|
||||||
for p in self.patterns:
|
for p in self.patterns:
|
||||||
@ -283,9 +286,9 @@ class ApprovalRule:
|
|||||||
rv = 'Any amount'
|
rv = 'Any amount'
|
||||||
|
|
||||||
if self.wallet == '1':
|
if self.wallet == '1':
|
||||||
rv += ' (non multisig)'
|
rv += ' (singlesig only)'
|
||||||
elif self.wallet:
|
elif self.wallet:
|
||||||
rv += ' from multisig wallet "%s"' % self.wallet
|
rv += ' from %s wallet "%s"' % (self.ms_type, self.wallet)
|
||||||
|
|
||||||
if self.users:
|
if self.users:
|
||||||
rv += ' may be authorized by '
|
rv += ' may be authorized by '
|
||||||
@ -328,10 +331,12 @@ class ApprovalRule:
|
|||||||
# rule limited to one wallet
|
# rule limited to one wallet
|
||||||
if psbt.active_multisig:
|
if psbt.active_multisig:
|
||||||
# if multisig signing, might need to match specific wallet name
|
# 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:
|
else:
|
||||||
# non multisig, but does this rule apply to all wallets or single-singers
|
# 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:
|
if self.max_amount is not None:
|
||||||
assert total_out <= self.max_amount, 'amount exceeded'
|
assert total_out <= self.max_amount, 'amount exceeded'
|
||||||
@ -504,9 +509,9 @@ class HSMPolicy:
|
|||||||
self.warnings_ok = pop_bool(j, 'warnings_ok')
|
self.warnings_ok = pop_bool(j, 'warnings_ok')
|
||||||
|
|
||||||
# a list of paths we can accept for signing
|
# a list of paths we can accept for signing
|
||||||
self.msg_paths = pop_deriv_list(j, 'msg_paths')
|
self.msg_paths = pop_deriv_list(j, 'msg_paths', ['any'])
|
||||||
self.share_xpubs = pop_deriv_list(j, 'share_xpubs')
|
self.share_xpubs = pop_deriv_list(j, 'share_xpubs', ['any'])
|
||||||
self.share_addrs = pop_deriv_list(j, 'share_addrs', 'p2sh')
|
self.share_addrs = pop_deriv_list(j, 'share_addrs', ['p2sh', 'any', 'msas'])
|
||||||
|
|
||||||
# free text shown at top
|
# free text shown at top
|
||||||
self.notes = pop_string(j, 'notes', 1, 80)
|
self.notes = pop_string(j, 'notes', 1, 80)
|
||||||
@ -814,12 +819,15 @@ class HSMPolicy:
|
|||||||
|
|
||||||
return match_deriv_path(self.share_xpubs, subpath)
|
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?
|
# Are we allowing "show address" requests over USB?
|
||||||
|
|
||||||
if not self.share_addrs:
|
if not self.share_addrs:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if miniscript:
|
||||||
|
return ('msas' in self.share_addrs)
|
||||||
|
|
||||||
if is_p2sh:
|
if is_p2sh:
|
||||||
return ('p2sh' in self.share_addrs)
|
return ('p2sh' in self.share_addrs)
|
||||||
|
|
||||||
@ -894,6 +902,7 @@ class HSMPolicy:
|
|||||||
|
|
||||||
# reject anything with warning, probably
|
# reject anything with warning, probably
|
||||||
if psbt.warnings:
|
if psbt.warnings:
|
||||||
|
print(psbt.warnings)
|
||||||
if self.warnings_ok:
|
if self.warnings_ok:
|
||||||
log.info("Txn has warnings, but policy is to accept anyway.")
|
log.info("Txn has warnings, but policy is to accept anyway.")
|
||||||
else:
|
else:
|
||||||
@ -994,7 +1003,8 @@ def hsm_status_report():
|
|||||||
rv['approval_wait'] = True
|
rv['approval_wait'] = True
|
||||||
|
|
||||||
rv['users'] = Users.list()
|
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')
|
rv['chain'] = settings.get('chain', 'BTC')
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,14 @@ freeze_as_mpy('', [
|
|||||||
'address_explorer.py',
|
'address_explorer.py',
|
||||||
'auth.py',
|
'auth.py',
|
||||||
'backups.py',
|
'backups.py',
|
||||||
|
'bsms.py',
|
||||||
'callgate.py',
|
'callgate.py',
|
||||||
'chains.py',
|
'chains.py',
|
||||||
'choosers.py',
|
'choosers.py',
|
||||||
'compat7z.py',
|
'compat7z.py',
|
||||||
'countdowns.py',
|
'countdowns.py',
|
||||||
'descriptor.py',
|
'descriptor.py',
|
||||||
|
'desc_utils.py',
|
||||||
'dev_helper.py',
|
'dev_helper.py',
|
||||||
'display.py',
|
'display.py',
|
||||||
'drv_entro.py',
|
'drv_entro.py',
|
||||||
@ -26,6 +28,7 @@ freeze_as_mpy('', [
|
|||||||
'login.py',
|
'login.py',
|
||||||
'main.py',
|
'main.py',
|
||||||
'menu.py',
|
'menu.py',
|
||||||
|
'miniscript.py',
|
||||||
'multisig.py',
|
'multisig.py',
|
||||||
'numpad.py',
|
'numpad.py',
|
||||||
'nvstore.py',
|
'nvstore.py',
|
||||||
|
|||||||
1906
shared/miniscript.py
Normal file
1906
shared/miniscript.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,19 +3,23 @@
|
|||||||
# multisig.py - support code for multisig signing and p2sh in general.
|
# multisig.py - support code for multisig signing and p2sh in general.
|
||||||
#
|
#
|
||||||
import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson, version
|
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 ubinascii import hexlify as b2a_hex
|
||||||
from utils import str_to_keypath, problem_file_line, parse_extended_key, get_filesize
|
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 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 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 files import CardSlot, CardMissingError, needs_microsd
|
||||||
from descriptor import MultisigDescriptor, multisig_descriptor_template
|
from descriptor import Descriptor
|
||||||
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS
|
from miniscript import Key, Sortedmulti, Number, Multi
|
||||||
|
from desc_utils import multisig_descriptor_template
|
||||||
|
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_P2TR
|
||||||
from menu import MenuSystem, MenuItem, NonDefaultMenuItem
|
from menu import MenuSystem, MenuItem, NonDefaultMenuItem
|
||||||
from opcodes import OP_CHECKMULTISIG
|
from opcodes import OP_CHECKMULTISIG
|
||||||
from exceptions import FatalPSBTIssue
|
from exceptions import FatalPSBTIssue
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
|
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
|
# PSBT Xpub trust policies
|
||||||
TRUST_VERIFY = const(0)
|
TRUST_VERIFY = const(0)
|
||||||
@ -23,14 +27,11 @@ TRUST_OFFER = const(1)
|
|||||||
TRUST_PSBT = const(2)
|
TRUST_PSBT = const(2)
|
||||||
|
|
||||||
|
|
||||||
class MultisigOutOfSpace(RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def disassemble_multisig_mn(redeem_script):
|
def disassemble_multisig_mn(redeem_script):
|
||||||
# pull out just M and N from script. Simple, faster, no memory.
|
# pull out just M and N from script. Simple, faster, no memory.
|
||||||
|
|
||||||
assert MAX_SIGNERS == 15
|
if redeem_script[-1] != OP_CHECKMULTISIG:
|
||||||
assert redeem_script[-1] == OP_CHECKMULTISIG, 'need CHECKMULTISIG'
|
return None, None
|
||||||
|
|
||||||
M = redeem_script[0] - 80
|
M = redeem_script[0] - 80
|
||||||
N = redeem_script[-2] - 80
|
N = redeem_script[-2] - 80
|
||||||
@ -42,9 +43,7 @@ def disassemble_multisig(redeem_script):
|
|||||||
# - only for multisig scripts, not general purpose
|
# - only for multisig scripts, not general purpose
|
||||||
# - expect OP_1 (pk1) (pk2) (pk3) OP_3 OP_CHECKMULTISIG for 1 of 3 case
|
# - expect OP_1 (pk1) (pk2) (pk3) OP_3 OP_CHECKMULTISIG for 1 of 3 case
|
||||||
# - returns M, N, (list of pubkeys)
|
# - returns M, N, (list of pubkeys)
|
||||||
# - for very unlikely/impossible asserts, dont document reason; otherwise do.
|
# - for very unlikely/impossible asserts, don't document reason; otherwise do.
|
||||||
from serializations import disassemble
|
|
||||||
|
|
||||||
M, N = disassemble_multisig_mn(redeem_script)
|
M, N = disassemble_multisig_mn(redeem_script)
|
||||||
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
|
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
|
||||||
assert len(redeem_script) == 1 + (N * 34) + 1 + 1, 'bad len'
|
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)
|
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
|
# Capture the info we need to store long-term in order to participate in a
|
||||||
# multisig wallet as a co-signer.
|
# multisig wallet as a co-signer.
|
||||||
# - can be saved to nvram
|
# - can be saved to nvram
|
||||||
@ -122,19 +121,20 @@ class MultisigWallet(WalletABC):
|
|||||||
(AF_P2SH, 'p2sh'),
|
(AF_P2SH, 'p2sh'),
|
||||||
(AF_P2WSH, 'p2wsh'),
|
(AF_P2WSH, 'p2wsh'),
|
||||||
(AF_P2WSH_P2SH, 'p2sh-p2wsh'), # preferred
|
(AF_P2WSH_P2SH, 'p2sh-p2wsh'), # preferred
|
||||||
|
(AF_P2TR, 'p2tr'),
|
||||||
(AF_P2WSH_P2SH, 'p2wsh-p2sh'), # obsolete (now an alias)
|
(AF_P2WSH_P2SH, 'p2wsh-p2sh'), # obsolete (now an alias)
|
||||||
]
|
]
|
||||||
|
|
||||||
# optional: user can short-circuit many checks (system wide, one power-cycle only)
|
# optional: user can short-circuit many checks (system wide, one power-cycle only)
|
||||||
disable_checks = False
|
disable_checks = False
|
||||||
|
key_name = "multisig"
|
||||||
|
|
||||||
def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, chain_type='BTC', bip67=True):
|
def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, chain_type=None, bip67=True):
|
||||||
self.storage_idx = -1
|
super().__init__(chain_type=chain_type)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
assert len(m_of_n) == 2
|
assert len(m_of_n) == 2
|
||||||
self.M, self.N = m_of_n
|
self.M, self.N = m_of_n
|
||||||
self.chain_type = chain_type or 'BTC'
|
|
||||||
assert len(xpubs[0]) == 3
|
assert len(xpubs[0]) == 3
|
||||||
self.xpubs = xpubs # list of (xfp(int), deriv, xpub(str))
|
self.xpubs = xpubs # list of (xfp(int), deriv, xpub(str))
|
||||||
self.addr_fmt = addr_fmt # address format for wallet
|
self.addr_fmt = addr_fmt # address format for wallet
|
||||||
@ -163,17 +163,13 @@ class MultisigWallet(WalletABC):
|
|||||||
deriv = derivs[0]
|
deriv = derivs[0]
|
||||||
return deriv + '/%d/%d' % (change_idx, idx)
|
return deriv + '/%d/%d' % (change_idx, idx)
|
||||||
|
|
||||||
@property
|
|
||||||
def chain(self):
|
|
||||||
return chains.get_chain(self.chain_type)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_trust_policy(cls):
|
def get_trust_policy(cls):
|
||||||
|
|
||||||
which = settings.get('pms', None)
|
which = settings.get('pms', None)
|
||||||
|
exists, _ = cls.exists()
|
||||||
if which is None:
|
if which is None:
|
||||||
which = TRUST_VERIFY if cls.exists() else TRUST_OFFER
|
which = TRUST_VERIFY if exists else TRUST_OFFER
|
||||||
|
|
||||||
return which
|
return which
|
||||||
|
|
||||||
@ -239,14 +235,29 @@ class MultisigWallet(WalletABC):
|
|||||||
return rv
|
return rv
|
||||||
|
|
||||||
@classmethod
|
@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.
|
# 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!!
|
# - 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):
|
for idx, rec in enumerate(lst):
|
||||||
if idx == not_idx:
|
if not cls.is_correct_chain(rec, c):
|
||||||
# ignore one by index
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if M or N:
|
if M or N:
|
||||||
@ -343,57 +354,6 @@ class MultisigWallet(WalletABC):
|
|||||||
|
|
||||||
return False
|
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):
|
def has_similar(self):
|
||||||
# check if we already have a saved duplicate to this proposed wallet
|
# check if we already have a saved duplicate to this proposed wallet
|
||||||
# - return (name_change, diff_items, count_similar) where:
|
# - return (name_change, diff_items, count_similar) where:
|
||||||
@ -454,12 +414,12 @@ class MultisigWallet(WalletABC):
|
|||||||
else:
|
else:
|
||||||
raise IndexError # consistency bug
|
raise IndexError # consistency bug
|
||||||
|
|
||||||
lst = settings.get('multisig', [])
|
lst = settings.get(self.key_name, [])
|
||||||
del lst[self.storage_idx]
|
del lst[self.storage_idx]
|
||||||
if lst:
|
if lst:
|
||||||
settings.set('multisig', lst)
|
settings.set(self.key_name, lst)
|
||||||
else:
|
else:
|
||||||
settings.remove_key('multisig')
|
settings.remove_key(self.key_name)
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
self.storage_idx = -1
|
self.storage_idx = -1
|
||||||
@ -472,7 +432,7 @@ class MultisigWallet(WalletABC):
|
|||||||
def yield_addresses(self, start_idx, count, change_idx=0):
|
def yield_addresses(self, start_idx, count, change_idx=0):
|
||||||
# Assuming a suffix of /0/0 on the defined prefix's, yield
|
# Assuming a suffix of /0/0 on the defined prefix's, yield
|
||||||
# possible deposit addresses for this wallet.
|
# possible deposit addresses for this wallet.
|
||||||
ch = self.chain
|
ch = chains.current_chain()
|
||||||
|
|
||||||
assert self.addr_fmt, 'no addr fmt known'
|
assert self.addr_fmt, 'no addr fmt known'
|
||||||
|
|
||||||
@ -501,6 +461,35 @@ class MultisigWallet(WalletABC):
|
|||||||
idx += 1
|
idx += 1
|
||||||
count -= 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):
|
def validate_script(self, redeem_script, subpaths=None, xfp_paths=None):
|
||||||
# Check we can generate all pubkeys in the redeem script, raise on errors.
|
# Check we can generate all pubkeys in the redeem script, raise on errors.
|
||||||
# - working from pubkeys in the script, because duplicate XFP can happen
|
# - working from pubkeys in the script, because duplicate XFP can happen
|
||||||
@ -572,7 +561,7 @@ class MultisigWallet(WalletABC):
|
|||||||
found_pk = node.pubkey()
|
found_pk = node.pubkey()
|
||||||
|
|
||||||
# Document path(s) used. Not sure this is useful info to user tho.
|
# 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.
|
# part of the path from fingerprint to here.
|
||||||
here = '[%s]' % xfp2str(xfp)
|
here = '[%s]' % xfp2str(xfp)
|
||||||
if dp != len(path):
|
if dp != len(path):
|
||||||
@ -683,7 +672,9 @@ class MultisigWallet(WalletABC):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# deserialize, update list and lots of checks
|
# 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:
|
if is_mine:
|
||||||
has_mine += 1
|
has_mine += 1
|
||||||
|
|
||||||
@ -696,21 +687,35 @@ class MultisigWallet(WalletABC):
|
|||||||
my_xfp = settings.get('xfp')
|
my_xfp = settings.get('xfp')
|
||||||
xpubs = []
|
xpubs = []
|
||||||
|
|
||||||
desc = MultisigDescriptor.parse(descriptor)
|
descriptor = Descriptor.from_string(descriptor)
|
||||||
for xfp, deriv, xpub in desc.keys:
|
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)
|
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:
|
if is_mine:
|
||||||
has_mine += 1
|
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):
|
def to_descriptor(self):
|
||||||
return MultisigDescriptor(
|
keys = [
|
||||||
M=self.M, N=self.N,
|
Key.from_cc_data(xfp, deriv, xpub)
|
||||||
keys=self.xpubs,
|
for xfp, deriv, xpub in self.xpubs
|
||||||
addr_fmt=self.addr_fmt,
|
]
|
||||||
is_sorted=self.bip67,
|
_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
|
@classmethod
|
||||||
def from_file(cls, config, name=None):
|
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)
|
# - M of N line (assume N of N if not spec'd)
|
||||||
# - xpub: any bip32 serialization we understand, but be consistent
|
# - xpub: any bip32 serialization we understand, but be consistent
|
||||||
#
|
#
|
||||||
expect_chain = chains.current_chain().ctype
|
expect_chain = chains.current_key_chain().ctype
|
||||||
if MultisigDescriptor.is_descriptor(config):
|
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)
|
_, addr_fmt, xpubs, has_mine, M, N, bip67 = cls.from_descriptor(config)
|
||||||
if not bip67 and not settings.get("unsort_ms", 0):
|
if not bip67 and not settings.get("unsort_ms", 0):
|
||||||
# BIP-67 disabled, but unsort_ms not allowed - raise
|
# 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,
|
return cls(name, (M, N), xpubs, addr_fmt=addr_fmt,
|
||||||
chain_type=expect_chain, bip67=bip67)
|
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'):
|
def make_fname(self, prefix, suffix='txt'):
|
||||||
rv = '%s-%s.%s' % (prefix, self.name, suffix)
|
rv = '%s-%s.%s' % (prefix, self.name, suffix)
|
||||||
return rv.replace(' ', '_')
|
return rv.replace(' ', '_')
|
||||||
@ -956,7 +886,7 @@ class MultisigWallet(WalletABC):
|
|||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
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
|
return
|
||||||
|
|
||||||
def render_export(self, fp, hdr_comment=None, descriptor=False, core=False, desc_pretty=True):
|
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)
|
print("importdescriptors '%s'\n" % core_str, file=fp)
|
||||||
else:
|
else:
|
||||||
if desc_pretty:
|
if desc_pretty:
|
||||||
desc = desc_obj.pretty_serialize()
|
# TODO pretty serialize
|
||||||
|
desc = desc_obj.to_string(internal=False)
|
||||||
else:
|
else:
|
||||||
desc = desc_obj.serialize()
|
desc = desc_obj.to_string(internal=False)
|
||||||
print("%s\n" % desc, file=fp)
|
print("%s\n" % desc, file=fp)
|
||||||
else:
|
else:
|
||||||
if hdr_comment:
|
if hdr_comment:
|
||||||
@ -1043,8 +974,9 @@ class MultisigWallet(WalletABC):
|
|||||||
for k, v in xpubs_list:
|
for k, v in xpubs_list:
|
||||||
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
|
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
|
||||||
xpub = ngu.codecs.b58_encode(v)
|
xpub = ngu.codecs.b58_encode(v)
|
||||||
is_mine = cls.check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
|
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
|
||||||
expect_chain, my_xfp, xpubs)
|
expect_chain, my_xfp, cls.disable_checks)
|
||||||
|
xpubs.append(item)
|
||||||
if is_mine:
|
if is_mine:
|
||||||
has_mine += 1
|
has_mine += 1
|
||||||
addr_fmt = cls.guess_addr_fmt(path)
|
addr_fmt = cls.guess_addr_fmt(path)
|
||||||
@ -1054,7 +986,7 @@ class MultisigWallet(WalletABC):
|
|||||||
name = 'PSBT-%d-of-%d' % (M, N)
|
name = 'PSBT-%d-of-%d' % (M, N)
|
||||||
# this will always create sortedmulti multisig (BIP-67)
|
# this will always create sortedmulti multisig (BIP-67)
|
||||||
# because BIP-174 came years after wide spread acceptance of BIP-67 policy
|
# 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
|
# 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
|
# 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
|
# cleanup and normalize xpub
|
||||||
tmp = []
|
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]
|
(_, deriv, xpub_reserialized) = tmp[0]
|
||||||
assert deriv # because given as arg
|
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
|
continue
|
||||||
|
|
||||||
if ch == 'y' and not is_dup:
|
if ch == 'y' and not is_dup:
|
||||||
# save to nvram, may raise MultisigOutOfSpace
|
# save to nvram, may raise WalletOutOfSpace
|
||||||
if name_change:
|
if name_change:
|
||||||
name_change.delete()
|
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))
|
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.
|
# SLIP-132 format [yz]pubs here when not p2sh mode.
|
||||||
# - has same info as proper bitcoin serialization, but looks much different
|
# - has same info as proper bitcoin serialization, but looks much different
|
||||||
node = self.chain.deserialize_node(xpub, AF_P2SH)
|
node = self.chain.deserialize_node(xpub, AF_P2SH)
|
||||||
@ -1343,8 +1277,11 @@ class MultisigMenu(MenuSystem):
|
|||||||
def construct(cls):
|
def construct(cls):
|
||||||
# Dynamic menu with user-defined names of wallets shown
|
# Dynamic menu with user-defined names of wallets shown
|
||||||
|
|
||||||
if not MultisigWallet.exists():
|
from bsms import make_ms_wallet_bsms_menu
|
||||||
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
|
|
||||||
|
exists, exists_other_chain = MultisigWallet.exists()
|
||||||
|
if not exists:
|
||||||
|
rv = [MenuItem(MultisigWallet.none_setup_yet(exists_other_chain), f=no_ms_yet)]
|
||||||
else:
|
else:
|
||||||
rv = []
|
rv = []
|
||||||
for ms in MultisigWallet.get_all():
|
for ms in MultisigWallet.get_all():
|
||||||
@ -1357,6 +1294,7 @@ class MultisigMenu(MenuSystem):
|
|||||||
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc,
|
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc,
|
||||||
predicate=bool(NFC), shortcut=KEY_NFC))
|
predicate=bool(NFC), shortcut=KEY_NFC))
|
||||||
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
|
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('Create Airgapped', f=create_ms_step1))
|
||||||
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
|
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
|
||||||
rv.append(MenuItem('Skip Checks?', f=disable_checks_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...")
|
dis.fullscreen("Wait...")
|
||||||
ms = item.arg
|
ms = item.arg
|
||||||
desc = ms.to_descriptor()
|
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")
|
ch = await ux_show_story("Press (1) to export in pretty human readable format.\n\n" + desc_str, escape="1")
|
||||||
if ch == "1":
|
if ch == "1":
|
||||||
await ms.export_wallet_file(descriptor=True, desc_pretty=True)
|
await ms.export_wallet_file(descriptor=True, desc_pretty=True)
|
||||||
@ -1516,6 +1454,8 @@ P2SH-P2WSH:
|
|||||||
m/48h/{coin}h/{{acct}}h/1h
|
m/48h/{coin}h/{{acct}}h/1h
|
||||||
P2WSH:
|
P2WSH:
|
||||||
m/48h/{coin}h/{{acct}}h/2h
|
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)
|
{ok} to continue. {x} to abort.'''.format(coin=chain.b44_cointype, ok=OK, x=X)
|
||||||
|
|
||||||
@ -1534,9 +1474,10 @@ P2WSH:
|
|||||||
dis.fullscreen('Generating...')
|
dis.fullscreen('Generating...')
|
||||||
|
|
||||||
todo = [
|
todo = [
|
||||||
( "m/45h", 'p2sh', AF_P2SH), # iff acct_num == 0
|
("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/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH),
|
||||||
( "m/48h/{coin}h/{acct_num}h/2h", 'p2wsh', AF_P2WSH ),
|
("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):
|
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'])
|
deriv = cleanup_deriv_path(obj[af_str + '_deriv'])
|
||||||
ln = obj.get(af_str)
|
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):
|
async def ms_coordinator_qr(af_str, my_xfp, chain):
|
||||||
# Scan a number of JSON files from BBQr w/ derive, xfp and xpub details.
|
# 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
|
# sigh, OS/filesystem variations
|
||||||
file_size = var[1] if len(var) == 2 else get_filesize(full_fname)
|
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
|
# out of range size
|
||||||
continue
|
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)
|
ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, addr_fmt=addr_fmt)
|
||||||
|
|
||||||
if num_mine:
|
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
|
# menu item case: add to stack
|
||||||
from ux import the_ux
|
from ux import the_ux
|
||||||
@ -1818,7 +1761,7 @@ async def import_multisig_nfc(*a):
|
|||||||
from glob import NFC
|
from glob import NFC
|
||||||
# this menu option should not be available if NFC is disabled
|
# this menu option should not be available if NFC is disabled
|
||||||
try:
|
try:
|
||||||
return await NFC.import_multisig_nfc()
|
return await NFC.import_miniscript_nfc(legacy_multisig=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story(title="ERROR", msg="Failed to import multisig. %s" % str(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:
|
if 'pub' in ln:
|
||||||
return True
|
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)
|
taster=possible, force_vdisk=force_vdisk)
|
||||||
if not fn: return
|
if not fn: return
|
||||||
|
|
||||||
|
|||||||
211
shared/nfc.py
211
shared/nfc.py
@ -613,7 +613,6 @@ class NFCHandler:
|
|||||||
aborted = await n.share_text("NFC is working: %s" % n.get_uid(), allow_enter=False)
|
aborted = await n.share_text("NFC is working: %s" % n.get_uid(), allow_enter=False)
|
||||||
assert not aborted, "Aborted"
|
assert not aborted, "Aborted"
|
||||||
|
|
||||||
|
|
||||||
async def share_file(self):
|
async def share_file(self):
|
||||||
# Pick file from SD card and share over NFC...
|
# Pick file from SD card and share over NFC...
|
||||||
from actions import file_picker
|
from actions import file_picker
|
||||||
@ -663,51 +662,40 @@ class NFCHandler:
|
|||||||
# user is pushing a file downloaded from another CC over NFC
|
# user is pushing a file downloaded from another CC over NFC
|
||||||
# - would need an NFC app in between for the sneakernet step
|
# - would need an NFC app in between for the sneakernet step
|
||||||
# get some data
|
# get some data
|
||||||
data = await self.start_nfc_rx()
|
def f(m):
|
||||||
if not data: return
|
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(
|
# multi( catches both multi( and sortedmulti(
|
||||||
if 'pub' in msg or "multi(" in msg:
|
if 'pub' in m or "multi(" in m:
|
||||||
winner = msg
|
return m
|
||||||
break
|
|
||||||
|
|
||||||
if not winner:
|
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
|
||||||
await ux_show_story('Unable to find multisig descriptor.')
|
|
||||||
return
|
|
||||||
|
|
||||||
from auth import maybe_enroll_xpub
|
if winner:
|
||||||
try:
|
from auth import maybe_enroll_xpub
|
||||||
maybe_enroll_xpub(config=winner)
|
try:
|
||||||
except Exception as e:
|
maybe_enroll_xpub(config=winner)
|
||||||
#import sys; sys.print_exception(e)
|
except Exception as e:
|
||||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(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):
|
async def import_ephemeral_seed_words_nfc(self, *a):
|
||||||
data = await self.start_nfc_rx()
|
def f(m):
|
||||||
if not data: return
|
sm = m.decode().strip().split(" ")
|
||||||
|
if len(sm) in stash.SEED_LEN_OPTS:
|
||||||
|
return sm
|
||||||
|
|
||||||
winner = None
|
winner = await self._nfc_reader(f, 'Unable to find seed words')
|
||||||
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
|
|
||||||
|
|
||||||
if not winner:
|
if winner:
|
||||||
await ux_show_story('Unable to find seed words')
|
try:
|
||||||
return
|
from seed import set_ephemeral_seed_words
|
||||||
|
await set_ephemeral_seed_words(winner, meta='NFC Import')
|
||||||
try:
|
except Exception as e:
|
||||||
from seed import set_ephemeral_seed_words
|
#import sys; sys.print_exception(e)
|
||||||
await set_ephemeral_seed_words(winner, meta='NFC Import')
|
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
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):
|
async def confirm_share_loop(self, string):
|
||||||
while True:
|
while True:
|
||||||
@ -720,21 +708,16 @@ class NFCHandler:
|
|||||||
break
|
break
|
||||||
|
|
||||||
async def address_show_and_share(self):
|
async def address_show_and_share(self):
|
||||||
from auth import show_address, ApproveMessageSign
|
from auth import show_address
|
||||||
|
|
||||||
data = await self.start_nfc_rx()
|
def f(m):
|
||||||
if not data: return
|
sm = m.decode().split("\n")
|
||||||
|
if 1 <= len(sm) <= 2:
|
||||||
|
return sm
|
||||||
|
|
||||||
winner = None
|
winner = await self._nfc_reader(f, 'Expected address and derivation path.')
|
||||||
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
|
|
||||||
|
|
||||||
if not winner:
|
if not winner:
|
||||||
await ux_show_story('Expected address and derivation path.')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(winner) == 1:
|
if len(winner) == 1:
|
||||||
@ -759,19 +742,15 @@ class NFCHandler:
|
|||||||
|
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
|
|
||||||
data = await self.start_nfc_rx()
|
def f(m):
|
||||||
if not data: return
|
m = m.decode()
|
||||||
|
split_msg = m.split("\n")
|
||||||
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) <= 3:
|
if 1 <= len(split_msg) <= 3:
|
||||||
winner = split_msg
|
return split_msg
|
||||||
break
|
|
||||||
|
winner = await self._nfc_reader(f, 'Unable to find correctly formated message to sign.')
|
||||||
|
|
||||||
if not winner:
|
if not winner:
|
||||||
await ux_show_story('Unable to find correctly formated message to sign.')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(winner) == 1:
|
if len(winner) == 1:
|
||||||
@ -805,82 +784,94 @@ class NFCHandler:
|
|||||||
async def verify_sig_nfc(self):
|
async def verify_sig_nfc(self):
|
||||||
from auth import verify_armored_signed_msg
|
from auth import verify_armored_signed_msg
|
||||||
|
|
||||||
data = await self.start_nfc_rx()
|
f = lambda x: x.decode().strip() if b"SIGNED MESSAGE" in x else None
|
||||||
if not data: return
|
winner = await self._nfc_reader(f, 'Unable to find signed message.')
|
||||||
|
|
||||||
winner = None
|
if winner:
|
||||||
for urn, msg, meta in ndef.record_parser(data):
|
await verify_armored_signed_msg(winner, digest_check=False)
|
||||||
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)
|
|
||||||
|
|
||||||
async def verify_address_nfc(self):
|
async def verify_address_nfc(self):
|
||||||
# Get an address or complete bip-21 url even and search it... slow.
|
# Get an address or complete bip-21 url even and search it... slow.
|
||||||
from utils import decode_bip21_text
|
from utils import decode_bip21_text
|
||||||
|
|
||||||
data = await self.start_nfc_rx()
|
def f(m):
|
||||||
if not data: return
|
m = m.decode()
|
||||||
|
what, vals = decode_bip21_text(m)
|
||||||
|
if what == 'addr':
|
||||||
|
return vals[1]
|
||||||
|
|
||||||
winner = None
|
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
|
||||||
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
|
|
||||||
|
|
||||||
if not winner:
|
if winner:
|
||||||
await ux_show_story('Unable to find address from NFC data.')
|
from ownership import OWNERSHIP
|
||||||
return
|
await OWNERSHIP.search_ux(winner)
|
||||||
|
|
||||||
from ownership import OWNERSHIP
|
|
||||||
await OWNERSHIP.search_ux(winner)
|
|
||||||
|
|
||||||
async def read_extended_private_key(self):
|
async def read_extended_private_key(self):
|
||||||
data = await self.start_nfc_rx()
|
f = lambda x: x.decode().strip() if b"prv" in x else None
|
||||||
if not data: return
|
return await self._nfc_reader(f, 'Unable to find extended private key.')
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def read_tapsigner_b64_backup(self):
|
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()
|
data = await self.start_nfc_rx()
|
||||||
if not data: return
|
if not data: return
|
||||||
|
|
||||||
winner = None
|
winner = None
|
||||||
for urn, msg, meta in ndef.record_parser(data):
|
for urn, msg, meta in ndef.record_parser(data):
|
||||||
msg = bytes(msg).decode() # from memory view
|
msg = bytes(msg)
|
||||||
try:
|
try:
|
||||||
if 150 <= len(msg) <= 280:
|
r = func(msg)
|
||||||
winner = a2b_base64(msg)
|
if r is not None:
|
||||||
|
winner = r
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not winner:
|
if not winner:
|
||||||
await ux_show_story('Unable to find base64 encoded TAPSIGNER backup.')
|
await ux_show_story(fail_msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
return winner
|
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
|
# EOF
|
||||||
|
|||||||
@ -21,7 +21,13 @@ from utils import problem_file_line, url_decode
|
|||||||
ONE_LINE = CHARS_W-2
|
ONE_LINE = CHARS_W-2
|
||||||
|
|
||||||
async def make_notes_menu(*a):
|
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.
|
# Explain feature, and then enable if interested. Drop them into menu.
|
||||||
ch = await ux_show_story('''\
|
ch = await ux_show_story('''\
|
||||||
Enable this feature to store short text notes and passwords inside the Coldcard.
|
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':
|
if ch != 'y':
|
||||||
return
|
return
|
||||||
|
|
||||||
# mark as enabled (altho empty)
|
# mark as enabled
|
||||||
settings.set('notes', [])
|
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.
|
# need to correct top menu now, so this choice is there.
|
||||||
goto_top_menu()
|
goto_top_menu()
|
||||||
@ -170,6 +178,7 @@ class NotesMenu(MenuSystem):
|
|||||||
async def disable_notes(cls, *a):
|
async def disable_notes(cls, *a):
|
||||||
# they don't want feature anymore; already checked no notes in effect
|
# they don't want feature anymore; already checked no notes in effect
|
||||||
# - no need for confirm, they aren't loosing anything
|
# - no need for confirm, they aren't loosing anything
|
||||||
|
settings.remove_key('secnap')
|
||||||
settings.remove_key('notes')
|
settings.remove_key('notes')
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ from utils import call_later_ms
|
|||||||
# _age = internal verison number for data (see below)
|
# _age = internal verison number for data (see below)
|
||||||
# tested = selftest has been completed successfully
|
# tested = selftest has been completed successfully
|
||||||
# multisig = list of defined multisig wallets (complex)
|
# multisig = list of defined multisig wallets (complex)
|
||||||
|
# miniscript = list of defined miniscript wallets (complex)
|
||||||
# pms = trust/import/distrust xpubs found in PSBT files
|
# pms = trust/import/distrust xpubs found in PSBT files
|
||||||
# fee_limit = (int) percentage of tx value allowed as max fee
|
# fee_limit = (int) percentage of tx value allowed as max fee
|
||||||
# axi = index of last selected address in explorer
|
# 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
|
# seedvault = (bool) opt-in enable seed vault feature
|
||||||
# seeds = list of stored secrets for seedvault feature
|
# seeds = list of stored secrets for seedvault feature
|
||||||
# bright = (int:0-255) LCD brightness when on battery
|
# 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
|
# 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
|
# accts = (list of tuples: (addr_fmt, account#)) Single-sig wallets we've seen them use
|
||||||
# aei = (bool) allow changing start index in Address Explorer
|
# 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
|
# terms_ok = customer has signed-off on the terms of sale
|
||||||
|
|
||||||
# settings linked to seed
|
# 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
|
# settings that does not make sense to copy to temporary secret
|
||||||
# LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"]
|
# LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"]
|
||||||
# prelogin settings - do not need to be part of other saved settings
|
# prelogin settings - do not need to be part of other saved settings
|
||||||
|
|||||||
@ -82,7 +82,7 @@ OP_RETURN = const(106)
|
|||||||
#OP_RSHIFT = const(153)
|
#OP_RSHIFT = const(153)
|
||||||
#OP_BOOLAND = const(154)
|
#OP_BOOLAND = const(154)
|
||||||
#OP_BOOLOR = const(155)
|
#OP_BOOLOR = const(155)
|
||||||
#OP_NUMEQUAL = const(156)
|
OP_NUMEQUAL = const(156)
|
||||||
#OP_NUMEQUALVERIFY = const(157)
|
#OP_NUMEQUALVERIFY = const(157)
|
||||||
#OP_NUMNOTEQUAL = const(158)
|
#OP_NUMNOTEQUAL = const(158)
|
||||||
#OP_LESSTHAN = const(159)
|
#OP_LESSTHAN = const(159)
|
||||||
@ -114,6 +114,7 @@ OP_CHECKMULTISIGVERIFY = const(175)
|
|||||||
#OP_NOP8 = const(183)
|
#OP_NOP8 = const(183)
|
||||||
#OP_NOP9 = const(184)
|
#OP_NOP9 = const(184)
|
||||||
#OP_NOP10 = const(185)
|
#OP_NOP10 = const(185)
|
||||||
|
OP_CHECKSIGADD = const(186)
|
||||||
#OP_NULLDATA = const(252)
|
#OP_NULLDATA = const(252)
|
||||||
#OP_PUBKEYHASH = const(253)
|
#OP_PUBKEYHASH = const(253)
|
||||||
#OP_PUBKEY = const(254)
|
#OP_PUBKEY = const(254)
|
||||||
|
|||||||
@ -7,6 +7,8 @@ from glob import settings
|
|||||||
from ucollections import namedtuple
|
from ucollections import namedtuple
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from exceptions import UnknownAddressExplained
|
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
|
# Track many addresses, but in compressed form
|
||||||
# - map from random Bech32/Base58 payment address to (wallet) + keypath
|
# - map from random Bech32/Base58 payment address to (wallet) + keypath
|
||||||
@ -49,7 +51,7 @@ class AddressCacheFile:
|
|||||||
def __init__(self, wallet, change_idx):
|
def __init__(self, wallet, change_idx):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.change_idx = change_idx
|
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))
|
h = b2a_hex(ngu.hash.sha256d(wallet.chain.ctype + desc))
|
||||||
self.fname = h[0:32] + '-%d.own' % change_idx
|
self.fname = h[0:32] + '-%d.own' % change_idx
|
||||||
self.salt = h[32:]
|
self.salt = h[32:]
|
||||||
@ -158,8 +160,8 @@ class AddressCacheFile:
|
|||||||
|
|
||||||
self.setup(self.change_idx, start_idx)
|
self.setup(self.change_idx, start_idx)
|
||||||
|
|
||||||
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
|
# change_idx is used as flag here
|
||||||
change_idx=self.change_idx):
|
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count, self.change_idx):
|
||||||
|
|
||||||
if here == addr:
|
if here == addr:
|
||||||
# Found it! But keep going a little for next time.
|
# 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
|
# - returns wallet object, and tuple2 of final 2 subpath components
|
||||||
# - if you start w/ testnet, we'll follow that
|
# - if you start w/ testnet, we'll follow that
|
||||||
from multisig import MultisigWallet
|
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
|
from glob import dis
|
||||||
|
|
||||||
ch = chains.current_chain()
|
ch = chains.current_chain()
|
||||||
@ -220,21 +222,28 @@ class OwnershipCache:
|
|||||||
|
|
||||||
possibles = []
|
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:
|
if addr_fmt & AFC_SCRIPT:
|
||||||
# multisig or script at least.. must exist already
|
# multisig or script at least.. must exist already
|
||||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
|
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:
|
if addr_fmt == AF_P2SH:
|
||||||
# might look like P2SH but actually be AF_P2WSH_P2SH
|
# might look like P2SH but actually be AF_P2WSH_P2SH
|
||||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=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
|
# 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,
|
# thing that hopefully is going away, so if they have any multisig wallets,
|
||||||
# defined, assume that that's the only p2sh address source.
|
# defined, assume that that's the only p2sh address source.
|
||||||
addr_fmt = AF_P2WPKH_P2SH
|
addr_fmt = AF_P2WPKH_P2SH
|
||||||
|
|
||||||
# TODO: add tapscript and such fancy stuff here
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Construct possible single-signer wallets, always at least account=0 case
|
# Construct possible single-signer wallets, always at least account=0 case
|
||||||
from wallet import MasterSingleSigWallet
|
from wallet import MasterSingleSigWallet
|
||||||
@ -252,7 +261,7 @@ class OwnershipCache:
|
|||||||
if not possibles:
|
if not possibles:
|
||||||
# can only happen w/ scripts; for single-signer we have things to check
|
# can only happen w/ scripts; for single-signer we have things to check
|
||||||
raise UnknownAddressExplained(
|
raise UnknownAddressExplained(
|
||||||
"No suitable multisig wallets are currently defined.")
|
"No suitable multisig/miniscript wallets are currently defined.")
|
||||||
|
|
||||||
# "quick" check first, before doing any generations
|
# "quick" check first, before doing any generations
|
||||||
|
|
||||||
@ -314,7 +323,8 @@ class OwnershipCache:
|
|||||||
|
|
||||||
msg = addr
|
msg = addr
|
||||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
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:
|
if version.has_qwerty:
|
||||||
esc = KEY_QR
|
esc = KEY_QR
|
||||||
else:
|
else:
|
||||||
@ -325,11 +335,15 @@ class OwnershipCache:
|
|||||||
ch = await ux_show_story(msg, title="Verified Address",
|
ch = await ux_show_story(msg, title="Verified Address",
|
||||||
escape=esc, hint_icons=KEY_QR)
|
escape=esc, hint_icons=KEY_QR)
|
||||||
if ch != esc: break
|
if ch != esc: break
|
||||||
await show_qr_code(addr, is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
await show_qr_code(addr,
|
||||||
msg=addr)
|
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
||||||
|
msg=addr)
|
||||||
|
|
||||||
except UnknownAddressExplained as exc:
|
except UnknownAddressExplained as exc:
|
||||||
await ux_show_story(addr + '\n\n' + str(exc), title="Unknown Address")
|
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
|
@classmethod
|
||||||
def note_subpath_used(cls, subpath):
|
def note_subpath_used(cls, subpath):
|
||||||
|
|||||||
@ -3,14 +3,15 @@
|
|||||||
#
|
#
|
||||||
# paper.py - generate paper wallets, based on random values (not linked to wallet)
|
# 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 ubinascii import hexlify as b2a_hex
|
||||||
from utils import imported
|
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 ux import ux_show_story, ux_dramatic_pause
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
from actions import file_picker
|
from actions import file_picker
|
||||||
from menu import MenuSystem, MenuItem
|
from menu import MenuSystem, MenuItem
|
||||||
|
from stash import blank_object
|
||||||
|
|
||||||
background_msg = '''\
|
background_msg = '''\
|
||||||
Coldcard will pick a random private key (which has no relation to your seed words), \
|
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"
|
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.
|
# These very-specific text values are matched on the Coldcard; cannot be changed.
|
||||||
class placeholders:
|
class placeholders:
|
||||||
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
|
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
|
||||||
@ -51,6 +48,12 @@ class PaperWalletMaker:
|
|||||||
self.my_menu = my_menu
|
self.my_menu = my_menu
|
||||||
self.template_fn = None
|
self.template_fn = None
|
||||||
self.is_segwit = False
|
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):
|
async def pick_template(self, *a):
|
||||||
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
|
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
|
||||||
@ -62,17 +65,17 @@ class PaperWalletMaker:
|
|||||||
def addr_format_chooser(self, *a):
|
def addr_format_chooser(self, *a):
|
||||||
# simple bool choice
|
# simple bool choice
|
||||||
def set(idx, text):
|
def set(idx, text):
|
||||||
self.is_segwit = bool(idx)
|
self.is_segwit = idx == 1
|
||||||
|
self.is_taproot = idx == 2
|
||||||
self.update_menu()
|
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):
|
def update_menu(self):
|
||||||
# Reconstruct the menu contents based on our state.
|
# Reconstruct the menu contents based on our state.
|
||||||
self.my_menu.replace_items([
|
self.my_menu.replace_items([
|
||||||
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
|
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
|
||||||
f=self.pick_template),
|
f=self.pick_template),
|
||||||
MenuItem('Classic P2PKH' if not self.is_segwit else 'Segwit P2WPKH',
|
MenuItem(self.atype()[1], chooser=self.addr_format_chooser),
|
||||||
chooser=self.addr_format_chooser),
|
|
||||||
MenuItem('Use Dice', f=self.use_dice),
|
MenuItem('Use Dice', f=self.use_dice),
|
||||||
MenuItem('GENERATE WALLET', f=self.doit),
|
MenuItem('GENERATE WALLET', f=self.doit),
|
||||||
], keep_position=True)
|
], keep_position=True)
|
||||||
@ -82,12 +85,6 @@ class PaperWalletMaker:
|
|||||||
from glob import dis, VD
|
from glob import dis, VD
|
||||||
|
|
||||||
try:
|
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:
|
if not have_key:
|
||||||
# get some random bytes
|
# get some random bytes
|
||||||
await ux_dramatic_pause("Picking key...", 2)
|
await ux_dramatic_pause("Picking key...", 2)
|
||||||
@ -104,12 +101,16 @@ class PaperWalletMaker:
|
|||||||
dis.fullscreen("Rendering...")
|
dis.fullscreen("Rendering...")
|
||||||
|
|
||||||
# make payment address
|
# make payment address
|
||||||
digest = hash160(pubkey)
|
ch = chains.current_chain()
|
||||||
ch = current_chain()
|
|
||||||
if self.is_segwit:
|
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:
|
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')
|
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
|
||||||
|
|
||||||
@ -164,8 +165,11 @@ class PaperWalletMaker:
|
|||||||
else:
|
else:
|
||||||
nice_pdf = ''
|
nice_pdf = ''
|
||||||
|
|
||||||
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
|
nice_sig = None
|
||||||
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
|
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
|
# Half-hearted attempt to cleanup secrets-contaminated memory
|
||||||
# - better would be force user to reboot
|
# - better would be force user to reboot
|
||||||
@ -185,7 +189,8 @@ class PaperWalletMaker:
|
|||||||
story = "Done! Created file(s):\n\n%s" % nice_txt
|
story = "Done! Created file(s):\n\n%s" % nice_txt
|
||||||
if nice_pdf:
|
if nice_pdf:
|
||||||
story += "\n\n%s" % 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)
|
await ux_show_story(story)
|
||||||
|
|
||||||
async def use_dice(self, *a):
|
async def use_dice(self, *a):
|
||||||
@ -214,10 +219,17 @@ class PaperWalletMaker:
|
|||||||
fp.write('Bitcoin Core command:\n\n')
|
fp.write('Bitcoin Core command:\n\n')
|
||||||
|
|
||||||
# new hotness: output descriptors
|
# new hotness: output descriptors
|
||||||
desc = ('wpkh(%s)' if self.is_segwit else 'pkh(%s)') % wif
|
if self.is_taproot:
|
||||||
multi = ujson.dumps(dict(timestamp=FEATURE_RELEASE_TIME, desc=append_checksum(desc)))
|
desc = 'tr(%s)'
|
||||||
fp.write(" bitcoin-cli importmulti '[%s]'\n\n" % multi)
|
elif self.is_segwit:
|
||||||
fp.write('# OR (more compatible, but slower)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
|
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:
|
if qr_addr and qr_wif:
|
||||||
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')
|
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')
|
||||||
|
|||||||
966
shared/psbt.py
966
shared/psbt.py
File diff suppressed because it is too large
Load Diff
@ -423,9 +423,11 @@ async def add_seed_to_vault(encoded, meta=None):
|
|||||||
|
|
||||||
if not settings.master_get("seedvault", False):
|
if not settings.master_get("seedvault", False):
|
||||||
# seed vault disabled
|
# seed vault disabled
|
||||||
|
# this can be re-enabled by attacker in deltamode
|
||||||
return
|
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 save anything if no SE secret yet
|
||||||
|
# do not offer any access to SV in deltamode
|
||||||
return
|
return
|
||||||
|
|
||||||
# do not offer to store secrets that are already in vault
|
# do not offer to store secrets that are already in vault
|
||||||
@ -828,42 +830,37 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
async def _remove(menu, label, item):
|
async def _remove(menu, label, item):
|
||||||
from glob import dis, settings
|
from glob import dis, settings
|
||||||
|
|
||||||
|
esc = ""
|
||||||
|
tmp_val = False
|
||||||
idx, xfp_str, encoded = item.arg
|
idx, xfp_str, encoded = item.arg
|
||||||
|
current_active = (pa.tmp_value == bytes(encoded))
|
||||||
|
|
||||||
msg = ("Remove seed from seed vault and delete its "
|
msg = "Remove seed from seed vault "
|
||||||
"settings?\n\nPress %s to continue, press (1) to "
|
if pa.tmp_value and current_active:
|
||||||
"only remove from seed vault and keep "
|
tmp_val = True
|
||||||
"encrypted settings for later use.\n\n"
|
msg += "?\n\n"
|
||||||
"WARNING: Funds will be lost if wallet is"
|
else:
|
||||||
" not backed-up elsewhere.") % OK
|
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
|
if ch == "x": return
|
||||||
|
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
|
|
||||||
wipe_slot = (ch != "1")
|
wipe_slot = not current_active and (ch != "1")
|
||||||
tmp_val = False
|
|
||||||
|
|
||||||
if pa.tmp_value:
|
|
||||||
tmp_val = True
|
|
||||||
|
|
||||||
if wipe_slot:
|
if wipe_slot:
|
||||||
# are we deleting current active ephemeral wallet
|
xs = SettingsObject()
|
||||||
# and its settings ?
|
xs.set_key(encoded)
|
||||||
# slot wiping
|
xs.load()
|
||||||
if tmp_val:
|
xs.blank()
|
||||||
# wipe current settings
|
del xs
|
||||||
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
|
|
||||||
|
|
||||||
# CAUTION: will get shadow copy if in tmp seed mode already
|
# CAUTION: will get shadow copy if in tmp seed mode already
|
||||||
seeds = settings.master_get("seeds", [])
|
seeds = settings.master_get("seeds", [])
|
||||||
@ -970,6 +967,12 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
from glob import settings
|
from glob import settings
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
|
|
||||||
|
if pa.is_deltamode():
|
||||||
|
# attacker has re-enabled SeedVault in Settings
|
||||||
|
import callgate
|
||||||
|
callgate.fast_wipe()
|
||||||
|
|
||||||
|
|
||||||
rv = []
|
rv = []
|
||||||
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)
|
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ ser_*, deser_*: functions that handle serialization/deserialization
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import unhexlify as a2b_hex
|
|
||||||
import ustruct as struct
|
import ustruct as struct
|
||||||
import ngu
|
import ngu
|
||||||
from opcodes import *
|
from opcodes import *
|
||||||
@ -30,6 +29,7 @@ hash160 = ngu.hash.hash160
|
|||||||
def bytes_to_hex_str(s):
|
def bytes_to_hex_str(s):
|
||||||
return str(b2a_hex(s), 'ascii')
|
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_ALL = const(1)
|
||||||
SIGHASH_NONE = const(2)
|
SIGHASH_NONE = const(2)
|
||||||
SIGHASH_SINGLE = const(3)
|
SIGHASH_SINGLE = const(3)
|
||||||
@ -37,6 +37,7 @@ SIGHASH_ANYONECANPAY = const(0x80)
|
|||||||
|
|
||||||
# list containing all flags that we support signing for
|
# list containing all flags that we support signing for
|
||||||
ALL_SIGHASH_FLAGS = [
|
ALL_SIGHASH_FLAGS = [
|
||||||
|
SIGHASH_DEFAULT,
|
||||||
SIGHASH_ALL,
|
SIGHASH_ALL,
|
||||||
SIGHASH_NONE,
|
SIGHASH_NONE,
|
||||||
SIGHASH_SINGLE,
|
SIGHASH_SINGLE,
|
||||||
@ -56,14 +57,20 @@ def ser_compact_size(l):
|
|||||||
else:
|
else:
|
||||||
return struct.pack("<BQ", 255, l)
|
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]
|
nit = struct.unpack("<B", f.read(1))[0]
|
||||||
|
num_bytes = 1
|
||||||
if nit == 253:
|
if nit == 253:
|
||||||
nit = struct.unpack("<H", f.read(2))[0]
|
nit = struct.unpack("<H", f.read(2))[0]
|
||||||
|
num_bytes += 2
|
||||||
elif nit == 254:
|
elif nit == 254:
|
||||||
nit = struct.unpack("<I", f.read(4))[0]
|
nit = struct.unpack("<I", f.read(4))[0]
|
||||||
|
num_bytes += 4
|
||||||
elif nit == 255:
|
elif nit == 255:
|
||||||
nit = struct.unpack("<Q", f.read(8))[0]
|
nit = struct.unpack("<Q", f.read(8))[0]
|
||||||
|
num_bytes += 8
|
||||||
|
if ret_num_bytes:
|
||||||
|
return nit, num_bytes
|
||||||
return nit
|
return nit
|
||||||
|
|
||||||
def deser_string(f):
|
def deser_string(f):
|
||||||
@ -367,6 +374,11 @@ class CTxOut(object):
|
|||||||
# aka. P2WPKH
|
# aka. P2WPKH
|
||||||
return 'p2pkh', self.scriptPubKey[2:2+20], True
|
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 \
|
if len(self.scriptPubKey) == 34 and \
|
||||||
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
|
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
|
||||||
# aka. P2WSH
|
# aka. P2WSH
|
||||||
|
|||||||
@ -53,7 +53,7 @@ HSM_WHITELIST = frozenset({
|
|||||||
'blkc', 'hsts', # report status values
|
'blkc', 'hsts', # report status values
|
||||||
'stok', 'smok', # completion check: sign txn or msg
|
'stok', 'smok', # completion check: sign txn or msg
|
||||||
'xpub', 'msck', # quick status checks
|
'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
|
'user', # auth HSM user, other user cmds not allowed
|
||||||
'gslr', # read storage locker; hsm mode only, limited usage
|
'gslr', # read storage locker; hsm mode only, limited usage
|
||||||
})
|
})
|
||||||
@ -483,7 +483,7 @@ class USBHandler:
|
|||||||
file_len, file_sha = unpack_from('<I32s', args)
|
file_len, file_sha = unpack_from('<I32s', args)
|
||||||
if file_sha != self.file_checksum.digest():
|
if file_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
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
|
# Start an UX interaction, return immediately here
|
||||||
from auth import maybe_enroll_xpub
|
from auth import maybe_enroll_xpub
|
||||||
@ -491,6 +491,82 @@ class USBHandler:
|
|||||||
|
|
||||||
return None
|
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':
|
if cmd == 'msck':
|
||||||
# Quick check to test if we have a wallet already installed.
|
# Quick check to test if we have a wallet already installed.
|
||||||
from multisig import MultisigWallet
|
from multisig import MultisigWallet
|
||||||
|
|||||||
126
shared/utils.py
126
shared/utils.py
@ -2,12 +2,14 @@
|
|||||||
#
|
#
|
||||||
# utils.py - Misc utils. My favourite kind of source file.
|
# 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 unhexlify as a2b_hex
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import a2b_base64, b2a_base64
|
from ubinascii import a2b_base64, b2a_base64
|
||||||
from uhashlib import sha256
|
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')
|
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
||||||
|
|
||||||
@ -91,7 +93,6 @@ def pop_count(i):
|
|||||||
|
|
||||||
def get_filesize(fn):
|
def get_filesize(fn):
|
||||||
# like os.path.getsize()
|
# like os.path.getsize()
|
||||||
import uos
|
|
||||||
try:
|
try:
|
||||||
return uos.stat(fn)[6]
|
return uos.stat(fn)[6]
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -220,8 +221,6 @@ def to_ascii_printable(s, strip=False):
|
|||||||
def problem_file_line(exc):
|
def problem_file_line(exc):
|
||||||
# return a string of just the filename.py and line number where
|
# return a string of just the filename.py and line number where
|
||||||
# an exception occured. Best used on AssertionError.
|
# an exception occured. Best used on AssertionError.
|
||||||
import uio, sys, ure
|
|
||||||
|
|
||||||
tmp = uio.StringIO()
|
tmp = uio.StringIO()
|
||||||
sys.print_exception(exc, tmp)
|
sys.print_exception(exc, tmp)
|
||||||
lines = tmp.getvalue().split('\n')[-3:]
|
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
|
# - assume 'm' prefix, so '34' becomes 'm/34', etc
|
||||||
# - do not assume /// is m/0/0/0
|
# - do not assume /// is m/0/0/0
|
||||||
# - if allow_star, then final position can be * or *h (wildcard)
|
# - if allow_star, then final position can be * or *h (wildcard)
|
||||||
import ure
|
|
||||||
from public_constants import MAX_PATH_DEPTH
|
from public_constants import MAX_PATH_DEPTH
|
||||||
|
|
||||||
s = to_ascii_printable(bin_path, strip=True).lower()
|
s = to_ascii_printable(bin_path, strip=True).lower()
|
||||||
@ -345,6 +343,13 @@ def match_deriv_path(patterns, path):
|
|||||||
|
|
||||||
return False
|
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:
|
class DecodeStreamer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.runt = bytearray()
|
self.runt = bytearray()
|
||||||
@ -431,7 +436,7 @@ def clean_shutdown(style=0):
|
|||||||
# wipe SPI flash and shutdown (wiping main memory)
|
# wipe SPI flash and shutdown (wiping main memory)
|
||||||
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
# - 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
|
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
||||||
import callgate, version, uasyncio
|
import callgate, uasyncio
|
||||||
|
|
||||||
# save if anything pending
|
# save if anything pending
|
||||||
from glob import settings
|
from glob import settings
|
||||||
@ -507,9 +512,7 @@ def word_wrap(ln, w):
|
|||||||
|
|
||||||
def parse_addr_fmt_str(addr_fmt):
|
def parse_addr_fmt_str(addr_fmt):
|
||||||
# accepts strings and also integers if already parsed
|
# 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, AF_P2TR]:
|
||||||
|
|
||||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
|
||||||
return addr_fmt
|
return addr_fmt
|
||||||
|
|
||||||
addr_fmt = addr_fmt.lower()
|
addr_fmt = addr_fmt.lower()
|
||||||
@ -519,9 +522,10 @@ def parse_addr_fmt_str(addr_fmt):
|
|||||||
return AF_CLASSIC
|
return AF_CLASSIC
|
||||||
elif addr_fmt == "p2wpkh":
|
elif addr_fmt == "p2wpkh":
|
||||||
return AF_P2WPKH
|
return AF_P2WPKH
|
||||||
|
elif addr_fmt == "p2tr":
|
||||||
|
return AF_P2TR
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid address format: '%s'\n\n"
|
raise ValueError("Unsupported address format: '%s'" % addr_fmt)
|
||||||
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
|
|
||||||
|
|
||||||
def parse_extended_key(ln, private=False):
|
def parse_extended_key(ln, private=False):
|
||||||
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
|
# 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 {
|
return {
|
||||||
AF_CLASSIC: "Classic P2PKH",
|
AF_CLASSIC: "Classic P2PKH",
|
||||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
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]
|
}[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)
|
dts = fmt % (y, mo, d, h, mi, s)
|
||||||
return dts + " UTC"
|
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):
|
def txid_from_fname(fname):
|
||||||
if len(fname) >= 64:
|
if len(fname) >= 64:
|
||||||
@ -698,7 +700,95 @@ def decode_bip21_text(got):
|
|||||||
|
|
||||||
raise ValueError('not bip-21')
|
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):
|
def encode_seed_qr(words):
|
||||||
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -456,7 +456,7 @@ def import_export_prompt_decode(ch):
|
|||||||
|
|
||||||
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
||||||
no_nfc=False, title=None, intro='', footnotes='',
|
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
|
# Show story allowing user to select source for importing/exporting
|
||||||
# - return either str(mode) OR dict(file_args)
|
# - return either str(mode) OR dict(file_args)
|
||||||
# - KEY_NFC or KEY_QR for those sources
|
# - 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:
|
if is_import:
|
||||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
|
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
|
||||||
else:
|
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
|
# TODO: detect if we're only asking A or B, when just one card is inserted
|
||||||
# - assume that's what they want to do
|
# - assume that's what they want to do
|
||||||
|
|||||||
@ -934,12 +934,13 @@ class QRScannerInteraction:
|
|||||||
await ux_visualize_bip21(proto, addr, args)
|
await ux_visualize_bip21(proto, addr, args)
|
||||||
return
|
return
|
||||||
|
|
||||||
if what == "multi":
|
if what in ("multi", "minisc"):
|
||||||
from auth import maybe_enroll_xpub
|
from auth import maybe_enroll_xpub
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
ms_config, = vals
|
ms_config, = vals
|
||||||
try:
|
try:
|
||||||
maybe_enroll_xpub(config=ms_config)
|
maybe_enroll_xpub(config=ms_config,
|
||||||
|
miniscript=False if what == "multi" else None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story(
|
await ux_show_story(
|
||||||
'Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
'Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|||||||
@ -122,6 +122,9 @@ def probe_system():
|
|||||||
# what firmware signing key did we boot with? are we in dev mode?
|
# what firmware signing key did we boot with? are we in dev mode?
|
||||||
is_devmode = get_is_devmode()
|
is_devmode = get_is_devmode()
|
||||||
|
|
||||||
|
# newer, edge code in effect?
|
||||||
|
is_edge = (get_mpy_version()[1][-1] == 'X')
|
||||||
|
|
||||||
# increase size limits for mk4
|
# increase size limits for mk4
|
||||||
from public_constants import MAX_TXN_LEN_MK4, MAX_UPLOAD_LEN_MK4
|
from public_constants import MAX_TXN_LEN_MK4, MAX_UPLOAD_LEN_MK4
|
||||||
MAX_UPLOAD_LEN = MAX_UPLOAD_LEN_MK4
|
MAX_UPLOAD_LEN = MAX_UPLOAD_LEN_MK4
|
||||||
|
|||||||
143
shared/wallet.py
143
shared/wallet.py
@ -3,12 +3,17 @@
|
|||||||
# wallet.py - A place you find UTXO, addresses and descriptors.
|
# wallet.py - A place you find UTXO, addresses and descriptors.
|
||||||
#
|
#
|
||||||
import chains
|
import chains
|
||||||
from descriptor import Descriptor
|
from glob import settings
|
||||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
|
||||||
from stash import SensitiveValues
|
from stash import SensitiveValues
|
||||||
|
|
||||||
|
|
||||||
MAX_BIP32_IDX = (2 ** 31) - 1
|
MAX_BIP32_IDX = (2 ** 31) - 1
|
||||||
|
|
||||||
|
class WalletOutOfSpace(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WalletABC:
|
class WalletABC:
|
||||||
# How to make this ABC useful without consuming memory/code space??
|
# How to make this ABC useful without consuming memory/code space??
|
||||||
# - be more of an "interface" than a base class
|
# - 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.
|
# Construct a wallet based on current master secret, and chain.
|
||||||
# - path is optional, and then we use standard path for addr_fmt
|
# - path is optional, and then we use standard path for addr_fmt
|
||||||
# - path can be overriden when we come here via address explorer
|
# - path can be overriden when we come here via address explorer
|
||||||
|
if addr_fmt == AF_P2TR:
|
||||||
if addr_fmt == AF_P2WPKH:
|
n = 'Taproot P2TR'
|
||||||
|
prefix = path or 'm/86h/{coin_type}h/{account}h'
|
||||||
|
elif addr_fmt == AF_P2WPKH:
|
||||||
n = 'Segwit P2WPKH'
|
n = 'Segwit P2WPKH'
|
||||||
prefix = path or 'm/84h/{coin_type}h/{account}h'
|
prefix = path or 'm/84h/{coin_type}h/{account}h'
|
||||||
elif addr_fmt == AF_CLASSIC:
|
elif addr_fmt == AF_CLASSIC:
|
||||||
@ -66,7 +73,6 @@ class MasterSingleSigWallet(WalletABC):
|
|||||||
if self.chain.ctype == 'XRT':
|
if self.chain.ctype == 'XRT':
|
||||||
n += ' (Regtest)'
|
n += ' (Regtest)'
|
||||||
|
|
||||||
|
|
||||||
self.name = n
|
self.name = n
|
||||||
self.addr_fmt = addr_fmt
|
self.addr_fmt = addr_fmt
|
||||||
|
|
||||||
@ -82,7 +88,6 @@ class MasterSingleSigWallet(WalletABC):
|
|||||||
|
|
||||||
self._path = p
|
self._path = p
|
||||||
|
|
||||||
|
|
||||||
def yield_addresses(self, start_idx, count, change_idx=None):
|
def yield_addresses(self, start_idx, count, change_idx=None):
|
||||||
# Render a range of addresses. Slow to start, since accesses SE in general
|
# Render a range of addresses. Slow to start, since accesses SE in general
|
||||||
# - if count==1, don't derive any subkey, just do path.
|
# - if count==1, don't derive any subkey, just do path.
|
||||||
@ -126,10 +131,132 @@ class MasterSingleSigWallet(WalletABC):
|
|||||||
|
|
||||||
def to_descriptor(self):
|
def to_descriptor(self):
|
||||||
from glob import settings
|
from glob import settings
|
||||||
|
from descriptor import Descriptor, Key
|
||||||
xfp = settings.get('xfp')
|
xfp = settings.get('xfp')
|
||||||
xpub = settings.get('xpub')
|
xpub = settings.get('xpub')
|
||||||
keys = (xfp, self._path, xpub)
|
d = Descriptor(key=Key.from_cc_data(xfp, self._path, xpub))
|
||||||
return Descriptor([keys], self.addr_fmt)
|
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
|
# EOF
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
//
|
//
|
||||||
// AUTO-generated.
|
// AUTO-generated.
|
||||||
//
|
//
|
||||||
// built: 2024-09-12
|
// built: 2024-12-18
|
||||||
// version: 5.4.0
|
// version: 6.3.4X
|
||||||
//
|
//
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
// this overrides ports/stm32/fatfs_port.c
|
// this overrides ports/stm32/fatfs_port.c
|
||||||
uint32_t get_fattime(void) {
|
uint32_t get_fattime(void) {
|
||||||
return 0x592c2880UL;
|
return 0x59923060UL;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
//
|
//
|
||||||
// AUTO-generated.
|
// AUTO-generated.
|
||||||
//
|
//
|
||||||
// built: 2024-09-12
|
// built: 2024-12-18
|
||||||
// version: 1.3.0Q
|
// version: 6.3.4QX
|
||||||
//
|
//
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
// this overrides ports/stm32/fatfs_port.c
|
// this overrides ports/stm32/fatfs_port.c
|
||||||
uint32_t get_fattime(void) {
|
uint32_t get_fattime(void) {
|
||||||
return 0x592c0860UL;
|
return 0x59923060UL;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ LATEST_RELEASE = $(shell ls -t1 ../releases/*-mk4-*.dfu | head -1)
|
|||||||
|
|
||||||
# Our version for this release.
|
# Our version for this release.
|
||||||
# - caution, the bootrom will not accept version < 3.0.0
|
# - 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)
|
# keep near top, because defined default target (all)
|
||||||
include shared.mk
|
include shared.mk
|
||||||
|
|||||||
@ -16,7 +16,7 @@ BOOTLOADER_DIR = q1-bootloader
|
|||||||
LATEST_RELEASE = $(shell ls -t1 ../releases/*-q1-*.dfu | head -1)
|
LATEST_RELEASE = $(shell ls -t1 ../releases/*-q1-*.dfu | head -1)
|
||||||
|
|
||||||
# Our version for this release.
|
# Our version for this release.
|
||||||
VERSION_STRING = 1.3.0Q
|
VERSION_STRING = 6.3.4QX
|
||||||
|
|
||||||
# Remove this closer to shipping.
|
# Remove this closer to shipping.
|
||||||
#$(warning "Forcing debug build")
|
#$(warning "Forcing debug build")
|
||||||
|
|||||||
@ -239,7 +239,6 @@ def bitcoind_d_sim_watch(bitcoind):
|
|||||||
descriptors = [
|
descriptors = [
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943 segwit v0",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/0/*)#erexmnep",
|
"desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/0/*)#erexmnep",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -252,7 +251,6 @@ def bitcoind_d_sim_watch(bitcoind):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943 segwit v1",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/0/*)#6ghw47ge",
|
"desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/0/*)#6ghw47ge",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -265,7 +263,6 @@ def bitcoind_d_sim_watch(bitcoind):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943 p2pkh",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/0/*)#fxwk08tc",
|
"desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/0/*)#fxwk08tc",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -278,7 +275,6 @@ def bitcoind_d_sim_watch(bitcoind):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943 p2sh-p2wpkh",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/0/*))#weah3vek",
|
"desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/0/*))#weah3vek",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -324,7 +320,6 @@ def bitcoind_d_sim_sign(bitcoind):
|
|||||||
descriptors = [
|
descriptors = [
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/0/*)#mzg0pna0",
|
"desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/0/*)#mzg0pna0",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -337,7 +332,6 @@ def bitcoind_d_sim_sign(bitcoind):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943 segwit v1",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/0/*)#x7dfk9mw",
|
"desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/0/*)#x7dfk9mw",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -350,7 +344,6 @@ def bitcoind_d_sim_sign(bitcoind):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/0/*)#kjnlnm3v",
|
"desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/0/*)#kjnlnm3v",
|
||||||
"internal": False
|
"internal": False
|
||||||
@ -363,7 +356,6 @@ def bitcoind_d_sim_sign(bitcoind):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "now",
|
"timestamp": "now",
|
||||||
"label": "Coldcard 0f056943",
|
|
||||||
"active": True,
|
"active": True,
|
||||||
"desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/0/*))#0qf5gv2y",
|
"desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/0/*))#0qf5gv2y",
|
||||||
"internal": False
|
"internal": False
|
||||||
|
|||||||
@ -6,8 +6,9 @@ from io import BytesIO
|
|||||||
try:
|
try:
|
||||||
from pysecp256k1 import (
|
from pysecp256k1 import (
|
||||||
ec_seckey_verify, ec_pubkey_create, ec_pubkey_serialize, ec_pubkey_parse,
|
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:
|
except ImportError:
|
||||||
import ecdsa
|
import ecdsa
|
||||||
SECP256k1 = ecdsa.curves.SECP256k1
|
SECP256k1 = ecdsa.curves.SECP256k1
|
||||||
@ -119,6 +120,10 @@ class PrivateKey(object):
|
|||||||
tweaked = ec_seckey_tweak_add(self.k, tweak32)
|
tweaked = ec_seckey_tweak_add(self.k, tweak32)
|
||||||
return PrivateKey(sec_exp=tweaked)
|
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
|
@classmethod
|
||||||
def from_wif(cls, wif_str: str) -> "PrivateKey":
|
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")
|
return self.K.to_string(encoding="compressed" if compressed else "uncompressed")
|
||||||
|
|
||||||
def tweak_add(self, tweak32: bytes) -> "PublicKey":
|
def tweak_add(self, tweak32: bytes) -> "PublicKey":
|
||||||
|
assert len(tweak32) == 32
|
||||||
return PublicKey(pub_key=ec_pubkey_tweak_add(self.K, tweak32))
|
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
|
@classmethod
|
||||||
def parse(cls, key_bytes: bytes) -> "PublicKey":
|
def parse(cls, key_bytes: bytes) -> "PublicKey":
|
||||||
"""
|
"""
|
||||||
@ -227,7 +241,7 @@ class PublicKey(object):
|
|||||||
"""
|
"""
|
||||||
return hash160(self.sec(compressed=compressed))
|
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:
|
addr_fmt: str = "p2wpkh") -> str:
|
||||||
"""
|
"""
|
||||||
Generates bitcoin address from public key.
|
Generates bitcoin address from public key.
|
||||||
@ -240,18 +254,33 @@ class PublicKey(object):
|
|||||||
3. p2wpkh (default)
|
3. p2wpkh (default)
|
||||||
:return: bitcoin address
|
: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)
|
h160 = self.h160(compressed=compressed)
|
||||||
if addr_fmt == "p2pkh":
|
if addr_fmt == "p2pkh":
|
||||||
prefix = b"\x6f" if testnet else b"\x00"
|
return encode_base58_checksum(pkh_prefix + h160)
|
||||||
return encode_base58_checksum(prefix + h160)
|
|
||||||
elif addr_fmt == "p2wpkh":
|
elif addr_fmt == "p2wpkh":
|
||||||
hrp = "tb" if testnet else "bc"
|
|
||||||
return bech32.encode(hrp=hrp, witver=0, witprog=h160)
|
return bech32.encode(hrp=hrp, witver=0, witprog=h160)
|
||||||
elif addr_fmt == "p2sh-p2wpkh":
|
elif addr_fmt == "p2sh-p2wpkh":
|
||||||
scr = b"\x00\x14" + h160 # witversion 0 + pubkey hash
|
scr = b"\x00\x14" + h160 # witversion 0 + pubkey hash
|
||||||
h160 = hash160(scr)
|
h160 = hash160(scr)
|
||||||
prefix = b"\xc4" if testnet else b"\x05"
|
return encode_base58_checksum(sh_prefix + h160)
|
||||||
return encode_base58_checksum(prefix + h160)
|
|
||||||
|
|
||||||
raise ValueError("Unsupported address type.")
|
raise ValueError("Unsupported address type.")
|
||||||
|
|
||||||
@ -708,6 +737,12 @@ class BIP32Node:
|
|||||||
ek = PubKeyNode.parse(extended_key, testnet)
|
ek = PubKeyNode.parse(extended_key, testnet)
|
||||||
return cls(ek, netcode="XTN" if testnet else "BTC")
|
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):
|
def subkey_for_path(self, path):
|
||||||
path_list = str_to_path(path)
|
path_list = str_to_path(path)
|
||||||
node = self.node
|
node = self.node
|
||||||
@ -730,9 +765,9 @@ class BIP32Node:
|
|||||||
def hash160(self, compressed=True):
|
def hash160(self, compressed=True):
|
||||||
return self.node.public_key.h160(compressed)
|
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,
|
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):
|
def sec(self, compressed=True):
|
||||||
return self.node.public_key.sec(compressed)
|
return self.node.public_key.sec(compressed)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
# (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 subprocess import check_output
|
||||||
from ckcc.protocol import CCProtocolPacker
|
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 base58 import decode_base58_checksum
|
||||||
from bip32 import BIP32Node
|
from bip32 import BIP32Node
|
||||||
from msg import verify_message
|
from msg import verify_message
|
||||||
@ -293,26 +293,30 @@ def addr_vs_path(master_xpub):
|
|||||||
from bip32 import BIP32Node
|
from bip32 import BIP32Node
|
||||||
from ckcc_protocol.constants import AF_CLASSIC, AFC_PUBKEY, AF_P2WPKH, AFC_SCRIPT
|
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 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
|
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:
|
if not script:
|
||||||
try:
|
try:
|
||||||
# prefer using xpub if we can
|
# prefer using xpub if we can
|
||||||
mk = BIP32Node.from_wallet_key(master_xpub)
|
mk = BIP32Node.from_wallet_key(master_xpub)
|
||||||
if not testnet:
|
mk._netcode = chain
|
||||||
mk._netcode = "BTC"
|
sk = mk.subkey_for_path(path)
|
||||||
sk = mk.subkey_for_path(path[2:])
|
|
||||||
except:
|
except:
|
||||||
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
|
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
|
||||||
if not testnet:
|
mk._netcode = chain
|
||||||
mk._netcode = "BTC"
|
sk = mk.subkey_for_path(path)
|
||||||
sk = mk.subkey_for_path(path[2:])
|
|
||||||
|
|
||||||
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
|
# easy
|
||||||
assert sk.address(netcode="XTN" if testnet else "BTC") == given_addr
|
assert sk.address(chain=chain) == given_addr
|
||||||
|
|
||||||
elif addr_fmt & AFC_PUBKEY:
|
elif addr_fmt & AFC_PUBKEY:
|
||||||
|
|
||||||
@ -360,7 +364,6 @@ def addr_vs_path(master_xpub):
|
|||||||
return doit
|
return doit
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def capture_enabled(sim_eval):
|
def capture_enabled(sim_eval):
|
||||||
# need to have sim_display imported early, see unix/frozen-modules/ckcc
|
# need to have sim_display imported early, see unix/frozen-modules/ckcc
|
||||||
@ -622,6 +625,12 @@ def get_secrets(sim_execfile):
|
|||||||
|
|
||||||
return doit
|
return doit
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clear_miniscript(unit_test):
|
||||||
|
def doit():
|
||||||
|
unit_test('devtest/wipe_miniscript.py')
|
||||||
|
return doit
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def press_select(dev, has_qwerty):
|
def press_select(dev, has_qwerty):
|
||||||
f = functools.partial(_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):
|
def nfc_write(request, needs_nfc, is_q1):
|
||||||
# WRITE data into NFC "chip"
|
# WRITE data into NFC "chip"
|
||||||
def doit_usb(ccfile):
|
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')
|
sim_exec = request.getfixturevalue('sim_exec')
|
||||||
press_select = request.getfixturevalue('press_select')
|
press_select = request.getfixturevalue('press_select')
|
||||||
rv = sim_exec('list(glob.NFC.big_write(%r))' % ccfile)
|
rv = sim_exec('list(glob.NFC.big_write(%r))' % ccfile)
|
||||||
@ -1689,7 +1701,7 @@ def load_shared_mod():
|
|||||||
return doit
|
return doit
|
||||||
|
|
||||||
@pytest.fixture
|
@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):
|
def doit(fnames, sig_fname, way, addr_fmt=None):
|
||||||
fpaths = []
|
fpaths = []
|
||||||
for fname in fnames:
|
for fname in fnames:
|
||||||
@ -1698,6 +1710,7 @@ def verify_detached_signature_file(microsd_path, virtdisk_path):
|
|||||||
else:
|
else:
|
||||||
path = virtdisk_path(fname)
|
path = virtdisk_path(fname)
|
||||||
fpaths.append(path)
|
fpaths.append(path)
|
||||||
|
garbage_collector.append(path)
|
||||||
|
|
||||||
if way == "sd":
|
if way == "sd":
|
||||||
sig_path = microsd_path(sig_fname)
|
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 (hashlib.sha256(contents).digest().hex() + fn_addendum) in msg
|
||||||
|
|
||||||
assert verify_message(address, sig, msg) is True
|
assert verify_message(address, sig, msg) is True
|
||||||
try:
|
garbage_collector.append(sig_path)
|
||||||
os.unlink(sig_path)
|
|
||||||
except: pass
|
|
||||||
return fcontents[0], address
|
return fcontents[0], address
|
||||||
|
|
||||||
return doit
|
return doit
|
||||||
@ -1774,10 +1785,10 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json,
|
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,
|
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,
|
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,
|
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
|
s_label = None
|
||||||
if label == "Address summary":
|
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"),
|
"nfc": nfc_key or (KEY_NFC if is_q1 else "3"),
|
||||||
"qr": qr_key or (KEY_QR if is_q1 else "4"),
|
"qr": qr_key or (KEY_QR if is_q1 else "4"),
|
||||||
}
|
}
|
||||||
time.sleep(0.2)
|
if not skip_query:
|
||||||
title, story = cap_story()
|
time.sleep(0.2)
|
||||||
if way == "sd":
|
title, story = cap_story()
|
||||||
if f"({key_map['sd']}) to save {s_label if s_label else label} file to SD Card" in story:
|
if way == "sd":
|
||||||
need_keypress(key_map['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":
|
elif way == "nfc":
|
||||||
if f"{key_map['nfc'] if is_q1 else '(3)'} to share via NFC" not in story:
|
if f"{key_map['nfc'] if is_q1 else '(3)'} to share via NFC" not in story:
|
||||||
pytest.skip("NFC disabled")
|
pytest.skip("NFC disabled")
|
||||||
else:
|
|
||||||
need_keypress(key_map['nfc'])
|
|
||||||
time.sleep(0.2)
|
|
||||||
if is_json:
|
|
||||||
nfc_export = nfc_read_json()
|
|
||||||
else:
|
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)
|
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:
|
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:
|
except:
|
||||||
return res
|
raise
|
||||||
else:
|
res = cap_screen_qr().decode('ascii')
|
||||||
# virtual disk
|
try:
|
||||||
if f"({key_map['vdisk']}) to save to Virtual Disk" not in story:
|
return json.loads(res)
|
||||||
pytest.skip("Vdisk disabled")
|
except:
|
||||||
|
return res
|
||||||
else:
|
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)
|
time.sleep(0.2)
|
||||||
title, story = cap_story()
|
title, story = cap_story()
|
||||||
@ -1865,6 +1877,8 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
|
|||||||
if is_json:
|
if is_json:
|
||||||
export = json.loads(export)
|
export = json.loads(export)
|
||||||
|
|
||||||
|
garbage_collector.append(path)
|
||||||
|
|
||||||
press_select()
|
press_select()
|
||||||
|
|
||||||
if ret_sig_addr and sig_addr:
|
if ret_sig_addr and sig_addr:
|
||||||
@ -1909,7 +1923,7 @@ def tapsigner_encrypted_backup(microsd_path, virtdisk_path):
|
|||||||
return doit
|
return doit
|
||||||
|
|
||||||
@pytest.fixture
|
@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
|
# for use in seed XOR menu system
|
||||||
def doit(num_words):
|
def doit(num_words):
|
||||||
if num_words == 12:
|
if num_words == 12:
|
||||||
@ -1917,7 +1931,7 @@ def choose_by_word_length(need_keypress):
|
|||||||
elif num_words == 18:
|
elif num_words == 18:
|
||||||
need_keypress("2")
|
need_keypress("2")
|
||||||
else:
|
else:
|
||||||
need_keypress("y")
|
press_select()
|
||||||
return doit
|
return doit
|
||||||
|
|
||||||
# workaround: need these fixtures to be global so I can call test from a test
|
# 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"):
|
elif af in ("p2wpkh", "p2wsh"):
|
||||||
target = "bc1q" if chain == "BTC" else "tb1q"
|
target = "bc1q" if chain == "BTC" else "tb1q"
|
||||||
assert addr.startswith(target)
|
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"):
|
elif af in ("p2sh", "p2wpkh-p2sh", "p2wsh-p2sh"):
|
||||||
target = "3" if chain == "BTC" else "2"
|
target = "3" if chain == "BTC" else "2"
|
||||||
assert addr.startswith(target)
|
assert addr.startswith(target)
|
||||||
@ -2194,6 +2211,30 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
|
|||||||
|
|
||||||
return doit
|
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
|
@pytest.fixture
|
||||||
def skip_if_useless_way(is_q1, nfc_disabled):
|
def skip_if_useless_way(is_q1, nfc_disabled):
|
||||||
@ -2220,7 +2261,8 @@ def dev_core_import_object(dev):
|
|||||||
ders = [
|
ders = [
|
||||||
("m/44h/1h/0h", AF_CLASSIC),
|
("m/44h/1h/0h", AF_CLASSIC),
|
||||||
("m/49h/1h/0h", AF_P2WPKH_P2SH),
|
("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 = []
|
descriptors = []
|
||||||
for idx, (path, addr_format) in enumerate(ders):
|
for idx, (path, addr_format) in enumerate(ders):
|
||||||
@ -2239,6 +2281,15 @@ def dev_core_import_object(dev):
|
|||||||
return descriptors
|
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
|
# useful fixtures
|
||||||
from test_backup import backup_system
|
from test_backup import backup_system
|
||||||
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll
|
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 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_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_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_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_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
|
||||||
from test_seed_xor import restore_seed_xor
|
from test_seed_xor import restore_seed_xor
|
||||||
|
|||||||
@ -25,9 +25,11 @@ unmap_addr_fmt = {
|
|||||||
'p2wsh': AF_P2WSH,
|
'p2wsh': AF_P2WSH,
|
||||||
'p2wsh-p2sh': AF_P2WSH_P2SH,
|
'p2wsh-p2sh': AF_P2WSH_P2SH,
|
||||||
'p2sh-p2wsh': AF_P2WSH_P2SH,
|
'p2sh-p2wsh': AF_P2WSH_P2SH,
|
||||||
|
"p2tr": AF_P2TR,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg_sign_unmap_addr_fmt = {
|
msg_sign_unmap_addr_fmt = {
|
||||||
|
'p2tr': AF_P2TR, # not supported for msg signign tho
|
||||||
'p2pkh': AF_CLASSIC,
|
'p2pkh': AF_CLASSIC,
|
||||||
'p2wpkh': AF_P2WPKH,
|
'p2wpkh': AF_P2WPKH,
|
||||||
'p2sh-p2wpkh': AF_P2WPKH_P2SH,
|
'p2sh-p2wpkh': AF_P2WPKH_P2SH,
|
||||||
@ -35,6 +37,7 @@ msg_sign_unmap_addr_fmt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addr_fmt_names = {
|
addr_fmt_names = {
|
||||||
|
AF_P2TR: 'p2tr',
|
||||||
AF_CLASSIC: 'p2pkh',
|
AF_CLASSIC: 'p2pkh',
|
||||||
AF_P2SH: 'p2sh',
|
AF_P2SH: 'p2sh',
|
||||||
AF_P2WPKH: 'p2wpkh',
|
AF_P2WPKH: 'p2wpkh',
|
||||||
@ -45,10 +48,10 @@ addr_fmt_names = {
|
|||||||
|
|
||||||
|
|
||||||
# all possible addr types, including multisig/scripts
|
# 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
|
# single-signer
|
||||||
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh']
|
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh', 'p2tr']
|
||||||
|
|
||||||
# multi signer
|
# multi signer
|
||||||
ADDR_STYLES_MS = ['p2sh', 'p2wsh', 'p2wsh-p2sh']
|
ADDR_STYLES_MS = ['p2sh', 'p2wsh', 'p2wsh-p2sh']
|
||||||
|
|||||||
BIN
testing/data/taproot/in_internal_key_len.psbt
Normal file
BIN
testing/data/taproot/in_internal_key_len.psbt
Normal file
Binary file not shown.
1
testing/data/taproot/in_key_pth_sig_len.psbt
Normal file
1
testing/data/taproot/in_key_pth_sig_len.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a075701133f173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc3475000000
|
||||||
1
testing/data/taproot/in_key_pth_sig_len1.psbt
Normal file
1
testing/data/taproot/in_key_pth_sig_len1.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757011342173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc34751701aa000000
|
||||||
1
testing/data/taproot/in_leaf_script_cb_len.psbt
Normal file
1
testing/data/taproot/in_leaf_script_cb_len.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926315c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e1f80023202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000
|
||||||
1
testing/data/taproot/in_leaf_script_cb_len1.psbt
Normal file
1
testing/data/taproot/in_leaf_script_cb_len1.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926115c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e123202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000
|
||||||
1
testing/data/taproot/in_script_sig_key_len.psbt
Normal file
1
testing/data/taproot/in_script_sig_key_len.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6924214022cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094089756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb0000
|
||||||
1
testing/data/taproot/in_script_sig_sig_len.psbt
Normal file
1
testing/data/taproot/in_script_sig_sig_len.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094289756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb01010000
|
||||||
1
testing/data/taproot/in_script_sig_sig_len1.psbt
Normal file
1
testing/data/taproot/in_script_sig_sig_len1.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b093f89756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd430000
|
||||||
1
testing/data/taproot/in_tr_deriv_key_len.psbt
Normal file
1
testing/data/taproot/in_tr_deriv_key_len.psbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757221602fe349064c98d6e2a853fa3c9b12bd8b304a19c195c60efa7ee2393046d3fa2321900772b2da75600008001000080000000800100000000000000000000
|
||||||
481
testing/descriptor.py
Normal file
481
testing/descriptor.py
Normal 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
|
||||||
@ -23,6 +23,7 @@ if not pa.is_secret_blank():
|
|||||||
pa.login()
|
pa.login()
|
||||||
|
|
||||||
assert pa.is_secret_blank()
|
assert pa.is_secret_blank()
|
||||||
|
settings.blank()
|
||||||
|
|
||||||
SettingsObject.master_sv_data = {}
|
SettingsObject.master_sv_data = {}
|
||||||
SettingsObject.master_nvram_key = None
|
SettingsObject.master_nvram_key = None
|
||||||
|
|||||||
13
testing/devtest/wipe_miniscript.py
Normal file
13
testing/devtest/wipe_miniscript.py
Normal 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()
|
||||||
@ -24,13 +24,15 @@ def prandom(count):
|
|||||||
return bytes(random.randint(0, 255) for i in range(count))
|
return bytes(random.randint(0, 255) for i in range(count))
|
||||||
|
|
||||||
def taptweak(internal_key, tweak=None):
|
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)"
|
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)
|
xonly_pubkey = xonly_pubkey_parse(internal_key)
|
||||||
tweak = tagged_sha256(b"TapTweak", tweak)
|
tweak = tagged_sha256(b"TapTweak", tweak)
|
||||||
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_pubkey, tweak)
|
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_pubkey, tweak)
|
||||||
tweaked_xonnly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
|
tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
|
||||||
return xonly_pubkey_serialize(tweaked_xonnly_pubkey)
|
return xonly_pubkey_serialize(tweaked_xonly_pubkey)
|
||||||
|
|
||||||
def fake_dest_addr(style='p2pkh'):
|
def fake_dest_addr(style='p2pkh'):
|
||||||
# Make a plausible output address, but it's random garbage. Cant use for change outs
|
# Make a plausible output address, but it's random garbage. Cant use for change outs
|
||||||
|
|||||||
@ -123,6 +123,9 @@ class BasicPSBTInput(PSBTSection):
|
|||||||
self.taproot_bip32_paths = {}
|
self.taproot_bip32_paths = {}
|
||||||
self.taproot_internal_key = None
|
self.taproot_internal_key = None
|
||||||
self.taproot_key_sig = None
|
self.taproot_key_sig = None
|
||||||
|
self.taproot_merkle_root = None
|
||||||
|
self.taproot_scripts = {}
|
||||||
|
self.taproot_script_sigs = {}
|
||||||
self.redeem_script = None
|
self.redeem_script = None
|
||||||
self.witness_script = None
|
self.witness_script = None
|
||||||
self.previous_txid = None # v2
|
self.previous_txid = None # v2
|
||||||
@ -147,6 +150,9 @@ class BasicPSBTInput(PSBTSection):
|
|||||||
a.taproot_key_sig == b.taproot_key_sig and \
|
a.taproot_key_sig == b.taproot_key_sig and \
|
||||||
a.taproot_bip32_paths == b.taproot_bip32_paths and \
|
a.taproot_bip32_paths == b.taproot_bip32_paths and \
|
||||||
a.taproot_internal_key == b.taproot_internal_key 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 \
|
sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys()) and \
|
||||||
a.previous_txid == b.previous_txid and \
|
a.previous_txid == b.previous_txid and \
|
||||||
a.prevout_idx == b.prevout_idx and \
|
a.prevout_idx == b.prevout_idx and \
|
||||||
@ -189,7 +195,7 @@ class BasicPSBTInput(PSBTSection):
|
|||||||
self.others[kt] = val
|
self.others[kt] = val
|
||||||
elif kt == PSBT_IN_TAP_BIP32_DERIVATION:
|
elif kt == PSBT_IN_TAP_BIP32_DERIVATION:
|
||||||
self.taproot_bip32_paths[key] = val
|
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
|
self.taproot_internal_key = val
|
||||||
elif kt == PSBT_IN_TAP_KEY_SIG:
|
elif kt == PSBT_IN_TAP_KEY_SIG:
|
||||||
self.taproot_key_sig = val
|
self.taproot_key_sig = val
|
||||||
@ -203,6 +209,21 @@ class BasicPSBTInput(PSBTSection):
|
|||||||
self.req_time_locktime = struct.unpack("<I", val)[0]
|
self.req_time_locktime = struct.unpack("<I", val)[0]
|
||||||
elif kt == PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
|
elif kt == PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
|
||||||
self.req_height_locktime = struct.unpack("<I", val)[0]
|
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:
|
else:
|
||||||
self.unknown[bytes([kt]) + key] = val
|
self.unknown[bytes([kt]) + key] = val
|
||||||
|
|
||||||
@ -236,6 +257,16 @@ class BasicPSBTInput(PSBTSection):
|
|||||||
if self.taproot_key_sig:
|
if self.taproot_key_sig:
|
||||||
wr(PSBT_IN_TAP_KEY_SIG, 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 v2:
|
||||||
if self.previous_txid is not None:
|
if self.previous_txid is not None:
|
||||||
wr(PSBT_IN_PREVIOUS_TXID, self.previous_txid)
|
wr(PSBT_IN_PREVIOUS_TXID, self.previous_txid)
|
||||||
@ -267,6 +298,7 @@ class BasicPSBTOutput(PSBTSection):
|
|||||||
self.bip32_paths = {}
|
self.bip32_paths = {}
|
||||||
self.taproot_bip32_paths = {}
|
self.taproot_bip32_paths = {}
|
||||||
self.taproot_internal_key = None
|
self.taproot_internal_key = None
|
||||||
|
self.taproot_tree = None
|
||||||
self.script = None # v2
|
self.script = None # v2
|
||||||
self.amount = None # v2
|
self.amount = None # v2
|
||||||
self.proprietary = {}
|
self.proprietary = {}
|
||||||
@ -282,6 +314,7 @@ class BasicPSBTOutput(PSBTSection):
|
|||||||
a.taproot_bip32_paths == b.taproot_bip32_paths and \
|
a.taproot_bip32_paths == b.taproot_bip32_paths and \
|
||||||
a.taproot_internal_key == b.taproot_internal_key and \
|
a.taproot_internal_key == b.taproot_internal_key and \
|
||||||
a.proprietary == b.proprietary and \
|
a.proprietary == b.proprietary and \
|
||||||
|
a.taproot_tree == b.taproot_tree and \
|
||||||
a.unknown == b.unknown
|
a.unknown == b.unknown
|
||||||
|
|
||||||
def parse_kv(self, kt, key, val):
|
def parse_kv(self, kt, key, val):
|
||||||
@ -297,6 +330,18 @@ class BasicPSBTOutput(PSBTSection):
|
|||||||
self.taproot_bip32_paths[key] = val
|
self.taproot_bip32_paths[key] = val
|
||||||
elif kt == PSBT_OUT_TAP_INTERNAL_KEY:
|
elif kt == PSBT_OUT_TAP_INTERNAL_KEY:
|
||||||
self.taproot_internal_key = val
|
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:
|
elif kt == PSBT_OUT_SCRIPT:
|
||||||
self.script = val
|
self.script = val
|
||||||
elif kt == PSBT_OUT_AMOUNT:
|
elif kt == PSBT_OUT_AMOUNT:
|
||||||
@ -319,6 +364,11 @@ class BasicPSBTOutput(PSBTSection):
|
|||||||
wr(PSBT_OUT_TAP_BIP32_DERIVATION, self.taproot_bip32_paths[k], k)
|
wr(PSBT_OUT_TAP_BIP32_DERIVATION, self.taproot_bip32_paths[k], k)
|
||||||
if self.taproot_internal_key:
|
if self.taproot_internal_key:
|
||||||
wr(PSBT_OUT_TAP_INTERNAL_KEY, 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:
|
if v2 and self.script is not None:
|
||||||
wr(PSBT_OUT_SCRIPT, self.script)
|
wr(PSBT_OUT_SCRIPT, self.script)
|
||||||
if v2 and self.amount is not None:
|
if v2 and self.amount is not None:
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from charcodes import KEY_QR
|
|||||||
from constants import msg_sign_unmap_addr_fmt
|
from constants import msg_sign_unmap_addr_fmt
|
||||||
|
|
||||||
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'"])
|
@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):
|
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)
|
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.qrcode
|
||||||
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'", "m/0h/500h"])
|
@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,
|
def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
|
||||||
cap_story, cap_screen_qr, qr_quality_check,
|
cap_story, cap_screen_qr, qr_quality_check,
|
||||||
press_cancel, is_q1):
|
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()
|
assert qr == addr or qr == addr.upper()
|
||||||
|
|
||||||
@pytest.mark.bitcoind
|
@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
|
# check our p2wpkh wrapped in p2sh is right
|
||||||
use_regtest()
|
use_regtest()
|
||||||
|
addr_fmt, addr_fmt_bitcoind = addr_fmt
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
core_addr = bitcoind_d_sim_sign.getnewaddress(f"{i}-addr", "p2sh-segwit")
|
core_addr = bitcoind_d_sim_sign.getnewaddress(f"{i}-addr", addr_fmt_bitcoind)
|
||||||
assert core_addr[0] == '2'
|
|
||||||
resp = bitcoind_d_sim_sign.getaddressinfo(core_addr)
|
resp = bitcoind_d_sim_sign.getaddressinfo(core_addr)
|
||||||
assert resp['embedded']['iswitness'] == True
|
assert resp["ismine"] is True
|
||||||
assert resp['isscript'] == 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']
|
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()
|
press_select()
|
||||||
assert addr == core_addr
|
assert addr == core_addr
|
||||||
|
|
||||||
@pytest.mark.parametrize("body_err", [
|
@pytest.mark.parametrize("body_err", [
|
||||||
("m\np2wsh", "Invalid address format: 'p2wsh'"),
|
("m\np2wsh", "Unsupported address format: 'p2wsh'"),
|
||||||
("m\np2sh-p2wsh", "Invalid address format: 'p2sh-p2wsh'"),
|
("m\np2sh-p2wsh", "Unsupported address format: 'p2sh-p2wsh'"),
|
||||||
("m\np2tr", "Invalid address format: 'p2tr'"),
|
|
||||||
("m/0/0/0/0/0/0/0/0/0/0/0/0/0\np2pkh", "too deep"),
|
("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"),
|
("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
|
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("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,
|
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,
|
goto_home, cap_story, press_nfc, addr_vs_path, press_select, is_q1,
|
||||||
cap_screen):
|
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
|
assert story_addr == addr
|
||||||
addr_vs_path(addr, path, addr_fmt)
|
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
|
||||||
@ -24,9 +24,10 @@ def mk_common_derivations():
|
|||||||
# Removed in v4.1.3: ( "m/{change}/{idx}", AF_CLASSIC ),
|
# Removed in v4.1.3: ( "m/{change}/{idx}", AF_CLASSIC ),
|
||||||
#( "m/{account}'/{change}'/{idx}'", AF_CLASSIC ),
|
#( "m/{account}'/{change}'/{idx}'", AF_CLASSIC ),
|
||||||
#( "m/{account}'/{change}'/{idx}'", AF_P2WPKH ),
|
#( "m/{account}'/{change}'/{idx}'", AF_P2WPKH ),
|
||||||
( "m/44h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_CLASSIC ),
|
("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/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/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
|
return doit
|
||||||
|
|
||||||
@ -57,32 +58,14 @@ def parse_display_screen(cap_story, is_mark3):
|
|||||||
return d
|
return d
|
||||||
return doit
|
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
|
@pytest.fixture
|
||||||
def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, microsd_path,
|
def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, microsd_path,
|
||||||
virtdisk_path, nfc_read_text, load_export_and_verify_signature,
|
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
|
# Generates the address file through the simulator, reads the file and
|
||||||
# returns a list of tuples of the form (subpath, address)
|
# 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
|
expected_qty = 250 if way != "nfc" else 10
|
||||||
if (start_idx + expected_qty) > MAX_BIP32_IDX:
|
if (start_idx + expected_qty) > MAX_BIP32_IDX:
|
||||||
expected_qty = (MAX_BIP32_IDX - start_idx) + 1
|
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:
|
if change and not is_custom_single:
|
||||||
need_keypress("0")
|
need_keypress("0")
|
||||||
if way == "sd":
|
if way == "sd":
|
||||||
need_keypress('1')
|
if "Press (1)" in story:
|
||||||
|
need_keypress('1')
|
||||||
elif way == "vdisk":
|
elif way == "vdisk":
|
||||||
if "save to Virtual Disk" not in story:
|
if "save to Virtual Disk" not in story:
|
||||||
raise pytest.skip("Vdisk disabled")
|
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
|
assert len(addresses.split("\n")) == expected_qty
|
||||||
raise pytest.xfail("PASSED - different export format for NFC")
|
raise pytest.xfail("PASSED - different export format for NFC")
|
||||||
|
|
||||||
time.sleep(.5) # always long enough to write the file?
|
if is_p2tr:
|
||||||
title, body = cap_story()
|
# p2tr - no signature file
|
||||||
contents, sig_addr = load_export_and_verify_signature(body, way, label="Address summary")
|
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)
|
addr_dump = io.StringIO(contents)
|
||||||
cc = csv.reader(addr_dump)
|
cc = csv.reader(addr_dump)
|
||||||
hdr = next(cc)
|
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):
|
for n, (idx, addr, deriv) in enumerate(cc, start=start_idx):
|
||||||
assert int(idx) == n
|
assert int(idx) == n
|
||||||
if n == start_idx:
|
if n == start_idx:
|
||||||
assert sig_addr == addr
|
if sig_addr:
|
||||||
|
assert sig_addr == addr
|
||||||
if not is_custom_single:
|
if not is_custom_single:
|
||||||
assert ('/%s' % idx) in deriv
|
assert ('/%s' % idx) in deriv
|
||||||
|
|
||||||
@ -272,7 +264,7 @@ def test_address_display(goto_address_explorer, parse_display_screen, mk_common_
|
|||||||
|
|
||||||
press_cancel() # back
|
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("change", [True, False])
|
||||||
@pytest.mark.parametrize("start_idx", [MAX_BIP32_IDX, 80965, 0])
|
@pytest.mark.parametrize("start_idx", [MAX_BIP32_IDX, 80965, 0])
|
||||||
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
|
@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)
|
set_addr_exp_start_idx(start_idx)
|
||||||
pick_menu_item(click_idx)
|
pick_menu_item(click_idx)
|
||||||
# Generate the addresses file and get each line in a list
|
# 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
|
# derive the subkey and validate the corresponding address
|
||||||
assert subpath.split("/")[-2] == "1" if change else "0"
|
assert subpath.split("/")[-2] == "1" if change else "0"
|
||||||
sk = node_prv.subkey_for_path(subpath)
|
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
|
# derive index=0 address
|
||||||
assert '{account}' in path
|
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)
|
sk = node_prv.subkey_for_path(subpath)
|
||||||
|
|
||||||
# capture full index=0 address from display screen & validate it
|
# 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.startswith(start)
|
||||||
assert expected_addr.endswith(end)
|
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"
|
assert subpath.split('/')[-3] == str(account_num)+"h"
|
||||||
sk = node_prv.subkey_for_path(subpath)
|
sk = node_prv.subkey_for_path(subpath)
|
||||||
validate_address(addr, sk)
|
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/1/2/3/4/5", MAX_BIP32_IDX),
|
||||||
("m/1h/2h/3h/4h/5h", 0),
|
("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,
|
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,
|
need_keypress, cap_menu, parse_display_screen, validate_address,
|
||||||
cap_screen_qr, qr_quality_check, nfc_read_text, get_setting,
|
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()
|
m = cap_menu()
|
||||||
assert m[0] == 'Classic P2PKH'
|
assert m[0] == 'Classic P2PKH'
|
||||||
assert m[1] == 'Segwit P2WPKH'
|
assert m[1] == 'Segwit P2WPKH'
|
||||||
assert m[2] == 'P2SH-Segwit'
|
assert m[2] == 'Taproot P2TR'
|
||||||
|
assert m[3] == 'P2SH-Segwit'
|
||||||
|
|
||||||
fmts = {
|
fmts = {
|
||||||
AF_CLASSIC: 'Classic P2PKH',
|
AF_CLASSIC: 'Classic P2PKH',
|
||||||
AF_P2WPKH: 'Segwit P2WPKH',
|
AF_P2WPKH: 'Segwit P2WPKH',
|
||||||
AF_P2WPKH_P2SH: 'P2SH-Segwit',
|
AF_P2WPKH_P2SH: 'P2SH-Segwit',
|
||||||
|
AF_P2TR: 'Taproot P2TR',
|
||||||
}
|
}
|
||||||
|
|
||||||
pick_menu_item(fmts[which_fmt])
|
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')
|
need_keypress(KEY_QR if is_q1 else '4')
|
||||||
qr = cap_screen_qr().decode('ascii')
|
qr = cap_screen_qr().decode('ascii')
|
||||||
if which_fmt == AF_P2WPKH:
|
if which_fmt in (AF_P2WPKH, AF_P2TR):
|
||||||
assert qr == addr.upper()
|
assert qr == addr.upper()
|
||||||
else:
|
else:
|
||||||
assert qr == addr
|
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
|
# remove QR from screen
|
||||||
press_cancel()
|
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)
|
f_path, f_addr = next(addr_gen)
|
||||||
assert f_path == path
|
assert f_path == path
|
||||||
assert f_addr == addr
|
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')
|
need_keypress(KEY_QR if is_q1 else '4')
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
qr = cap_screen_qr().decode('ascii')
|
qr = cap_screen_qr().decode('ascii')
|
||||||
if which_fmt == AF_P2WPKH:
|
if which_fmt in (AF_P2WPKH, AF_P2TR):
|
||||||
qr = qr.lower()
|
qr = qr.lower()
|
||||||
qr_addr_list.append(qr)
|
qr_addr_list.append(qr)
|
||||||
need_keypress(KEY_RIGHT if is_q1 else "9")
|
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())
|
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}
|
assert addr_dict == {p: a for i,(p, a) in enumerate(addr_gen) if i < n}
|
||||||
|
|
||||||
# check the rest of file export
|
# check the rest of file export
|
||||||
for p, a in addr_gen:
|
for p, a in addr_gen:
|
||||||
addr_vs_path(a, p, addr_fmt=which_fmt)
|
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
|
# EOF
|
||||||
|
|||||||
1667
testing/test_bsms.py
Normal file
1667
testing/test_bsms.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,20 +14,24 @@ wordlist = Mnemonic('english').wordlist
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def try_decode(sim_exec):
|
def try_decode(sim_exec):
|
||||||
def doit(arg):
|
def doit(arg, ):
|
||||||
cmd = "from decoders import decode_qr_result; " + \
|
cmd = "from decoders import decode_qr_result; " + \
|
||||||
f"RV.write(repr(decode_qr_result({arg!r})))"
|
f"RV.write(repr(decode_qr_result({arg!r})))"
|
||||||
|
|
||||||
result = sim_exec(cmd)
|
result = sim_exec(cmd)
|
||||||
|
|
||||||
if 'Traceback' in result:
|
if 'Traceback' in result:
|
||||||
raise RuntimeError(result)
|
raise RuntimeError(result)
|
||||||
|
|
||||||
if '<' in result:
|
try:
|
||||||
# objects, like "<HexStreamer..."
|
return eval(result)
|
||||||
result = result.replace('<', "'").replace('>', "'")
|
except SyntaxError:
|
||||||
|
if '<' in result:
|
||||||
|
# objects, like "<HexStreamer..."
|
||||||
|
result = result.replace('<', "'").replace('>', "'")
|
||||||
|
return eval(result)
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
return eval(result)
|
|
||||||
return doit
|
return doit
|
||||||
|
|
||||||
@pytest.mark.parametrize('fname,expect', [
|
@pytest.mark.parametrize('fname,expect', [
|
||||||
@ -145,7 +149,6 @@ def test_urldecode(url, sim_exec):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', [
|
@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: tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n6ba6cfd0: tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
|
||||||
'0f056943: xpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n6ba6cfd0: tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
|
'0f056943: xpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n6ba6cfd0: tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
|
||||||
' 0F056943 : tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n 6BA6CFD0 : tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
|
' 0F056943 : tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP\n 6BA6CFD0 : tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm',
|
||||||
@ -163,6 +166,18 @@ def test_multisig(config, try_decode):
|
|||||||
assert ft == "multi"
|
assert ft == "multi"
|
||||||
assert vals[0] == config
|
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', [
|
@pytest.mark.parametrize('data', [
|
||||||
('5J9Gfy2FNTw2EpkkQu41S9CTBBVij123kYPkbYAnaQkUHtMuv2Q', False, False),
|
('5J9Gfy2FNTw2EpkkQu41S9CTBBVij123kYPkbYAnaQkUHtMuv2Q', False, False),
|
||||||
('L2TgtddYM9ueK2auJVkNaNEF3egMMK1MTMkng5RBAcBWXnCMnxcb', True, False),
|
('L2TgtddYM9ueK2auJVkNaNEF3egMMK1MTMkng5RBAcBWXnCMnxcb', True, False),
|
||||||
|
|||||||
@ -256,7 +256,7 @@ def confirm_tmp_seed(need_keypress, cap_story, press_select):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def seed_vault_delete(pick_menu_item, need_keypress, cap_menu, cap_story,
|
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):
|
def doit(xfp, wipe=True):
|
||||||
# delete it from records
|
# delete it from records
|
||||||
goto_home()
|
goto_home()
|
||||||
@ -276,12 +276,17 @@ def seed_vault_delete(pick_menu_item, need_keypress, cap_menu, cap_story,
|
|||||||
title, story = cap_story()
|
title, story = cap_story()
|
||||||
assert "Remove" in story
|
assert "Remove" in story
|
||||||
assert xfp in title
|
assert xfp in title
|
||||||
assert "press (1)" in story
|
|
||||||
if wipe:
|
if wipe:
|
||||||
press_select()
|
press_select()
|
||||||
else:
|
else:
|
||||||
# preserve settings - remove just from seed vaul
|
if xfp2str(settings_get("xfp")) == xfp:
|
||||||
need_keypress("1")
|
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)
|
time.sleep(.1)
|
||||||
goto_home()
|
goto_home()
|
||||||
@ -1117,16 +1122,21 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item
|
|||||||
m = cap_menu()
|
m = cap_menu()
|
||||||
assert m[0] == "AAA"
|
assert m[0] == "AAA"
|
||||||
pick_menu_item("Delete")
|
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()
|
press_select()
|
||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
|
goto_home()
|
||||||
m = cap_menu()
|
m = cap_menu()
|
||||||
# after we delete from seed vault together with its settings
|
# still in tmp mode
|
||||||
# we're back to master secret
|
assert m[0] != "Ready To Sign"
|
||||||
assert m[0] == "Ready To Sign"
|
|
||||||
pick_menu_item("Seed Vault")
|
pick_menu_item("Seed Vault")
|
||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
m = cap_menu()
|
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_down()
|
||||||
press_select()
|
press_select()
|
||||||
@ -1146,7 +1156,10 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item
|
|||||||
assert "Delete" in m
|
assert "Delete" in m
|
||||||
|
|
||||||
pick_menu_item("Delete")
|
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)
|
time.sleep(.1)
|
||||||
m = cap_menu()
|
m = cap_menu()
|
||||||
assert len(m) == 3
|
assert len(m) == 3
|
||||||
@ -1164,6 +1177,25 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item
|
|||||||
# still in ephemeral
|
# still in ephemeral
|
||||||
assert title == m[0]
|
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,
|
def test_xfp_collision(reset_seed_words, settings_set, import_ephemeral_xprv,
|
||||||
cap_story, press_cancel, pick_menu_item, cap_menu,
|
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()
|
m = cap_menu()
|
||||||
assert m[0] == "Ready To Sign"
|
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
|
# EOF
|
||||||
|
|||||||
@ -4,12 +4,10 @@
|
|||||||
#
|
#
|
||||||
# Start simulator with: simulator.py --eff --set nfc=1
|
# 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
|
import pytest, time, os, json, io, bech32
|
||||||
from bip32 import BIP32Node
|
from bip32 import BIP32Node
|
||||||
|
from descriptor import Descriptor
|
||||||
|
from mnemonic import Mnemonic
|
||||||
from ckcc_protocol.constants import *
|
from ckcc_protocol.constants import *
|
||||||
from helpers import xfp2str, slip132undo
|
from helpers import xfp2str, slip132undo
|
||||||
from conftest import simulator_fixed_xfp, simulator_fixed_tprv, simulator_fixed_words, simulator_fixed_xprv
|
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 = []
|
addrs = []
|
||||||
imm_js = None
|
imm_js = None
|
||||||
imd_js = None
|
imd_js = None
|
||||||
|
imd_js_tr = None
|
||||||
|
tr = False
|
||||||
for ln in fp:
|
for ln in fp:
|
||||||
|
if ln.startswith("p2tr:"):
|
||||||
|
tr = True
|
||||||
|
|
||||||
if 'importmulti' in ln:
|
if 'importmulti' in ln:
|
||||||
# PLAN: this will become obsolete
|
# PLAN: this will become obsolete
|
||||||
assert ln.startswith("importmulti '")
|
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"
|
assert not imm_js, "dup importmulti lines"
|
||||||
imm_js = ln[13:-2]
|
imm_js = ln[13:-2]
|
||||||
elif "importdescriptors '" in ln:
|
elif "importdescriptors '" in ln:
|
||||||
|
ln = ln.strip()
|
||||||
assert ln.startswith("importdescriptors '")
|
assert ln.startswith("importdescriptors '")
|
||||||
assert ln.endswith("'\n")
|
if tr:
|
||||||
assert not imd_js, "dup importdesc lines"
|
imd_js_tr = ln[19:-1]
|
||||||
imd_js = ln[19:-2]
|
tr = False
|
||||||
|
else:
|
||||||
|
imd_js = ln[19:-1]
|
||||||
elif '=>' in ln:
|
elif '=>' in ln:
|
||||||
path, addr = ln.strip().split(' => ', 1)
|
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)
|
sk = BIP32Node.from_wallet_key(simulator_fixed_tprv).subkey_for_path(path)
|
||||||
h20 = sk.hash160()
|
if path.startswith(f"m/86h/1h/{acct_num}h/0"):
|
||||||
assert addr == bech32.encode(addr[0:4], 0, h20) # TODO here we should differentiate if testnet or smthg
|
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)
|
addrs.append(addr)
|
||||||
|
|
||||||
assert len(addrs) == 3
|
assert len(addrs) == 6
|
||||||
|
|
||||||
xfp = xfp2str(simulator_fixed_xfp).lower()
|
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])
|
x = bitcoind_wallet.getaddressinfo(addrs[-1])
|
||||||
pprint(x)
|
pprint(x)
|
||||||
assert x['address'] == addrs[-1]
|
assert x['address'] == addrs[-1]
|
||||||
if 'label' in x:
|
# assert x['iswatchonly'] == True
|
||||||
# pre 0.21.?
|
assert x['iswitness'] is True
|
||||||
assert x['label'] == 'testcase'
|
# assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
|
||||||
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)
|
|
||||||
|
|
||||||
# importdescriptors -- its better
|
# importdescriptors -- its better
|
||||||
assert imd_js
|
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 in desc
|
||||||
assert expect+f'/{n}/*' 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
|
# test against bitcoind -- needs a "descriptor native" wallet
|
||||||
res = bitcoind_d_wallet.importdescriptors(obj)
|
res = bitcoind_d_wallet.importdescriptors(obj)
|
||||||
assert res[0]["success"]
|
assert res[0]["success"]
|
||||||
assert res[1]["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])
|
x = bitcoind_d_wallet.getaddressinfo(addrs[-1])
|
||||||
pprint(x)
|
pprint(x)
|
||||||
assert x['address'] == addrs[-1]
|
assert x['address'] == addrs[-1]
|
||||||
assert x['iswatchonly'] == False
|
assert x['iswatchonly'] is False
|
||||||
assert x['iswitness'] == True
|
assert x['iswitness'] is True
|
||||||
# assert x['ismine'] == True # TODO we have imported pubkeys - it has no idea if it is ours or solvable
|
assert x['solvable'] is True
|
||||||
# assert x['solvable'] == True
|
assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
|
||||||
# assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
|
assert x['hdkeypath'].replace("'", "h") == f"m/86h/1h/{acct_num}h/0/%d" % 2
|
||||||
#assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
|
@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('acct_num', [ None, '99', '1236'])
|
||||||
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
|
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
|
||||||
@pytest.mark.parametrize('testnet', [True, False])
|
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
|
||||||
@pytest.mark.parametrize('app', [
|
@pytest.mark.parametrize('app', [
|
||||||
# no need to run them all - just name check differs
|
# no need to run them all - just name check differs
|
||||||
("Generic JSON", "Generic Export"),
|
("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,
|
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,
|
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, expect_acctnum_captured):
|
||||||
|
|
||||||
skip_if_useless_way(way)
|
skip_if_useless_way(way)
|
||||||
|
|
||||||
if not testnet:
|
if chain == "BTC":
|
||||||
use_mainnet()
|
use_mainnet()
|
||||||
|
|
||||||
export_mi, app_f_name = app
|
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)
|
addr = v.get('first', None)
|
||||||
|
|
||||||
if fn == 'bip44':
|
if fn == 'bip44':
|
||||||
assert first.address(netcode="XTN" if testnet else "BTC") == v['first']
|
assert first.address(chain=chain) == v['first']
|
||||||
addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, testnet=testnet)
|
addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, chain=chain)
|
||||||
elif ('bip48_' in fn) or (fn == 'bip45'):
|
elif ('bip48_' in fn) or (fn == 'bip45'):
|
||||||
# multisig: cant do addrs
|
# multisig: cant do addrs
|
||||||
assert addr == None
|
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()
|
h20 = first.hash160()
|
||||||
if fn == 'bip84':
|
if fn == 'bip84':
|
||||||
assert addr == bech32.encode(addr[0:2], 0, h20)
|
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':
|
elif fn == 'bip49':
|
||||||
# don't have test logic for verifying these addrs
|
# don't have test logic for verifying these addrs
|
||||||
# - need to make script, and bleh
|
# - 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:
|
else:
|
||||||
assert False
|
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('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,
|
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,
|
addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_testnet,
|
||||||
load_export, testnet, skip_if_useless_way):
|
load_export, chain, skip_if_useless_way):
|
||||||
# test UX and values produced.
|
# test UX and values produced.
|
||||||
skip_if_useless_way(way)
|
skip_if_useless_way(way)
|
||||||
|
|
||||||
if not testnet:
|
use_testnet(chain == "XTN")
|
||||||
use_mainnet()
|
|
||||||
goto_home()
|
goto_home()
|
||||||
pick_menu_item('Advanced/Tools')
|
pick_menu_item('Advanced/Tools')
|
||||||
pick_menu_item('File Management')
|
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()
|
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)
|
root = BIP32Node.from_wallet_key(ek)
|
||||||
|
|
||||||
for ln in fp:
|
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 not f:
|
||||||
if rhs[0] in '1mn':
|
if rhs[0] in '1mn':
|
||||||
f = AF_CLASSIC
|
f = AF_CLASSIC
|
||||||
elif rhs[0:3] in ['tb1', "bc1"]:
|
elif rhs[0:4] in ['tb1q', "bc1q"]:
|
||||||
f = AF_P2WPKH
|
f = AF_P2WPKH
|
||||||
|
elif rhs[0:4] in ['tb1p', "bc1p"]:
|
||||||
|
f = AF_P2TR
|
||||||
elif rhs[0] in '23':
|
elif rhs[0] in '23':
|
||||||
f = AF_P2WPKH_P2SH
|
f = AF_P2WPKH_P2SH
|
||||||
else:
|
else:
|
||||||
raise ValueError(rhs)
|
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
|
@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
|
is_xfp = False
|
||||||
if '-84' in m:
|
if '-84' in m:
|
||||||
expect = "m/84h/0h/{acct}h"
|
expect = "m/84h/0h/{acct}h"
|
||||||
|
elif '86' in m and 'P2TR' in m:
|
||||||
|
expect = "m/86h/0h/{acct}h"
|
||||||
elif '-44' in m:
|
elif '-44' in m:
|
||||||
expect = "m/44h/0h/{acct}h"
|
expect = "m/44h/0h/{acct}h"
|
||||||
elif '49' in m:
|
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("chain", ["BTC", "XTN", "XRT"])
|
||||||
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"])
|
@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("acct_num", [None, 0, 1, (2 ** 31) - 1])
|
||||||
@pytest.mark.parametrize("int_ext", [True, False])
|
@pytest.mark.parametrize("int_ext", [True, False])
|
||||||
def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
|
def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
|
||||||
settings_set, need_keypress, expect_acctnum_captured, OK,
|
settings_set, need_keypress, expect_acctnum_captured, OK,
|
||||||
pick_menu_item, way, cap_story, cap_menu, int_ext, settings_get,
|
pick_menu_item, way, cap_story, cap_menu, int_ext, settings_get,
|
||||||
virtdisk_path, load_export, press_select, skip_if_useless_way):
|
virtdisk_path, load_export, press_select):
|
||||||
skip_if_useless_way(way)
|
|
||||||
|
|
||||||
settings_set('chain', chain)
|
settings_set('chain', chain)
|
||||||
chain_num = 1 if chain in ["XTN", "XRT"] else 0
|
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"
|
menu_item = "P2SH-Segwit"
|
||||||
desc_prefix = "sh(wpkh("
|
desc_prefix = "sh(wpkh("
|
||||||
bip44_purpose = 49
|
bip44_purpose = 49
|
||||||
|
elif addr_fmt == AF_P2TR:
|
||||||
|
menu_item = "Taproot P2TR"
|
||||||
|
desc_prefix = "tr("
|
||||||
|
bip44_purpose = 86
|
||||||
else:
|
else:
|
||||||
# addr_fmt == AF_CLASSIC:
|
# addr_fmt == AF_CLASSIC:
|
||||||
menu_item = "Classic P2PKH"
|
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)
|
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()
|
descriptor = contents.strip()
|
||||||
|
|
||||||
if int_ext is False:
|
if int_ext is False:
|
||||||
|
|||||||
@ -206,7 +206,7 @@ def hsm_reset(dev, sim_exec):
|
|||||||
|
|
||||||
# wallets
|
# wallets
|
||||||
(DICT(rules=[dict(wallet='1')]),
|
(DICT(rules=[dict(wallet='1')]),
|
||||||
'(non multisig)'),
|
'(singlesig only)'),
|
||||||
|
|
||||||
# users
|
# users
|
||||||
(DICT(rules=[dict(users=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
|
# simple p2pkh should fail
|
||||||
|
|
||||||
psbt = fake_txn(1, 2, dev.master_xpub, outvals=[amount, 1E8-amount], change_outputs=[1], fee=0)
|
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
|
# but txn w/ multisig wallet should work
|
||||||
psbt = fake_ms_txn(1, 2, M, keys, fee=0, outvals=[amount, 1E8-amount], outstyles=['p2wsh'],
|
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
|
# check ms txn not accepted when rule spec's a single signer
|
||||||
tweak_rule(0, dict(wallet='1'))
|
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])
|
@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):
|
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)
|
start_hsm(policy)
|
||||||
|
|
||||||
# try all addr types
|
# 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 = []
|
dests = []
|
||||||
psbt = fake_txn(1, 2, dev.master_xpub,
|
psbt = fake_txn(1, 2, dev.master_xpub,
|
||||||
outstyles=[style, 'p2wpkh'],
|
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))
|
M, xfp_paths, scr, addr_fmt=AF_P2WSH))
|
||||||
assert 'Not allowed in HSM mode' in str(ee)
|
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):
|
def test_xpub_sharing(dev, start_hsm, change_hsm, addr_fmt=AF_CLASSIC):
|
||||||
# xpub sharing, but only at certain derivations
|
# xpub sharing, but only at certain derivations
|
||||||
# - note 'm' is always shared
|
# - 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):
|
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
|
cc = bitcoind_d_sim_watch
|
||||||
dest_address = cc.getnewaddress()
|
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"]
|
psbt = cc.walletcreatefundedpsbt([], [{dest_address: 1.0}, {"data": op_return_data.hex()}], 0, {"fee_rate": 20})["psbt"]
|
||||||
policy = DICT(rules=[dict(max_amount=10)])
|
policy = DICT(rules=[dict(max_amount=10)])
|
||||||
start_hsm(policy)
|
start_hsm(policy)
|
||||||
|
|||||||
3011
testing/test_miniscript.py
Normal file
3011
testing/test_miniscript.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,9 +6,6 @@
|
|||||||
#
|
#
|
||||||
# py.test test_multisig.py -m ms_danger --ms-danger
|
# 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
|
import time, pytest, os, random, json, shutil, pdb, io, base64, struct, bech32, itertools, re
|
||||||
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
|
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
|
||||||
from ckcc.protocol import CCProtocolPacker, MAX_TXN_LEN
|
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 io import BytesIO
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from bbqr import split_qrs
|
from bbqr import split_qrs
|
||||||
|
from descriptor import MULTI_FMT_TO_SCRIPT, MultisigDescriptor, parse_desc_str
|
||||||
from charcodes import KEY_QR
|
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)
|
# default is BIP-45: m/45'/... (but no co-signer idx)
|
||||||
# - but can provide str format for deriviation, use {idx} for cosigner 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 = []
|
keys = []
|
||||||
|
|
||||||
for i in range(N-1):
|
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]
|
xfp = unpack("<I", pk.fingerprint())[0]
|
||||||
|
|
||||||
@ -126,7 +124,7 @@ def make_multisig(dev, sim_execfile):
|
|||||||
xfp_bytes = pk.fingerprint()
|
xfp_bytes = pk.fingerprint()
|
||||||
xfp = swab32(struct.unpack('>I', xfp_bytes)[0])
|
xfp = swab32(struct.unpack('>I', xfp_bytes)[0])
|
||||||
else:
|
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
|
xfp = simulator_fixed_xfp
|
||||||
|
|
||||||
if not deriv:
|
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,
|
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None,
|
||||||
keys=None, do_import=True, derivs=None, descriptor=False,
|
keys=None, do_import=True, derivs=None, descriptor=False,
|
||||||
int_ext_desc=False, dev_key=False, way=None, bip67=True,
|
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
|
# param: bip67 if false, only usable together with descriptor=True
|
||||||
if not bip67:
|
if not bip67:
|
||||||
assert descriptor, "needs descriptor=True"
|
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)
|
settings_set("unsort_ms", 1)
|
||||||
|
|
||||||
keys = keys or make_multisig(M, N, unique=unique, dev_key=dev_key,
|
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}'
|
name = name or f'test-{M}-{N}'
|
||||||
|
|
||||||
if not do_import:
|
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
|
# the different addr formats
|
||||||
for af in unmap_addr_fmt.keys():
|
for af in unmap_addr_fmt.keys():
|
||||||
|
if af == "p2tr": continue
|
||||||
config = f'format: {af}\n'
|
config = f'format: {af}\n'
|
||||||
config += '\n'.join(sk.hwif(as_private=False) for xfp,m,sk in keys)
|
config += '\n'.join(sk.hwif(as_private=False) for xfp,m,sk in keys)
|
||||||
title, story = offer_ms_import(config)
|
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
|
@pytest.fixture
|
||||||
def test_ms_show_addr(dev, cap_story, press_select, addr_vs_path, bitcoind_p2sh,
|
def test_ms_show_addr(dev, cap_story, press_select, addr_vs_path, bitcoind_p2sh,
|
||||||
has_ms_checks, is_q1):
|
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
|
# test we are showing addresses correctly
|
||||||
# - verifies against bitcoind as well
|
# - verifies against bitcoind as well
|
||||||
addr_fmt = unmap_addr_fmt.get(addr_fmt, addr_fmt)
|
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()
|
press_select()
|
||||||
|
|
||||||
# check expected addr was generated based on my math
|
# 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
|
# also check against bitcoind
|
||||||
core_addr, core_scr = bitcoind_p2sh(M, pubkeys, addr_fmt)
|
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:
|
try:
|
||||||
# test an address that should be in that wallet.
|
# test an address that should be in that wallet.
|
||||||
time.sleep(.1)
|
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:
|
finally:
|
||||||
clear_ms()
|
clear_ms()
|
||||||
@ -1051,8 +1051,8 @@ def test_import_dup_safe(N, clear_ms, make_multisig, offer_ms_import,
|
|||||||
|
|
||||||
menu = cap_menu()
|
menu = cap_menu()
|
||||||
assert f'{M}/{N}: {name}' in menu
|
assert f'{M}/{N}: {name}' in menu
|
||||||
# depending if NFC enabled or not, and if Q (has QR)
|
# depending if NFC enabled or not, and if Q (has QR) or whether EDGE
|
||||||
assert (len(menu) - num_wallets) in [6, 7, 8]
|
assert (len(menu) - num_wallets) in [6, 7, 8, 9]
|
||||||
|
|
||||||
title, story = offer_ms_import(make_named('xxx-orig'))
|
title, story = offer_ms_import(make_named('xxx-orig'))
|
||||||
assert 'Create new multisig wallet' in story
|
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
|
# 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)
|
num_outs = len(all_out_styles)
|
||||||
|
|
||||||
clear_ms()
|
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
|
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('N', [ 3, 15])
|
||||||
@pytest.mark.parametrize('xderiv', [ None, 'any', 'unknown', '*', '', 'none'])
|
@pytest.mark.parametrize('xderiv', [ None, 'any', 'unknown', '*', '', 'none'])
|
||||||
def test_ms_import_nopath(N, xderiv, make_multisig, clear_ms, offer_ms_import):
|
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
|
# once change is selected - do not offer this option again
|
||||||
assert "change addresses." not in story
|
assert "change addresses." not in story
|
||||||
assert "(0)" not in story
|
assert "(0)" not in story
|
||||||
|
|
||||||
# unwrap text a bit
|
# unwrap text a bit
|
||||||
if change:
|
if change:
|
||||||
story = story.replace("=>\n", "=> ").replace('1/0]\n =>', "1/0 =>")
|
story = story.replace("=>\n", "=> ").replace('1/0]\n =>', "1/0] =>")
|
||||||
else:
|
else:
|
||||||
story = story.replace("=>\n", "=> ").replace('0/0]\n =>', "0/0 =>")
|
story = story.replace("=>\n", "=> ").replace('0/0]\n =>', "0/0] =>")
|
||||||
|
|
||||||
maps = []
|
maps = []
|
||||||
for ln in story.split('\n'):
|
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()
|
path,chk,addr = ln.split()
|
||||||
assert chk == '=>'
|
assert chk == '=>'
|
||||||
assert '/' in path
|
assert '/' in path
|
||||||
|
path = path.replace("[", "").replace("]", "")
|
||||||
|
|
||||||
maps.append( (path, addr) )
|
maps.append((path, addr))
|
||||||
|
|
||||||
if start_idx <= 2147483638:
|
if start_idx <= 2147483638:
|
||||||
assert len(maps) == 10
|
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)
|
path_mapper=path_mapper, bip67=bip67)
|
||||||
|
|
||||||
assert int(subpath.split('/')[-1]) == idx
|
assert int(subpath.split('/')[-1]) == idx
|
||||||
|
assert int(subpath.split('/')[-2]) == chng_idx
|
||||||
#print('../0/%s => \n %s' % (idx, B2A(script)))
|
#print('../0/%s => \n %s' % (idx, B2A(script)))
|
||||||
|
|
||||||
start, end = detruncate_address(addr)
|
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)
|
bitcoind_addrs = bitcoind.deriveaddresses(desc_export, addr_range)
|
||||||
for idx, cc_item in enumerate(cc_addrs):
|
for idx, cc_item in enumerate(cc_addrs):
|
||||||
cc_item = cc_item.split(",")
|
cc_item = cc_item.split(",")
|
||||||
partial_address = cc_item[part_addr_index]
|
address = cc_item[part_addr_index]
|
||||||
_start, _end = partial_address.split("___")
|
|
||||||
if way != "nfc":
|
if way != "nfc":
|
||||||
_start, _end = _start[1:], _end[:-1]
|
address = address[1:-1]
|
||||||
assert bitcoind_addrs[idx].startswith(_start)
|
assert bitcoind_addrs[idx] == address
|
||||||
assert bitcoind_addrs[idx].endswith(_end)
|
|
||||||
|
|
||||||
|
@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
|
@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", [
|
@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))"),
|
# ("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"),
|
("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/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"),
|
("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"),
|
("xpub depth", "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"),
|
("Key derivation too long", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0))#s487stua"),
|
||||||
("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"),
|
|
||||||
("Cannot use hardened sub derivation path", "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[c463f778/44'/0'/0']tpubDD8pw7eZ9bUzYUR1LK5wpkA69iy3BpuLxPzsE6FFNdtTnJDySduc1VJdFEhEJQDKjYktznKdJgHwaQDRfQDQJpceDxH22c1ZKUMjrarVs7M/0'/*))#3w6hpha3"),
|
("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"),
|
("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,
|
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('cmn_pth_from_root', [True, False])
|
||||||
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
|
@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('desc', ["multi", "sortedmulti"])
|
||||||
@pytest.mark.parametrize('addr_fmt', [AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH])
|
@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,
|
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()
|
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):
|
def test_multisig_name_validation(microsd_path, offer_ms_import):
|
||||||
with open("data/multisig/export-p2wsh-myself.txt", "r") as f:
|
with open("data/multisig/export-p2wsh-myself.txt", "r") as f:
|
||||||
config = f.read()
|
config = f.read()
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
#
|
#
|
||||||
# Address ownership tests.
|
# Address ownership tests.
|
||||||
#
|
#
|
||||||
import pytest, time, io, csv
|
import pytest, time, io, csv, json
|
||||||
from txn import fake_address
|
from txn import fake_address
|
||||||
from base58 import encode_base58_checksum
|
from base58 import encode_base58_checksum
|
||||||
from helpers import hash160
|
from helpers import hash160, taptweak
|
||||||
from bip32 import BIP32Node
|
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
|
from constants import simulator_fixed_xprv, simulator_fixed_tprv, addr_fmt_names
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -23,7 +23,7 @@ def wipe_cache(sim_exec):
|
|||||||
[14, 8, 26, 1, 7, 19]
|
[14, 8, 26, 1, 7, 19]
|
||||||
'''
|
'''
|
||||||
@pytest.mark.parametrize('addr_fmt', [
|
@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] )
|
@pytest.mark.parametrize('testnet', [ False, True] )
|
||||||
def test_negative(addr_fmt, testnet, sim_exec):
|
def test_negative(addr_fmt, testnet, sim_exec):
|
||||||
@ -36,24 +36,26 @@ def test_negative(addr_fmt, testnet, sim_exec):
|
|||||||
|
|
||||||
assert 'Explained' in lst
|
assert 'Explained' in lst
|
||||||
|
|
||||||
@pytest.mark.parametrize('addr_fmt, testnet', [
|
@pytest.mark.parametrize('addr_fmt, chain', [
|
||||||
(AF_CLASSIC, True),
|
(AF_CLASSIC, "XTN"),
|
||||||
(AF_CLASSIC, False),
|
(AF_CLASSIC, "BTC"),
|
||||||
(AF_P2WPKH, True),
|
(AF_P2WPKH, "XTN"),
|
||||||
(AF_P2WPKH, False),
|
(AF_P2WPKH, "BTC"),
|
||||||
(AF_P2WPKH_P2SH, True),
|
(AF_P2WPKH_P2SH, "XTN"),
|
||||||
(AF_P2WPKH_P2SH, False),
|
(AF_P2WPKH_P2SH, "BTC"),
|
||||||
|
(AF_P2TR, "XTN"),
|
||||||
|
(AF_P2TR, "BTC"),
|
||||||
|
|
||||||
# multisig - testnet only
|
# multisig - testnet only
|
||||||
(AF_P2WSH, True),
|
(AF_P2WSH, "XTN"),
|
||||||
(AF_P2SH, True),
|
(AF_P2SH, "XTN"),
|
||||||
(AF_P2WSH_P2SH,True),
|
(AF_P2WSH_P2SH, "XTN"),
|
||||||
])
|
])
|
||||||
@pytest.mark.parametrize('offset', [ 3, 760] )
|
@pytest.mark.parametrize('offset', [ 3, 760] )
|
||||||
@pytest.mark.parametrize('subaccount', [ 0, 34] )
|
@pytest.mark.parametrize('subaccount', [ 0, 34] )
|
||||||
@pytest.mark.parametrize('change_idx', [ 0, 1] )
|
@pytest.mark.parametrize('change_idx', [ 0, 1] )
|
||||||
@pytest.mark.parametrize('from_empty', [ True, False] )
|
@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,
|
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
|
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
|
# API/Unit test, limited UX
|
||||||
|
|
||||||
if not testnet and addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
|
if chain == "BTC":
|
||||||
# multisig jigs assume testnet
|
use_testnet(False)
|
||||||
raise pytest.skip('testnet only')
|
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:
|
if from_empty:
|
||||||
wipe_cache() # very different codepaths
|
wipe_cache() # very different codepaths
|
||||||
settings_set('accts', [])
|
settings_set('accts', [])
|
||||||
|
|
||||||
coin_type = 1 if testnet else 0
|
|
||||||
|
|
||||||
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
|
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
|
||||||
from test_multisig import make_ms_address, HARD
|
from test_multisig import make_ms_address, HARD
|
||||||
M, N = 1, 3
|
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:
|
elif addr_fmt == AF_P2WPKH:
|
||||||
menu_item = expect_name = 'Segwit P2WPKH'
|
menu_item = expect_name = 'Segwit P2WPKH'
|
||||||
path = "m/84h/{ct}h/{acc}h"
|
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:
|
else:
|
||||||
raise ValueError(addr_fmt)
|
raise ValueError(addr_fmt)
|
||||||
|
|
||||||
@ -108,14 +119,18 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
|
|||||||
|
|
||||||
# see addr_vs_path
|
# see addr_vs_path
|
||||||
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
|
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:
|
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:
|
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||||
pkh = sk.hash160()
|
pkh = sk.hash160()
|
||||||
digest = hash160(b'\x00\x14' + pkh)
|
digest = hash160(b'\x00\x14' + pkh)
|
||||||
addr = encode_base58_checksum(bytes([196 if testnet else 5]) + digest)
|
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:
|
else:
|
||||||
pkh = sk.hash160()
|
pkh = sk.hash160()
|
||||||
addr = bech32_encode('tb' if testnet else 'bc', 0, pkh)
|
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)
|
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))
|
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
|
||||||
sk = mk.subkey_for_path(path)
|
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:
|
else:
|
||||||
addr = fake_address(addr_fmt, testnet)
|
addr = fake_address(addr_fmt, testnet)
|
||||||
|
|
||||||
@ -220,20 +235,26 @@ def test_ux(valid, testnet, method,
|
|||||||
assert 'Searched ' in story
|
assert 'Searched ' in story
|
||||||
assert 'candidates without finding a match' 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,
|
def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explorer,
|
||||||
pick_menu_item, need_keypress, sim_exec, clear_ms,
|
pick_menu_item, need_keypress, sim_exec, clear_ms,
|
||||||
import_ms_wallet, press_select, goto_home, nfc_write,
|
import_ms_wallet, press_select, goto_home, nfc_write,
|
||||||
load_shared_mod, load_export_and_verify_signature,
|
load_shared_mod, load_export_and_verify_signature,
|
||||||
cap_story):
|
cap_story, load_export, offer_minsc_import):
|
||||||
goto_home()
|
goto_home()
|
||||||
wipe_cache()
|
wipe_cache()
|
||||||
settings_set('accts', [])
|
settings_set('accts', [])
|
||||||
|
|
||||||
if af == "ms0":
|
if af == "ms0":
|
||||||
clear_ms()
|
clear_ms()
|
||||||
import_ms_wallet(2,3, name=af)
|
import_ms_wallet(2, 3, name=af)
|
||||||
press_select() # accept ms import
|
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()
|
goto_address_explorer()
|
||||||
pick_menu_item(af)
|
pick_menu_item(af)
|
||||||
@ -245,17 +266,19 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
|
|||||||
lst = eval(lst)
|
lst = eval(lst)
|
||||||
assert lst
|
assert lst
|
||||||
|
|
||||||
if af == "ms0":
|
|
||||||
return # multisig addresses are blanked
|
|
||||||
|
|
||||||
title, body = cap_story()
|
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)
|
addr_dump = io.StringIO(contents)
|
||||||
cc = csv.reader(addr_dump)
|
cc = csv.reader(addr_dump)
|
||||||
hdr = next(cc)
|
hdr = next(cc)
|
||||||
assert hdr == ['Index', 'Payment Address', 'Derivation']
|
|
||||||
addr = None
|
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
|
assert int(idx) == n
|
||||||
if idx == 200:
|
if idx == 200:
|
||||||
addr = addr
|
addr = addr
|
||||||
@ -279,7 +302,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
|
|||||||
assert addr in story
|
assert addr in story
|
||||||
assert title == 'Verified Address'
|
assert title == 'Verified Address'
|
||||||
assert 'Found in wallet' in story
|
assert 'Found in wallet' in story
|
||||||
assert 'Derivation path' in story
|
# assert 'Derivation path' in story
|
||||||
if af == "P2SH-Segwit":
|
if af == "P2SH-Segwit":
|
||||||
assert "P2WPKH-in-P2SH" in story
|
assert "P2WPKH-in-P2SH" in story
|
||||||
elif af == "Segwit P2WPKH":
|
elif af == "Segwit P2WPKH":
|
||||||
|
|||||||
@ -6,19 +6,19 @@
|
|||||||
# This module can and should be run with `-l` and without it.
|
# 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 binascii import a2b_hex
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from bip32 import PrivateKey
|
from bip32 import PrivateKey
|
||||||
from ckcc_protocol.constants import *
|
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('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,
|
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,
|
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
|
# test UX and operation of the 'bitcoin core' wallet export
|
||||||
mx = "Don't make PDF"
|
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()
|
goto_home()
|
||||||
pick_menu_item('Advanced/Tools')
|
pick_menu_item('Advanced/Tools')
|
||||||
try:
|
pick_menu_item('Paper Wallets')
|
||||||
pick_menu_item('Paper Wallets')
|
|
||||||
except:
|
|
||||||
raise pytest.skip('Feature absent')
|
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
title, story = cap_story()
|
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')
|
pick_menu_item('Segwit P2WPKH')
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if mode == 'taproot':
|
||||||
|
pick_menu_item('Classic P2PKH')
|
||||||
|
pick_menu_item('Taproot P2TR')
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
if pdf:
|
if pdf:
|
||||||
assert mx in cap_menu()
|
assert mx in cap_menu()
|
||||||
shutil.copy('../docs/paperwallet.pdf', microsd_path('paperwallet.pdf'))
|
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)
|
time.sleep(0.1)
|
||||||
title, story = cap_story()
|
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")
|
need_keypress("1")
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
title, story = cap_story()
|
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]
|
story = [i for i in story.split('\n') if i]
|
||||||
sig_file = story[-1]
|
sig_file = story[-1]
|
||||||
if not pdf:
|
if not pdf:
|
||||||
fname = story[-2]
|
if mode == "taproot":
|
||||||
fnames = [fname]
|
fname = story[-1]
|
||||||
|
else:
|
||||||
|
fname = story[-2]
|
||||||
|
fnames = [fname]
|
||||||
else:
|
else:
|
||||||
fname = story[-3]
|
if mode == "taproot":
|
||||||
pdf_name = story[-2]
|
fname = story[-2]
|
||||||
fnames = [fname, pdf_name]
|
pdf_name = story[-1]
|
||||||
|
else:
|
||||||
|
fname = story[-3]
|
||||||
|
pdf_name = story[-2]
|
||||||
|
fnames = [fname, pdf_name]
|
||||||
assert pdf_name.endswith('.pdf')
|
assert pdf_name.endswith('.pdf')
|
||||||
|
|
||||||
assert fname.endswith('.txt')
|
assert fname.endswith('.txt')
|
||||||
assert sig_file.endswith(".sig")
|
if mode != 'taproot':
|
||||||
verify_detached_signature_file(fnames, sig_file, "sd",
|
assert sig_file.endswith(".sig")
|
||||||
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
|
verify_detached_signature_file(fnames, sig_file, "sd",
|
||||||
|
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
|
||||||
|
|
||||||
path = microsd_path(fname)
|
path = microsd_path(fname)
|
||||||
|
_wif = None
|
||||||
|
_sk = None
|
||||||
|
_addr = None
|
||||||
|
_idesc = None
|
||||||
with open(path, 'rt') as fp:
|
with open(path, 'rt') as fp:
|
||||||
hdr = None
|
hdr = None
|
||||||
for ln in fp:
|
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()
|
val = ln.strip()
|
||||||
if 'Deposit address' in hdr:
|
if 'Deposit address' in hdr:
|
||||||
assert val == fname.split('.', 1)[0].split('-', 1)[0]
|
assert val == fname.split('.', 1)[0].split('-', 1)[0]
|
||||||
txt_addr = val
|
_addr = val
|
||||||
addr = val
|
|
||||||
elif hdr == 'Private key:': # for QR case
|
elif hdr == 'Private key:': # for QR case
|
||||||
assert val == wif
|
assert val == _wif
|
||||||
elif 'Private key' in hdr and 'WIF=Wallet' in hdr:
|
elif 'Private key' in hdr and 'WIF=Wallet' in hdr:
|
||||||
wif = val
|
_wif = val
|
||||||
k1 = PrivateKey.from_wif(val)
|
|
||||||
elif 'Private key' in hdr and 'Hex, 32 bytes' in hdr:
|
elif 'Private key' in hdr and 'Hex, 32 bytes' in hdr:
|
||||||
k2 = PrivateKey(sec_exp=a2b_hex(val))
|
_sk = val
|
||||||
elif 'Bitcoin Core command':
|
elif 'Bitcoin Core command':
|
||||||
assert wif in val
|
assert _wif in val
|
||||||
assert 'importmulti' in val or 'importprivkey' in val
|
if 'importdescriptors' in val:
|
||||||
|
_idesc = val
|
||||||
|
assert 'importprivkey' in val or 'importdescriptors' in val
|
||||||
else:
|
else:
|
||||||
print(f'{hdr} => {val}')
|
print(f'{hdr} => {val}')
|
||||||
raise ValueError(hdr)
|
raise ValueError(hdr)
|
||||||
|
|
||||||
assert k1.K.sec() == k2.K.sec()
|
if netcode != "XRT":
|
||||||
assert addr == k1.K.address(addr_fmt="p2wpkh" if mode == "segwit" else "p2pkh",
|
from bip32 import PrivateKey
|
||||||
testnet=True if netcode == "XTN" else False)
|
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
|
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:
|
with open(path, 'rb') as fp:
|
||||||
|
|
||||||
d = fp.read()
|
d = fp.read()
|
||||||
assert wif.encode('ascii') in d
|
assert _wif.encode('ascii') in d
|
||||||
assert txt_addr.encode('ascii') in d
|
assert _addr.encode('ascii') in d
|
||||||
|
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
@ -276,7 +309,7 @@ def test_dice_generate(rolls, testnet, dev, cap_menu, pick_menu_item, goto_home,
|
|||||||
val, = hx
|
val, = hx
|
||||||
|
|
||||||
k2 = PrivateKey(sec_exp=a2b_hex(val))
|
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()
|
assert val == sha256(rolls.encode('ascii')).hexdigest()
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from pprint import pprint, pformat
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from base64 import b64encode, b64decode
|
from base64 import b64encode, b64decode
|
||||||
from base58 import encode_base58_checksum
|
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 helpers import xfp2str, seconds2human_readable, hash160
|
||||||
from msg import verify_message
|
from msg import verify_message
|
||||||
from bip32 import BIP32Node
|
from bip32 import BIP32Node
|
||||||
@ -133,8 +133,8 @@ def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec):
|
|||||||
assert oo == rb
|
assert oo == rb
|
||||||
|
|
||||||
@pytest.mark.unfinalized
|
@pytest.mark.unfinalized
|
||||||
def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
|
@pytest.mark.parametrize("taproot", [True, False])
|
||||||
press_select):
|
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
|
# measure time to sign a larger txn
|
||||||
if is_mark4:
|
if is_mark4:
|
||||||
# Mk4: expect
|
# 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_in = 9
|
||||||
num_out = 100
|
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)
|
open('debug/speed.psbt', 'wb').write(psbt)
|
||||||
dt = time.time()
|
dt = time.time()
|
||||||
@ -191,8 +194,9 @@ if 0:
|
|||||||
@pytest.mark.bitcoind
|
@pytest.mark.bitcoind
|
||||||
@pytest.mark.veryslow
|
@pytest.mark.veryslow
|
||||||
@pytest.mark.parametrize('segwit', [True, False])
|
@pytest.mark.parametrize('segwit', [True, False])
|
||||||
def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
|
@pytest.mark.parametrize('taproot', [True, False])
|
||||||
start_sign, end_sign, dev, segwit, accept = True):
|
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
|
# try a bunch of different bigger sized txns
|
||||||
# - important to test on real device, due to it's limited memory
|
# - 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_in = 250
|
||||||
num_out = 2000
|
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)
|
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.bitcoind
|
||||||
@pytest.mark.parametrize('num_ins', [ 2, 7, 15 ])
|
@pytest.mark.parametrize('num_ins', [ 2, 7, 15 ])
|
||||||
@pytest.mark.parametrize('segwit', [True, False])
|
@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
|
# create a TXN using actual addresses that are correct for DUT
|
||||||
xp = dev.master_xpub
|
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)
|
open('debug/real-%d.psbt' % num_ins, 'wb').write(psbt)
|
||||||
|
|
||||||
_, txn = try_sign(psbt, accept=True, finalize=True)
|
_, 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('num_outs', [ 2, 7, 15 ])
|
||||||
@pytest.mark.parametrize('act_outs', [ 2, 1, -1])
|
@pytest.mark.parametrize('act_outs', [ 2, 1, -1])
|
||||||
@pytest.mark.parametrize('segwit', [True, False])
|
@pytest.mark.parametrize('segwit', [True, False])
|
||||||
|
@pytest.mark.parametrize('taproot', [True, False])
|
||||||
@pytest.mark.parametrize('add_xpub', [True, False])
|
@pytest.mark.parametrize('add_xpub', [True, False])
|
||||||
@pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE)
|
@pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE)
|
||||||
@pytest.mark.parametrize('visualized', [0, STXN_VISUALIZE, STXN_VISUALIZE|STXN_SIGNED])
|
@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,
|
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.
|
# create a TXN which has change outputs, which shouldn't be shown to user, and also not fail.
|
||||||
xp = dev.master_xpub
|
xp = dev.master_xpub
|
||||||
|
|
||||||
couts = num_outs if act_outs == -1 else num_ins-act_outs
|
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)
|
outstyles=[out_style], change_outputs=range(couts), add_xpub=add_xpub)
|
||||||
|
|
||||||
open('debug/change.psbt', 'wb').write(psbt)
|
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'),
|
("45'/1'/0'/1/5", 'diff path prefix'),
|
||||||
("44'/2'/0'/1/5", 'diff path prefix'),
|
("44'/2'/0'/1/5", 'diff path prefix'),
|
||||||
("44'/1'/1'/1/5", 'diff path prefix'),
|
("44'/1'/1'/1/5", 'diff path prefix'),
|
||||||
("44'/1'/0'/3000/5", '2nd last component'),
|
# ("44'/1'/0'/3000/5", '2nd last component'),
|
||||||
("44'/1'/0'/3/5", '2nd last component'),
|
# ("44'/1'/0'/3/5", '2nd last component'),
|
||||||
])
|
])
|
||||||
def test_change_troublesome(dev, start_sign, cap_story, try_path, expect):
|
def test_change_troublesome(dev, start_sign, cap_story, try_path, expect):
|
||||||
# NOTE: out#1 is change:
|
# NOTE: out#1 is change:
|
||||||
@ -1209,8 +1217,10 @@ def hist_count(sim_exec):
|
|||||||
|
|
||||||
@pytest.mark.parametrize('num_utxo', [9, 100])
|
@pytest.mark.parametrize('num_utxo', [9, 100])
|
||||||
@pytest.mark.parametrize('segwit_in', [False, True])
|
@pytest.mark.parametrize('segwit_in', [False, True])
|
||||||
def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, settings_set,
|
@pytest.mark.parametrize('taproot_in', [False, True])
|
||||||
settings_get, cap_story, sim_exec, hist_count):
|
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
|
# cleanup prev runs, if very first time thru
|
||||||
sim_exec('import history; history.OutptValueCache.clear()')
|
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
|
# 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),
|
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)
|
_, txn = try_sign(psbt, accept=True, finalize=True)
|
||||||
|
|
||||||
open('debug/funding.psbt', 'wb').write(psbt)
|
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)
|
time.sleep(.1)
|
||||||
title, story = cap_story()
|
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('segwit', [False, True])
|
||||||
|
@pytest.mark.parametrize('taproot', [False, True])
|
||||||
@pytest.mark.parametrize('num_ins', [1, 17])
|
@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
|
# verify correct txid for transactions is being calculated
|
||||||
xp = dev.master_xpub
|
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)
|
_, 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]
|
pp = psbt.inputs[0].bip32_paths[pk]
|
||||||
psbt.inputs[0].bip32_paths[pk] = b'what' + pp[4:]
|
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,
|
_, txn, txid = try_sign_microsd(psbt, finalize=not partial,
|
||||||
encoding=encoding, del_after=del_after)
|
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)
|
txn = end_sign(True, finalize=False)
|
||||||
|
|
||||||
@pytest.mark.parametrize('segwit', [False, True])
|
@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
|
# 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
|
# change all inputs to be "not ours" ... but with utxo details
|
||||||
for i in psbt.inputs:
|
for i in psbt.inputs:
|
||||||
i.bip32_paths.clear()
|
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:
|
with pytest.raises(CCProtoError) as ee:
|
||||||
orig, result = try_sign(psbt, accept=True)
|
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)
|
assert 'does not contain any key path information' in str(ee)
|
||||||
|
|
||||||
@pytest.mark.parametrize('segwit', [False, True])
|
@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
|
# 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 i in psbt.inputs:
|
||||||
for pubkey in i.bip32_paths:
|
for pubkey in i.bip32_paths:
|
||||||
i.bip32_paths[pubkey] = wrong_xfp + i.bip32_paths[pubkey][4:]
|
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:
|
with pytest.raises(CCProtoError) as ee:
|
||||||
orig, result = try_sign(psbt, accept=True)
|
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)
|
assert 'found 12345678' in str(ee)
|
||||||
|
|
||||||
@pytest.mark.parametrize('segwit', [False, True])
|
@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
|
# A PSBT which is unsigned and doesn't involve our XFP value
|
||||||
# - but multiple wrong XFP values
|
# - 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)
|
here = struct.pack('<I', idx+73)
|
||||||
i.bip32_paths[pubkey] = here + i.bip32_paths[pubkey][4:]
|
i.bip32_paths[pubkey] = here + i.bip32_paths[pubkey][4:]
|
||||||
wrongs.add(xfp2str(idx+73))
|
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)
|
open('debug/wrong-xfp.psbt', 'wb').write(psbt)
|
||||||
with pytest.raises(CCProtoError) as ee:
|
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('out_style', ADDR_STYLES_SINGLE)
|
||||||
@pytest.mark.parametrize('segwit', [False, True])
|
@pytest.mark.parametrize('segwit', [False, True])
|
||||||
|
@pytest.mark.parametrize('taproot', [False, True])
|
||||||
@pytest.mark.parametrize('outval', ['.5', '.788888', '0.92640866'])
|
@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
|
# check how we render the value of outputs
|
||||||
# - works on simulator and connected USB real-device
|
# - works on simulator and connected USB real-device
|
||||||
xp = dev.master_xpub
|
xp = dev.master_xpub
|
||||||
oi = int(Decimal(outval) * int(1E8))
|
oi = int(Decimal(outval) * int(1E8))
|
||||||
|
|
||||||
psbt = fake_txn(1, 2, dev.master_xpub, segwit_in=segwit, outvals=[oi, int(1E8-oi)],
|
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)
|
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':
|
elif out_style == 'p2wpkh-p2sh':
|
||||||
assert len(set(i[0] for i in addrs)) == 1
|
assert len(set(i[0] for i in addrs)) == 1
|
||||||
assert addrs[0][0] in {'2', '3'}
|
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):
|
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("segwit_in", [True, False])
|
||||||
@pytest.mark.parametrize('num_not_ours', [1, 3, 4])
|
@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):
|
def hack(psbt):
|
||||||
# change first input to not be ours
|
# change first input to not be ours
|
||||||
for i in range(num_not_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
|
assert signed != psbt
|
||||||
|
|
||||||
@pytest.mark.parametrize("segwit_in", [True, False])
|
@pytest.mark.parametrize("segwit_in", [True, False])
|
||||||
|
@pytest.mark.parametrize("taproot_in", [True, False])
|
||||||
@pytest.mark.parametrize("num_missing", [1, 3, 4])
|
@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,
|
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):
|
def hack(psbt):
|
||||||
for i in range(num_missing):
|
for i in range(num_missing):
|
||||||
# no utxo provided for our input
|
# no utxo provided for our input
|
||||||
psbt.inputs[i].utxo = None
|
psbt.inputs[i].utxo = None
|
||||||
psbt.inputs[i].witness_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)
|
start_sign(psbt)
|
||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
title, story = cap_story()
|
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")
|
alice = bitcoind.create_wallet(wallet_name="alice")
|
||||||
bob = bitcoind.create_wallet(wallet_name="bob")
|
bob = bitcoind.create_wallet(wallet_name="bob")
|
||||||
cc = bitcoind_d_sim_watch
|
cc = bitcoind_d_sim_watch
|
||||||
|
tap_dave = bitcoind.create_wallet(wallet_name="tap_dave")
|
||||||
alice_addr = alice.getnewaddress()
|
alice_addr = alice.getnewaddress()
|
||||||
alice_pubkey = alice.getaddressinfo(alice_addr)["pubkey"]
|
alice_pubkey = alice.getaddressinfo(alice_addr)["pubkey"]
|
||||||
bob_addr = bob.getnewaddress()
|
bob_addr = bob.getnewaddress()
|
||||||
bob_pubkey = bob.getaddressinfo(bob_addr)["pubkey"]
|
bob_pubkey = bob.getaddressinfo(bob_addr)["pubkey"]
|
||||||
cc_addr = cc.getnewaddress()
|
cc_addr = cc.getnewaddress()
|
||||||
cc_pubkey = cc.getaddressinfo(cc_addr)["pubkey"]
|
cc_pubkey = cc.getaddressinfo(cc_addr)["pubkey"]
|
||||||
|
tap_dave_addr = tap_dave.getnewaddress("", "bech32m")
|
||||||
# fund all addresses
|
# fund all addresses
|
||||||
for addr in (alice_addr, bob_addr, cc_addr):
|
for addr in (alice_addr, bob_addr, cc_addr, tap_dave_addr):
|
||||||
bitcoind.supply_wallet.generatetoaddress(101, addr)
|
bitcoind.supply_wallet.sendtoaddress(addr, 2)
|
||||||
|
|
||||||
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
||||||
|
|
||||||
psbt_list = []
|
psbt_list = []
|
||||||
for w in (alice, bob, cc):
|
for w in (alice, bob, cc, tap_dave):
|
||||||
assert w.listunspent()
|
assert w.listunspent()
|
||||||
psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
|
psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
|
||||||
psbt_list.append(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)
|
assert pk.hex() in (alice_pubkey, bob_pubkey)
|
||||||
inp.utxo = None
|
inp.utxo = None
|
||||||
inp.witness_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()
|
psbt0 = the_psbt_obj.as_bytes()
|
||||||
orig, res = try_sign(psbt0, accept=True)
|
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
|
# 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"]
|
psbt1 = bob.walletprocesspsbt(base64.b64encode(res).decode(), True, "ALL")["psbt"]
|
||||||
# finally sign with alice
|
# finally sign with alice
|
||||||
res = alice.walletprocesspsbt(psbt1, True, "ALL")
|
res = alice.walletprocesspsbt(psbt1, True)
|
||||||
psbt2 = res["psbt"]
|
psbt2 = res["psbt"]
|
||||||
|
res = tap_dave.walletprocesspsbt(psbt2, True)
|
||||||
|
psbt3 = res["psbt"]
|
||||||
assert res["complete"] is True
|
assert res["complete"] is True
|
||||||
tx = alice.finalizepsbt(psbt2)["hex"]
|
tx = alice.finalizepsbt(psbt3)["hex"]
|
||||||
assert alice.testmempoolaccept([tx])[0]["allowed"] is True
|
assert alice.testmempoolaccept([tx])[0]["allowed"] is True
|
||||||
tx_id = alice.sendrawtransaction(tx)
|
tx_id = alice.sendrawtransaction(tx)
|
||||||
assert isinstance(tx_id, str) and len(tx_id) == 64
|
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):
|
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
|
cc = bitcoind_d_sim_watch
|
||||||
dest_address = cc.getnewaddress()
|
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"]
|
psbt = cc.walletcreatefundedpsbt([], [{dest_address: 1.0}, {"data": op_return_data.hex()}], 0, {"fee_rate": 20})["psbt"]
|
||||||
start_sign(base64.b64decode(psbt))
|
start_sign(base64.b64decode(psbt))
|
||||||
time.sleep(.1)
|
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,
|
def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
|
||||||
bitcoind, bitcoind_d_dev_watch, settings_set, finalize_v2_v0_convert):
|
bitcoind, bitcoind_d_dev_watch, settings_set, finalize_v2_v0_convert):
|
||||||
def doit(addr_fmt, sighash, num_inputs=2, num_outputs=2, consolidation=False, sh_checks=False,
|
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
|
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)
|
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)
|
bitcoind_d_dev_watch.keypoolrefill(num_inputs + num_outputs)
|
||||||
input_val = bitcoind.supply_wallet.getbalance() / num_inputs
|
input_val = bitcoind.supply_wallet.getbalance() / num_inputs
|
||||||
cc_dest = [
|
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()
|
unspent = bitcoind_d_dev_watch.listunspent()
|
||||||
output_val = bitcoind_d_dev_watch.getbalance() / num_outputs
|
output_val = bitcoind_d_dev_watch.getbalance() / num_outputs
|
||||||
# consolidation or not?
|
# 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 = [
|
destinations = [
|
||||||
{dest_wal.getnewaddress("", addr_fmt): Decimal(output_val).quantize(Decimal('.0000001'), rounding=ROUND_DOWN)}
|
{dest_wal.getnewaddress("", addr_fmt): Decimal(output_val).quantize(Decimal('.0000001'), rounding=ROUND_DOWN)}
|
||||||
for _ in range(num_outputs)
|
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
|
assert resp["complete"] is True
|
||||||
tx_hex = resp["hex"]
|
tx_hex = resp["hex"]
|
||||||
|
|
||||||
if tx_check:
|
# sign again - this time get finalized tx ready for broadcast out
|
||||||
# sign and get finalized tx ready for broadcast out
|
start_sign(psbt_sh_bytes, finalize=True)
|
||||||
start_sign(psbt_sh_bytes, finalize=True)
|
cc_tx_hex = end_sign(accept=True, finalize=True).hex()
|
||||||
cc_tx_hex = end_sign(accept=True, finalize=True)
|
if addr_fmt != "bech32m":
|
||||||
assert tx_hex == cc_tx_hex.hex()
|
# schnorr signatures are not deterministic
|
||||||
|
# any subsequent sign will produce different witness
|
||||||
|
assert tx_hex == cc_tx_hex
|
||||||
|
|
||||||
if psbt_v2:
|
if psbt_v2:
|
||||||
# check txn_modifiable properly set
|
# 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
|
assert mod & 4 == 0
|
||||||
|
|
||||||
# for PSBTv2 here we check if we correctly finalize
|
# 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"]
|
assert res[0]["allowed"]
|
||||||
txn_id = bitcoind.supply_wallet.sendrawtransaction(tx_hex)
|
txn_id = bitcoind.supply_wallet.sendrawtransaction(cc_tx_hex)
|
||||||
assert txn_id
|
assert txn_id
|
||||||
|
|
||||||
return doit
|
return doit
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.bitcoind
|
@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("sighash", [sh for sh in SIGHASH_MAP if sh != 'ALL'])
|
||||||
@pytest.mark.parametrize("num_outs", [1, 3, 5])
|
@pytest.mark.parametrize("num_outs", [1, 3, 5])
|
||||||
@pytest.mark.parametrize("num_ins", [2, 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.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("sighash", list(itertools.combinations(SIGHASH_MAP.keys(), 2)))
|
||||||
@pytest.mark.parametrize("num_outs", [2, 3, 5])
|
@pytest.mark.parametrize("num_outs", [2, 3, 5])
|
||||||
@pytest.mark.parametrize("psbt_v2", [True, False])
|
@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.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("num_outs", [5, 8])
|
||||||
@pytest.mark.parametrize("psbt_v2", [True, False])
|
@pytest.mark.parametrize("psbt_v2", [True, False])
|
||||||
def test_sighash_fullmix(addr_fmt, num_outs, psbt_v2, _test_single_sig_sighash):
|
def test_sighash_fullmix(addr_fmt, num_outs, 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?"
|
assert title == "OK TO SEND?"
|
||||||
# we do not understand change in taproot (taproot not supported)
|
# we do not understand change in taproot (taproot not supported)
|
||||||
assert "Consolidating" not in story
|
assert "Consolidating" not in story
|
||||||
assert "Change back" not in story
|
assert "Change back" in story
|
||||||
# but we should show address
|
# but we should show address
|
||||||
assert "to script" not in story
|
assert "to script" not in story
|
||||||
assert "tb1p" 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("chain", ["BTC", "XTN"])
|
||||||
@pytest.mark.parametrize("data", [
|
@pytest.mark.parametrize("data", [
|
||||||
# (out_style, amount, is_change)
|
# (out_style, amount, is_change)
|
||||||
|
[("p2tr", 999999, 1)] + [("p2tr", 888888, 0)] * 12,
|
||||||
[("p2pkh", 1000000, 0)] * 99,
|
[("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)] * 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,
|
def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign,
|
||||||
settings_set, txout_explorer, cap_story):
|
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
|
assert "OP_RETURN" in story
|
||||||
|
|
||||||
# EOF
|
# 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
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import pytest, os, shutil
|
import pytest, os, shutil
|
||||||
from helpers import B2A
|
from helpers import B2A, taptweak
|
||||||
|
|
||||||
|
|
||||||
def test_remote_exec(sim_exec):
|
def test_remote_exec(sim_exec):
|
||||||
@ -71,9 +71,13 @@ def test_public(sim_execfile):
|
|||||||
assert sk.hwif() == result
|
assert sk.hwif() == result
|
||||||
elif result[0] in '1mn':
|
elif result[0] in '1mn':
|
||||||
assert result == sk.address()
|
assert result == sk.address()
|
||||||
elif result[0:3] in { 'bc1', 'tb1' }:
|
elif result[0:4] in {'bc1q', 'tb1q'}:
|
||||||
h20 = sk.hash160()
|
h20 = sk.hash160()
|
||||||
assert result == bech32.encode(result[0:2], 0, h20)
|
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':
|
elif result[0] in '23':
|
||||||
h20 = hash160(b'\x00\x14' + sk.hash160())
|
h20 = hash160(b'\x00\x14' + sk.hash160())
|
||||||
assert h20 == decode_base58_checksum(result)[1:]
|
assert h20 == decode_base58_checksum(result)[1:]
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import pytest, struct
|
|||||||
from ckcc_protocol.protocol import MAX_TXN_LEN
|
from ckcc_protocol.protocol import MAX_TXN_LEN
|
||||||
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
|
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
|
||||||
from io import BytesIO
|
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 base58 import decode_base58
|
||||||
from bip32 import BIP32Node
|
from bip32 import BIP32Node
|
||||||
from constants import ADDR_STYLES, simulator_fixed_tprv
|
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,
|
invals=None, outvals=None, segwit_in=False, wrapped=False,
|
||||||
outstyles=['p2pkh'], psbt_hacker=None, change_outputs=[],
|
outstyles=['p2pkh'], psbt_hacker=None, change_outputs=[],
|
||||||
capture_scripts=None, add_xpub=None, op_return=None,
|
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()
|
psbt = BasicPSBT()
|
||||||
|
|
||||||
@ -61,7 +61,40 @@ def fake_txn(dev, pytestconfig):
|
|||||||
assert len(sec) == 33, "expect compressed"
|
assert len(sec) == 33, "expect compressed"
|
||||||
assert subpath[0:2] == '0/'
|
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
|
# UTXO that provides the funding for to-be-signed txn
|
||||||
supply = CTransaction()
|
supply = CTransaction()
|
||||||
@ -72,17 +105,6 @@ def fake_txn(dev, pytestconfig):
|
|||||||
)
|
)
|
||||||
supply.vin = [CTxIn(out_point, nSequence=0xffffffff)]
|
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))
|
supply.vout.append(CTxOut(int(input_amount if not invals else invals[i]), scr))
|
||||||
|
|
||||||
if segwit_in:
|
if segwit_in:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user