Compare commits
469 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c5907d812 | ||
|
|
7af4593e66 | ||
|
|
58a7a1c756 | ||
|
|
b9ff1a2a16 | ||
|
|
5d08ca08b0 | ||
|
|
ed5b7b1b63 | ||
|
|
76f6b9d0cb | ||
|
|
c36184e6bf | ||
|
|
28ce88b6a6 | ||
|
|
25c60cfc20 | ||
|
|
f080fe20ee | ||
|
|
82c081b9e0 | ||
|
|
cff4f3bf08 | ||
|
|
ce1fc62789 | ||
|
|
3f2e471ef5 | ||
|
|
e630dd614f | ||
|
|
bf0086bbc1 | ||
|
|
cd003034d7 | ||
|
|
d2f93b6a58 | ||
|
|
b0424125f9 | ||
|
|
1ee1b0eb36 | ||
|
|
0edb093201 | ||
|
|
1acd8fde75 | ||
|
|
cc58ff0b2b | ||
|
|
d8f9a88bcb | ||
|
|
8cf271ab4a | ||
|
|
6637c521ad | ||
|
|
ec92f13d23 | ||
|
|
fdafce2a42 | ||
|
|
716af73798 | ||
|
|
3cffd6089a | ||
|
|
302bf2a4c3 | ||
|
|
fb8e1047c9 | ||
|
|
c59b122bd3 | ||
|
|
0e2c7ce0d6 | ||
|
|
05d792cb79 | ||
|
|
398911c48a | ||
|
|
094b58bb38 | ||
|
|
52d7e4c541 | ||
|
|
395e31b34a | ||
|
|
5ed8f5a4a9 | ||
|
|
d3408165af | ||
|
|
fee2eac4fc | ||
|
|
119e9ef0fa | ||
|
|
ce79507a9a | ||
|
|
c7e0f6d18f | ||
|
|
6157c1d73d | ||
|
|
f4b242dcc8 | ||
|
|
1c95588b01 | ||
|
|
2908d95afe | ||
|
|
55d6cf8f2a | ||
|
|
647410fbf9 | ||
|
|
a820e52fce | ||
|
|
01ceb902c5 | ||
|
|
573a47ca98 | ||
|
|
ae39515ddf | ||
|
|
87a6956f06 | ||
|
|
239642e5f2 | ||
|
|
4ce6de51f8 | ||
|
|
79374d7a7d | ||
|
|
515b74b606 | ||
|
|
667e37e6f3 | ||
|
|
8342e289ea | ||
|
|
d5d7c4bb68 | ||
|
|
271599fde6 | ||
|
|
e914fd8f03 | ||
|
|
5c82bd684c | ||
|
|
64be61e562 | ||
|
|
84213cc250 | ||
|
|
36be797608 | ||
|
|
a9a90f30c6 | ||
|
|
a8d9eda6e5 | ||
|
|
bb4a1f9b30 | ||
|
|
42c8e3e8c0 | ||
|
|
aa72433207 | ||
|
|
c2edfc8da7 | ||
|
|
95cc006d7b | ||
|
|
3a5350d1ea | ||
|
|
e6b99ac352 | ||
|
|
e449ef32ff | ||
|
|
8cdc09d0af | ||
|
|
fcb88f5cdd | ||
|
|
9e0e187e49 | ||
|
|
413a91cf39 | ||
|
|
f9c0711dee | ||
|
|
904601f531 | ||
|
|
51e0a81e77 | ||
|
|
19f200a210 | ||
|
|
01c27fe568 | ||
|
|
440421731e | ||
|
|
54b1afb5eb | ||
|
|
387258f9a9 | ||
|
|
3792292b5d | ||
|
|
76f30b66c9 | ||
|
|
9e83ac99a4 | ||
|
|
5d9c1a553e | ||
|
|
421d3c1bf4 | ||
|
|
0484e3cd3b | ||
|
|
f7721cbbbe | ||
|
|
7b8a533265 | ||
|
|
f7c7275f12 | ||
|
|
373cf604b3 | ||
|
|
a62ee99fd5 | ||
|
|
a2d272336c | ||
|
|
f306f0a308 | ||
|
|
147e191456 | ||
|
|
acc58f9122 | ||
|
|
ac6525c10c | ||
|
|
2d002e3ef6 | ||
|
|
aaa91f6301 | ||
|
|
ab7b8f3a1f | ||
|
|
f3f3589613 | ||
|
|
f6807e0907 | ||
|
|
80ffbd65b7 | ||
|
|
0f92ac7f68 | ||
|
|
bf058bf756 | ||
|
|
68e8f45f96 | ||
|
|
89a2cb26ed | ||
|
|
79ecc09adc | ||
|
|
32068fd49f | ||
|
|
76802f5b64 | ||
|
|
d92c0e624c | ||
|
|
8e305739d8 | ||
|
|
995f3c7426 | ||
|
|
83ee24f326 | ||
|
|
2b333c9f5c | ||
|
|
255ca17905 | ||
|
|
46f4576746 | ||
|
|
d0c8316cf9 | ||
|
|
70ebd3365c | ||
|
|
cc4034bce7 | ||
|
|
37f4a1fd26 | ||
|
|
c84eb79fe6 | ||
|
|
d9c9098fa0 | ||
|
|
497478212a | ||
|
|
54fb9d6a08 | ||
|
|
b0f4d8b4a5 | ||
|
|
9d04ea8233 | ||
|
|
e1cbe655ef | ||
|
|
5f38ae20c1 | ||
|
|
1d3be6d17e | ||
|
|
eeabaef852 | ||
|
|
9259b03e15 | ||
|
|
35501077e7 | ||
|
|
b4a3304e35 | ||
|
|
a1e486a424 | ||
|
|
fc4daa6200 | ||
|
|
8552571c38 | ||
|
|
514b1ae2d3 | ||
|
|
52ae9f96cd | ||
|
|
a5caa48489 | ||
|
|
d05568059b | ||
|
|
fa6a36140c | ||
|
|
203c288de8 | ||
|
|
3f938c31e1 | ||
|
|
30b1c04259 | ||
|
|
e361408a28 | ||
|
|
ff3b385b0a | ||
|
|
fc2bfe26ae | ||
|
|
337ee8c463 | ||
|
|
a1eb3e901b | ||
|
|
8d8b496044 | ||
|
|
ca1b7b6345 | ||
|
|
e3b2e49bb0 | ||
|
|
449e889b1b | ||
|
|
cb539f6f18 | ||
|
|
807e7a00d2 | ||
|
|
512349b230 | ||
|
|
6dd8237123 | ||
|
|
7c8aceb753 | ||
|
|
30946eb1aa | ||
|
|
2dc1b97a92 | ||
|
|
b1b2edd72e | ||
|
|
1349b730b9 | ||
|
|
c0b9ea1087 | ||
|
|
8709817d47 | ||
|
|
4abf8b7077 | ||
|
|
f34faaf57d | ||
|
|
727beab889 | ||
|
|
8081732dea | ||
|
|
9841af46bb | ||
|
|
8d0d9af9a0 | ||
|
|
5f06e0949f | ||
|
|
785b8a479d | ||
|
|
3a1ad131e5 | ||
|
|
27843086ea | ||
|
|
44701a3e0a | ||
|
|
edc859300c | ||
|
|
64eb5cc3e0 | ||
|
|
3b8930b281 | ||
|
|
fa9bff5d36 | ||
|
|
89bdf2d499 | ||
|
|
4ea01f7d04 | ||
|
|
0ee23f5faf | ||
|
|
aa4e2595c2 | ||
|
|
c72b77ad3f | ||
|
|
5b67e2c3cb | ||
|
|
eb55a6dc11 | ||
|
|
a1342ac3bb | ||
|
|
0a0d93799a | ||
|
|
9a592179aa | ||
|
|
548c63b9e7 | ||
|
|
c323d2f5c2 | ||
|
|
ba3bc33611 | ||
|
|
d55a869550 | ||
|
|
fe7244c205 | ||
|
|
95a1b25c22 | ||
|
|
f3168475a1 | ||
|
|
b870f93eb0 | ||
|
|
ee39e91415 | ||
|
|
65e712121a | ||
|
|
f8622021f8 | ||
|
|
8f8b952223 | ||
|
|
1ca5f2e6a1 | ||
|
|
5bd7f4b351 | ||
|
|
6a47b29580 | ||
|
|
b30fc664c0 | ||
|
|
74c81c293f | ||
|
|
e22c6b6af0 | ||
|
|
292e580952 | ||
|
|
13888fd59b | ||
|
|
32272b93b3 | ||
|
|
0bc4c4f06e | ||
|
|
c2f1041927 | ||
|
|
5edc688e22 | ||
|
|
edc558493f | ||
|
|
b5d05359bd | ||
|
|
c3500f087d | ||
|
|
5f29751254 | ||
|
|
d2f4871b0e | ||
|
|
437961ca93 | ||
|
|
08df482119 | ||
|
|
041f803155 | ||
|
|
76d4e53b67 | ||
|
|
3cae633446 | ||
|
|
7da6d97934 | ||
|
|
bea721a8d6 | ||
|
|
8ac90d4c4d | ||
|
|
47cc8eaf04 | ||
|
|
5020ee546b | ||
|
|
e7f0a089f4 | ||
|
|
1a10e11bdc | ||
|
|
79dbfa9780 | ||
|
|
ee62d42fb3 | ||
|
|
39d8767949 | ||
|
|
b9deed6b80 | ||
|
|
ec9d6251e4 | ||
|
|
56180d7961 | ||
|
|
e4eaaf16fa | ||
|
|
6cb1173671 | ||
|
|
a7713e38db | ||
|
|
9c4257a51b | ||
|
|
78d5f2dc52 | ||
|
|
e7f42cf7f1 | ||
|
|
d8210a25a5 | ||
|
|
ad3b1e4ace | ||
|
|
2aa6c45daf | ||
|
|
dcc028fa2e | ||
|
|
e296bda059 | ||
|
|
a70dd4ecf2 | ||
|
|
8e90fe67b0 | ||
|
|
e01822c25e | ||
|
|
2d5e1234e8 | ||
|
|
d2aeedbd4d | ||
|
|
5b7bee406a | ||
|
|
4e77d38108 | ||
|
|
5132051d43 | ||
|
|
5fd2ee9404 | ||
|
|
b4d3e50f00 | ||
|
|
e202ab986b | ||
|
|
329c6c51bd | ||
|
|
4808ae45df | ||
|
|
b4e45057f3 | ||
|
|
c3a894192a | ||
|
|
734bcc78ca | ||
|
|
f3f3f1dbb9 | ||
|
|
b09fde8337 | ||
|
|
3d57e1f94b | ||
|
|
b8d702c0bc | ||
|
|
a12ac4b26d | ||
|
|
b1fdbae35c | ||
|
|
c4c2ae8c58 | ||
|
|
9998b576b7 | ||
|
|
d2dce34b9f | ||
|
|
6f6b245965 | ||
|
|
bc6b5a26c3 | ||
|
|
c6a0ab9bf5 | ||
|
|
20207f27db | ||
|
|
30457c98cc | ||
|
|
4439f3d7fe | ||
|
|
5a27fcc45a | ||
|
|
7a0e0a269b | ||
|
|
ef92efe67a | ||
|
|
e01e23aa63 | ||
|
|
a973c7edc1 | ||
|
|
7d3c9828ba | ||
|
|
962dd2ef3f | ||
|
|
8223765d1a | ||
|
|
70299186b9 | ||
|
|
3a986413e7 | ||
|
|
8169ec067b | ||
|
|
0ca4109693 | ||
|
|
754c4c3f64 | ||
|
|
54ccfe6cd6 | ||
|
|
f70b05ba6f | ||
|
|
6eb1ef272d | ||
|
|
f8d32a8272 | ||
|
|
789b87c33c | ||
|
|
638e7acc55 | ||
|
|
3090d220c0 | ||
|
|
37a677e6f9 | ||
|
|
0b20ef5360 | ||
|
|
f7cb241e9c | ||
|
|
e9b04ff408 | ||
|
|
32d28dea01 | ||
|
|
d4e8dfb9d5 | ||
|
|
68ffcecc30 | ||
|
|
0ac89511f1 | ||
|
|
f6639e45a7 | ||
|
|
7d84ba4c32 | ||
|
|
4254fbad5d | ||
|
|
24dccb694a | ||
|
|
dc73bdf253 | ||
|
|
d4e9549f24 | ||
|
|
cd344cb646 | ||
|
|
1ff76f30c7 | ||
|
|
9cd60433e7 | ||
|
|
f70b3247e4 | ||
|
|
34b7152e9f | ||
|
|
4c6a6b8b87 | ||
|
|
6852ce3a4d | ||
|
|
7a94a6f8e0 | ||
|
|
ab91463ec6 | ||
|
|
9d31349298 | ||
|
|
e04d391884 | ||
|
|
93e182724a | ||
|
|
b134cd9ed2 | ||
|
|
b9d8d8c0dc | ||
|
|
042b0eec3b | ||
|
|
ac472ab669 | ||
|
|
0dc88cc55b | ||
|
|
68792a7392 | ||
|
|
6507856861 | ||
|
|
3075cd07b2 | ||
|
|
60a6777b08 | ||
|
|
da80fd357b | ||
|
|
215357a9c2 | ||
|
|
2ebe376be2 | ||
|
|
a1d9652436 | ||
|
|
b6a3564a03 | ||
|
|
41b200bd6e | ||
|
|
e68123caa0 | ||
|
|
e209980630 | ||
|
|
339b2bce5d | ||
|
|
c10aff8a02 | ||
|
|
940aa3206a | ||
|
|
706466afe4 | ||
|
|
5a3b64e6c7 | ||
|
|
06a6796f11 | ||
|
|
a735a4c60e | ||
|
|
0427af8caa | ||
|
|
3c016004c7 | ||
|
|
d3d301f48c | ||
|
|
5c3be0caac | ||
|
|
ef2ff9793d | ||
|
|
d2ef212079 | ||
|
|
e6ac73b95b | ||
|
|
d95b1db57b | ||
|
|
77dda12301 | ||
|
|
16cf7abb17 | ||
|
|
9f31ed3f09 | ||
|
|
374315115d | ||
|
|
b335d92f4b | ||
|
|
2e30c9d576 | ||
|
|
1f7205186f | ||
|
|
97ee810a6d | ||
|
|
127f651a46 | ||
|
|
312432549d | ||
|
|
5d15fe4a27 | ||
|
|
65d3a9ffa1 | ||
|
|
607794aeb0 | ||
|
|
7173a92baf | ||
|
|
e6658312ae | ||
|
|
1bc2a95a1b | ||
|
|
58c809fc3f | ||
|
|
0073298061 | ||
|
|
2f18f17315 | ||
|
|
21cf0f97d0 | ||
|
|
17d6d58657 | ||
|
|
6985512be7 | ||
|
|
1e7cb8970f | ||
|
|
5bfb7388c9 | ||
|
|
a0a71e0393 | ||
|
|
4a6766579a | ||
|
|
b2fc03a4da | ||
|
|
dafb475ee5 | ||
|
|
b26ee00d27 | ||
|
|
6d4a4f4eaa | ||
|
|
7134f03eab | ||
|
|
132315f72b | ||
|
|
d2705983a6 | ||
|
|
3e33e310fd | ||
|
|
556d5084b2 | ||
|
|
ee572d89c1 | ||
|
|
e05a1a4898 | ||
|
|
b6d58a7468 | ||
|
|
a180fe4b20 | ||
|
|
cbfe72b8d5 | ||
|
|
ebf824a7f0 | ||
|
|
53b7aae325 | ||
|
|
718c0ca354 | ||
|
|
6a71550709 | ||
|
|
d42221e64e | ||
|
|
8fce5bae3a | ||
|
|
3ccd73b8e6 | ||
|
|
2794916ee8 | ||
|
|
4e3efee30f | ||
|
|
a5d0399c14 | ||
|
|
966f48dfa7 | ||
|
|
5df7583ab3 | ||
|
|
b756d43334 | ||
|
|
49643617a1 | ||
|
|
21daefdbea | ||
|
|
6e52fd8d72 | ||
|
|
5802827528 | ||
|
|
54f3fcdf95 | ||
|
|
3c8252d3ef | ||
|
|
500ac3588a | ||
|
|
ead0f009f2 | ||
|
|
8ed0e05f47 | ||
|
|
55ea38116c | ||
|
|
480b2d7cb0 | ||
|
|
4b93075df3 | ||
|
|
51c1b1d056 | ||
|
|
dc8732ad58 | ||
|
|
ed5f54e32b | ||
|
|
23d252f79b | ||
|
|
e794ecf979 | ||
|
|
5b08eaead7 | ||
|
|
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 |
@ -28,11 +28,9 @@ has been automated using Docker. Steps are as follows:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/Coldcard/firmware.git
|
git clone https://github.com/Coldcard/firmware.git
|
||||||
cd firmware
|
git checkout 2023-12-21T1526-v5.2.2
|
||||||
# DOWNLOAD https://coldcard.com/downloads
|
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
|
||||||
# get a copy of binary into ./releases/2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
cd firmware/stm32
|
||||||
git checkout 2026-03-05T2052-v5.5.0
|
|
||||||
cd stm32
|
|
||||||
make -f MK4-Makefile repro
|
make -f MK4-Makefile repro
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -3,32 +3,14 @@
|
|||||||
These docs are meant for you hackers out there... but also for anyone who
|
These docs are meant for you hackers out there... but also for anyone who
|
||||||
wants to understand why it's safe to put your moneys into Coldcard.
|
wants to understand why it's safe to put your moneys into Coldcard.
|
||||||
|
|
||||||
- [`security-model.md`](security-model.md) The COLDCARD Mk4/Mk5/Q security model.
|
|
||||||
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
|
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
|
||||||
- [`secure-elements.md`](secure-elements.md) How the dual secure elements work together.
|
|
||||||
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
|
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
|
||||||
- [`memory-map.md`](memory-map.md) Memory map highlights
|
- [`memory-map.md`](memory-map.md) Memory map highlights
|
||||||
- [`notes-on-repro.md`](notes-on-repro.md) Detailed breakdown of the reproducible build process.
|
|
||||||
- [`upgrade-recovery.md`](upgrade-recovery.md) Firmware upgrade and recovery process.
|
|
||||||
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
|
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
|
||||||
- [`temporary-seeds.md`](temporary-seeds.md) Temporary (ephemeral) seeds and the Seed Vault.
|
|
||||||
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
|
||||||
- [`key-teleport.md`](key-teleport.md) Key Teleport: encrypted transfer of seeds and secrets between Q devices.
|
|
||||||
- [`spending-policy.md`](spending-policy.md) Spending policy: autonomous signing with configurable limits.
|
|
||||||
- [`microsd-2fa.md`](microsd-2fa.md) Using a MicroSD card as a second factor for login.
|
|
||||||
- [`web2fa.md`](web2fa.md) Web 2FA authentication.
|
|
||||||
- [`bip85-passwords.md`](bip85-passwords.md) Deriving deterministic passwords via BIP-85.
|
|
||||||
- [`msg-signing.md`](msg-signing.md) COLDCARD message signing.
|
|
||||||
- [`proof-of-reserves-bip-322.md`](proof-of-reserves-bip-322.md) BIP-322 generic signed message format and proof of reserves.
|
|
||||||
- [`generic-wallet-export.md`](generic-wallet-export.md) Generic JSON wallet export file format.
|
|
||||||
- [`bip-21-extensions.md`](bip-21-extensions.md) Coldcard's BIP-21 URI extensions, including multisig ownership address check.
|
|
||||||
- [`nfc-coldcard.md`](nfc-coldcard.md) NFC support on Coldcard Mk4 and Q.
|
|
||||||
- [`nfc-pushtx.md`](nfc-pushtx.md) NFC Push Transaction: broadcast a signed transaction via your phone.
|
|
||||||
- [`usb-batteries.md`](usb-batteries.md) Using USB battery packs with Coldcard.
|
|
||||||
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
|
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
|
||||||
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
|
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
|
||||||
- [`bitcoin-core2of2desc.md`](bitcoin-core2of2desc.md) Airgapped 2-of-2 multisig with Bitcoin Core using descriptors.
|
|
||||||
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
|
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
|
||||||
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
|
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
|
||||||
|
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
||||||
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.
|
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,13 @@ according to [BIP-85 PWD BASE64](https://github.com/bitcoin/bips/blob/master/bip
|
|||||||
Generated passwords can be sent as keystrokes via USB to the host computer,
|
Generated passwords can be sent as keystrokes via USB to the host computer,
|
||||||
effectively using Coldcard as specialized password manager.
|
effectively using Coldcard as specialized password manager.
|
||||||
|
|
||||||
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard
|
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard Mk4
|
||||||
can also type them into a computer by emulating a USB keyboard, and simulating the
|
can also type them into a computer by emulating a USB keyboard, and simulating the
|
||||||
keystrokes needed to type the password.
|
keystrokes needed to type the password.
|
||||||
|
|
||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
* Coldcard Mk4 or Mk5 (firmware 5.0.5 or newer), or any Q
|
* Coldcard Mk4 with version 5.0.5 or newer
|
||||||
* USB-C with data link (won't work with power only cable from Coinkite)
|
* USB-C with data link (won't work with power only cable from Coinkite)
|
||||||
|
|
||||||
## Type Passwords over USB
|
## Type Passwords over USB
|
||||||
@ -32,13 +32,11 @@ to exit. Exiting from "Type Passwords" will cause Coldcard to turn off keyboard
|
|||||||
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
|
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
|
||||||
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
|
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
|
||||||
3. Screen shows generated password, path, and entropy from which password was derived
|
3. Screen shows generated password, path, and entropy from which password was derived
|
||||||
4. A few different options are available at this point (on Mk; on Q the NFC and
|
4. A few different options are available at this point:
|
||||||
QR buttons are used instead of (3)/(4)):
|
1. press 1 to save password backup file on MicroSD card (cleartext!)
|
||||||
1. press (1) to save password backup file on MicroSD card (cleartext!)
|
2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation)
|
||||||
2. press (2) to save to Virtual Disk (only when available)
|
3. press 3 to view password as QR code
|
||||||
3. press (3) to send over NFC (only appears when NFC is enabled)
|
4. press 4 to send over NFC (only appears when NFC is enabled)
|
||||||
4. press (4) to view password as QR code
|
|
||||||
5. press (6) to send keystrokes over USB (this enables keyboard emulation, sends keystrokes + enter, then disables keyboard emulation)
|
|
||||||
|
|
||||||
## Keyboard language settings
|
## Keyboard language settings
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,9 @@ wallet systems, but we also have a file format for general purpose
|
|||||||
exports, which we hope future wallet makers will leverage.
|
exports, which we hope future wallet makers will leverage.
|
||||||
|
|
||||||
It contains master XPUB, XFP for that, and derived values for the top hardened
|
It contains master XPUB, XFP for that, and derived values for the top hardened
|
||||||
position of the single-signature schemes BIP44, BIP49 and BIP84, plus the
|
position of BIP44, BIP84 and BIP49.
|
||||||
multisig schemes BIP48 (`bip48_1` = `.../1h` P2SH-P2WSH and `bip48_2` = `.../2h` P2WSH).
|
|
||||||
When the account number is zero, a BIP45 (`m/45h`) multisig section is also included
|
|
||||||
(it is omitted for non-zero accounts, as in the example below).
|
|
||||||
|
|
||||||
The feature can be found here: _Advanced/Tools > Export Wallet > Generic JSON_
|
The feature can be found here: _Advanced > MicroSD > Export Wallet > Generic JSON_
|
||||||
|
|
||||||
Please contact us (or better yet, make a pull request), if you need something
|
Please contact us (or better yet, make a pull request), if you need something
|
||||||
more in this file.
|
more in this file.
|
||||||
@ -21,51 +18,32 @@ Here is an example, produced by the Simulator for account number 123.
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"chain": "BTC",
|
"chain": "XTN",
|
||||||
"xfp": "0F056943",
|
"xfp": "0F056943",
|
||||||
|
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
|
||||||
"account": 123,
|
"account": 123,
|
||||||
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
|
|
||||||
"bip44": {
|
"bip44": {
|
||||||
|
"deriv": "m/44'/1'/123'",
|
||||||
|
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
|
||||||
"name": "p2pkh",
|
"name": "p2pkh",
|
||||||
"xfp": "5F898064",
|
"xfp": "B7908B26",
|
||||||
"deriv": "m/44h/0h/123h",
|
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo"
|
||||||
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
|
|
||||||
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
|
|
||||||
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
|
|
||||||
},
|
},
|
||||||
"bip49": {
|
"bip49": {
|
||||||
"name": "p2sh-p2wpkh",
|
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52",
|
||||||
"xfp": "A748B1FC",
|
"deriv": "m/49'/1'/123'",
|
||||||
"deriv": "m/49h/0h/123h",
|
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB",
|
||||||
"xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
|
"name": "p2wpkh-p2sh",
|
||||||
"desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
|
"xfp": "CEE1D809",
|
||||||
"_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
|
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T"
|
||||||
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
|
|
||||||
},
|
},
|
||||||
"bip84": {
|
"bip84": {
|
||||||
|
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
|
||||||
|
"deriv": "m/84'/1'/123'",
|
||||||
|
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
|
||||||
"name": "p2wpkh",
|
"name": "p2wpkh",
|
||||||
"xfp": "2C5207AA",
|
"xfp": "78CF94E5",
|
||||||
"deriv": "m/84h/0h/123h",
|
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"
|
||||||
"xpub": "xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16",
|
|
||||||
"desc": "wpkh([0f056943/84h/0h/123h]xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16/<0;1>/*)#yk84tprf",
|
|
||||||
"_pub": "zpub6rF34DckutvQCjaE8kW4cya7vT2t2jeQuSUUMhLHFw5F8zY8XwZZvAbuJHVgHU7QQF8nCKoW6NANoG4FoJWTdmtDhxJYLt3ZjUK5RqUSMdF",
|
|
||||||
"first": "bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an"
|
|
||||||
},
|
|
||||||
"bip48_1": {
|
|
||||||
"name": "p2sh-p2wsh",
|
|
||||||
"xfp": "845A3542",
|
|
||||||
"deriv": "m/48h/0h/123h/1h",
|
|
||||||
"xpub": "xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La",
|
|
||||||
"desc": "sh(wsh(sortedmulti(M,[0f056943/48h/0h/123h/1h]xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La/0/*,...)))",
|
|
||||||
"_pub": "Ypub6kUxqLsLQa4M43jXJ3ux8SyP6t8dD5ZbpZmxkpP1jc7VXLW4RkZ76ouEPAZ1gMgiiXzrYFPfzBC8MfjYaoTxfTm1zUfdeqiTnHDX8raCfeg"
|
|
||||||
},
|
|
||||||
"bip48_2": {
|
|
||||||
"name": "p2wsh",
|
|
||||||
"xfp": "2A01C6B0",
|
|
||||||
"deriv": "m/48h/0h/123h/2h",
|
|
||||||
"xpub": "xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF",
|
|
||||||
"desc": "wsh(sortedmulti(M,[0f056943/48h/0h/123h/2h]xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF/0/*,...))",
|
|
||||||
"_pub": "Zpub75KE91YFZFbpup5PMS2AxgZCKRWnozdEWTEmaUKGQysSmUaKuL5WYyf2kk5UNQhhupRnddQe9GzST7crvfLoRTHTg6KtDPZiFjxBJobzcUz"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -73,14 +51,7 @@ Here is an example, produced by the Simulator for account number 123.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
|
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
|
||||||
to be the first (non-change) receive address for the wallet. It is only present on the
|
to be the first (non-change) receive address for the wallet.
|
||||||
single-signature sections (`bip44`, `bip49`, `bip84`); multisig sections omit it.
|
|
||||||
|
|
||||||
1a. Each section includes a `desc` field: a ready-to-import Bitcoin output descriptor
|
|
||||||
(with `#checksum`). Single-sig descriptors use the `<0;1>/*` multipath form. Multisig
|
|
||||||
sections (`bip48_1`, `bip48_2`, and `bip45` when present) emit a `sortedmulti(...)`
|
|
||||||
template with `M` and a trailing `...` as placeholders, to be completed with your
|
|
||||||
threshold and the other co-signers' keys.
|
|
||||||
|
|
||||||
2. The user may specify any value (up to 9999) for the account number, and it's meant to
|
2. The user may specify any value (up to 9999) for the account number, and it's meant to
|
||||||
segregate funds into sub-wallets. Don't assume it's zero.
|
segregate funds into sub-wallets. Don't assume it's zero.
|
||||||
@ -88,8 +59,8 @@ segregate funds into sub-wallets. Don't assume it's zero.
|
|||||||
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
|
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
|
||||||
(`0F056943` in this example) is the root of the subkey paths found in the file, and
|
(`0F056943` in this example) is the root of the subkey paths found in the file, and
|
||||||
you must include the full derivation path from master. So based on this example,
|
you must include the full derivation path from master. So based on this example,
|
||||||
to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
|
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section
|
||||||
of your PSBT would need to specify `(m=0F056943)/84'/0'/123'/0/0`.
|
of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`.
|
||||||
|
|
||||||
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
|
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
|
||||||
a specific address format.
|
a specific address format.
|
||||||
|
|||||||
@ -77,6 +77,7 @@
|
|||||||
- derivation path for each cosigner must be known and consistent with PSBT
|
- derivation path for each cosigner must be known and consistent with PSBT
|
||||||
- XFP values (fingerprints) MUST be unique for each of the co-signers
|
- XFP values (fingerprints) MUST be unique for each of the co-signers
|
||||||
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
||||||
|
- for taproot multisig (musig) limitations check musig.md
|
||||||
|
|
||||||
### BIP-67
|
### BIP-67
|
||||||
|
|
||||||
@ -134,7 +135,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
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ directly from python programs.
|
|||||||
|
|
||||||
| Start | Size | Notes
|
| Start | Size | Notes
|
||||||
|---------------|-----------|--------------------------
|
|---------------|-----------|--------------------------
|
||||||
| 0x0800 0000 | 112k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
|
| 0x0800 0000 | 128k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
|
||||||
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
|
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
|
||||||
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
|
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
|
||||||
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)
|
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)
|
||||||
|
|||||||
@ -247,14 +247,13 @@
|
|||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Nunchuk
|
Nunchuk
|
||||||
Bull Bitcoin
|
Bull Bitcoin
|
||||||
Blue Wallet
|
Zeus
|
||||||
Electrum Wallet
|
Electrum Wallet
|
||||||
Wasabi Wallet
|
Wasabi Wallet
|
||||||
Fully Noded
|
Fully Noded
|
||||||
Unchained
|
Unchained
|
||||||
Theya
|
Theya
|
||||||
Bitcoin Safe
|
Bitcoin Safe
|
||||||
Zeus
|
|
||||||
Samourai Postmix
|
Samourai Postmix
|
||||||
Samourai Premix
|
Samourai Premix
|
||||||
Descriptor
|
Descriptor
|
||||||
@ -280,14 +279,13 @@
|
|||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Nunchuk
|
Nunchuk
|
||||||
Bull Bitcoin
|
Bull Bitcoin
|
||||||
Blue Wallet
|
Zeus
|
||||||
Electrum Wallet
|
Electrum Wallet
|
||||||
Wasabi Wallet
|
Wasabi Wallet
|
||||||
Fully Noded
|
Fully Noded
|
||||||
Unchained
|
Unchained
|
||||||
Theya
|
Theya
|
||||||
Bitcoin Safe
|
Bitcoin Safe
|
||||||
Zeus
|
|
||||||
Samourai Postmix
|
Samourai Postmix
|
||||||
Samourai Premix
|
Samourai Premix
|
||||||
Descriptor
|
Descriptor
|
||||||
@ -362,7 +360,6 @@
|
|||||||
Enable
|
Enable
|
||||||
User Management [MAYBE]
|
User Management [MAYBE]
|
||||||
Paper Wallets
|
Paper Wallets
|
||||||
WIF Store
|
|
||||||
NFC Tools [IF NFC ENABLED]
|
NFC Tools [IF NFC ENABLED]
|
||||||
Sign PSBT
|
Sign PSBT
|
||||||
Show Address
|
Show Address
|
||||||
@ -633,14 +630,13 @@
|
|||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Nunchuk
|
Nunchuk
|
||||||
Bull Bitcoin
|
Bull Bitcoin
|
||||||
Blue Wallet
|
Zeus
|
||||||
Electrum Wallet
|
Electrum Wallet
|
||||||
Wasabi Wallet
|
Wasabi Wallet
|
||||||
Fully Noded
|
Fully Noded
|
||||||
Unchained
|
Unchained
|
||||||
Theya
|
Theya
|
||||||
Bitcoin Safe
|
Bitcoin Safe
|
||||||
Zeus
|
|
||||||
Samourai Postmix
|
Samourai Postmix
|
||||||
Samourai Premix
|
Samourai Premix
|
||||||
Descriptor
|
Descriptor
|
||||||
@ -665,14 +661,13 @@
|
|||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Nunchuk
|
Nunchuk
|
||||||
Bull Bitcoin
|
Bull Bitcoin
|
||||||
Blue Wallet
|
Zeus
|
||||||
Electrum Wallet
|
Electrum Wallet
|
||||||
Wasabi Wallet
|
Wasabi Wallet
|
||||||
Fully Noded
|
Fully Noded
|
||||||
Unchained
|
Unchained
|
||||||
Theya
|
Theya
|
||||||
Bitcoin Safe
|
Bitcoin Safe
|
||||||
Zeus
|
|
||||||
Samourai Postmix
|
Samourai Postmix
|
||||||
Samourai Premix
|
Samourai Premix
|
||||||
Descriptor
|
Descriptor
|
||||||
@ -699,7 +694,6 @@
|
|||||||
Coldcard Backup
|
Coldcard Backup
|
||||||
Restore Seed XOR
|
Restore Seed XOR
|
||||||
Paper Wallets
|
Paper Wallets
|
||||||
WIF Store
|
|
||||||
NFC Tools [IF NFC ENABLED]
|
NFC Tools [IF NFC ENABLED]
|
||||||
Sign PSBT
|
Sign PSBT
|
||||||
Show Address
|
Show Address
|
||||||
|
|||||||
27
docs/miniscript.md
Normal file
27
docs/miniscript.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Miniscript
|
||||||
|
|
||||||
|
**COLDCARD<sup>®</sup>** `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
|
||||||
|
* both keys with key origin info `[xfp/p/a/t/h]xpub/<0;1>/*` & blinded keys `xpub/<2;3>/*` allowed
|
||||||
|
* use of blinded keys for co-signers requires PSBT provider to supply path from current key fingerprint
|
||||||
|
* maximum number of keys allowed in segwit v0 miniscript is 20
|
||||||
|
* check MiniTapscript limitations in `docs/taproot.md`
|
||||||
@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
|
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
|
||||||
sign messages provided via specially crafted file on SD card or Vdisk,
|
sign messages provided via specially crafted file on SD card or Vdisk,
|
||||||
and NFC-equipped models (Mk4, Mk5, and Q) can also sign messages sent to COLDCARD via NFC.
|
and Mk4 can also sign messages sent to COLDCARD via NFC.
|
||||||
The resulting signature can be returned over SD card/Vdisk, NFC, or — on Q — as a QR code.
|
|
||||||
|
|
||||||
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
|
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
|
||||||
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
|
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
|
||||||
From version `5.1.0` correct header byte is used for corresponding script type.
|
From Mk4 `5.1.0` correct header byte is used for corresponding script type.
|
||||||
|
|
||||||
### Verification
|
### Verification
|
||||||
|
|
||||||
From version `5.1.0` users can verify signed messages directly on the device.
|
From COLDCARD Mk4 version `5.1.0` users can verify signed messages directly on the device.
|
||||||
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
|
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
|
||||||
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
|
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
|
||||||
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
|
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
|
||||||
@ -22,7 +21,7 @@ Bitcoin core can only verify P2PKH.
|
|||||||
|
|
||||||
## Signed Exports
|
## Signed Exports
|
||||||
|
|
||||||
From version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
|
From Mk4 version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
|
||||||
If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
|
If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
|
||||||
|
|
||||||
### Message construction and signature file format
|
### Message construction and signature file format
|
||||||
@ -40,6 +39,8 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
|||||||
-----END BITCOIN SIGNATURE-----
|
-----END BITCOIN SIGNATURE-----
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### What is signed
|
||||||
|
|
||||||
### What Is Signed
|
### What Is Signed
|
||||||
|
|
||||||
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
|
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
|
||||||
@ -59,4 +60,14 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
|||||||
6. **Multisig exports:** public keys are encoded as P2PKH address for all multisg signature exports
|
6. **Multisig exports:** public keys are encoded as P2PKH address for all multisg signature exports
|
||||||
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
|
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
|
||||||
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
|
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
|
||||||
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
|
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
|
||||||
|
|
||||||
|
### What is NOT signed
|
||||||
|
|
||||||
|
Multisig exports and generic multisig xpub exports are not signed. It is not clear at this point
|
||||||
|
whether to sign these exports with some generic single signature key (i.e. `m/44'/<coin_type>'/0'/0/0`)
|
||||||
|
or with our portion (leg) of script. In both cases script type (address format) would not match as multisignature
|
||||||
|
message signing is not standardized.
|
||||||
|
|
||||||
|
1. **Multisig exports**
|
||||||
|
2. **Generic multisig exports**
|
||||||
40
docs/musig.md
Normal file
40
docs/musig.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# MuSig2
|
||||||
|
|
||||||
|
**COLDCARD<sup>®</sup>** `EDGE` versions support [MuSig2](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) from version `6.5.0X` & `6.5.0QX`.
|
||||||
|
|
||||||
|
COLDCARD implements all following BIPs, further restricting their scope (read more in Limitations section):
|
||||||
|
* PSBT fields [BIP-373](https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki)
|
||||||
|
* `musig()` descriptor key expression [BIP-390](https://github.com/bitcoin/bips/blob/master/bip-0390.mediawiki)
|
||||||
|
* Derivation Scheme for MuSig2 Aggregate Keys [BIP-328](https://github.com/bitcoin/bips/blob/master/bip-0328.mediawiki)
|
||||||
|
|
||||||
|
|
||||||
|
### Why MuSig2?
|
||||||
|
* higher level of **privacy** than OP_CHECKSIGADD. MuSig2 Taproot outputs are indistinguishable for a blockchain observer from regular, single-signer Taproot outputs even though they are actually controlled by multiple signers
|
||||||
|
* **on-chain footprint** of a MuSig2 Taproot output is essentially a single BIP340 public key. This is more compact and has lower verification cost than each signer providing an individual public key and signature
|
||||||
|
|
||||||
|
|
||||||
|
### Limitations:
|
||||||
|
* COLDCARD must stay powered up between 1st and 2nd round as necessary musig session data are stored in volatile memory only
|
||||||
|
* `musig()` can only be used inside `tr()` expression as key expression
|
||||||
|
* cannot be nested within another `musig()` expression
|
||||||
|
* only one own key in `musig()` expression
|
||||||
|
* `musig(KEY, KEY, ..., KEY)/<NUM;NUM;...>/*`
|
||||||
|
* all `KEY`s MUST be unique - no repeated keys
|
||||||
|
* `KEY` expression MUST be extended key (not plain pubkey)
|
||||||
|
* `KEY` expression cannot contain child derivation, only `musig()` expression can contain derivation steps
|
||||||
|
* `KEY`s are sorted prior to aggregation
|
||||||
|
* hardened derivation not allowed for `musig()` expression
|
||||||
|
* derivation must end with `*` - only ranged `musig()` expression allowed, if `musig()` derivation is omitted, `/<0;1>/*` is implied
|
||||||
|
* PSBT must contain all the data required by BIP-373
|
||||||
|
* COLDCARD strictly differentiate between 1st & 2nd MuSig2 round. If COLDCARD provides nonce, it will not attempt to sign even if it could (a.k.a enough nonces from cosigners are available).
|
||||||
|
To provide both nonce(s) & signature(s) signing needs to be preformed twice.
|
||||||
|
* keys from WIF Store cannot be used for MuSig2 signing
|
||||||
|
* `musig()` key expression is not allowed inside `multi_a` & `sortedmulti_a` fragments, use `thresh` instead
|
||||||
|
* inputs that are in different musig rounds in same PSBT are not allowed
|
||||||
|
* transaction cannot be modified after 1st musig round was initiated as that would change musig session
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Following policy is example how to do threshold multisig with MuSig2 (and Taptree) even thought MuSig2 is not a native threshold scheme.
|
||||||
|
|
||||||
|
`tr(musig(@0,@1,@2),{{pk(musig(@0,@1)),pk(musig(@1,@2))},pk(musig(@0,@2))})`
|
||||||
@ -1,20 +1,10 @@
|
|||||||
# NFC and Coldcard
|
# NFC and Coldcard Mk4
|
||||||
|
|
||||||
(Applies to NFC-equipped models: Mk4, Mk5, and Q)
|
(Applies to Coldcard Mk4 only)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The NFC antenna location depends on the hardware:
|
Mk4 NFC antenna is centered under number `8` on the keypad. Before using NFC,
|
||||||
|
|
||||||
- **Mk4**: a PCB trace loop, centered under number `8` on the keypad.
|
|
||||||
- **Mk5**: a discrete coil (`L6`) in the **top-right corner** of the device
|
|
||||||
- **Q1**: a flexible "sticker" antenna behind the display. The green LED below the
|
|
||||||
bottom-right of the display (`D12`) lights up while an NFC transfer is active —
|
|
||||||
it is the activity indicator, not the antenna.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Before using NFC,
|
|
||||||
it is important to locate the position of NFC antenna on your device and point it
|
it is important to locate the position of NFC antenna on your device and point it
|
||||||
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
|
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
|
||||||
that has NFC antenna located at the top right edge. The NFC smartphone antenna
|
that has NFC antenna located at the top right edge. The NFC smartphone antenna
|
||||||
@ -46,7 +36,7 @@ in general. Good interoperability is critical with radio standards.
|
|||||||
|
|
||||||
## Lower Layers
|
## Lower Layers
|
||||||
|
|
||||||
The Coldcard has a chip that acts as a Type 5 NFC tag. The
|
The Coldcard Mk4 has an chip that acts as a Type 5 NFC tag. The
|
||||||
radio standard is called "NFC-V" or ISO-15693, and operates on a
|
radio standard is called "NFC-V" or ISO-15693, and operates on a
|
||||||
13.56 Mhz carrier wave.
|
13.56 Mhz carrier wave.
|
||||||
|
|
||||||
@ -68,13 +58,9 @@ unless we are actively sharing something. We disable the "energy
|
|||||||
harvesting" features of the chip, so it will not do anything when
|
harvesting" features of the chip, so it will not do anything when
|
||||||
the Coldcard is powered-down, regardless of the NFC setting.
|
the Coldcard is powered-down, regardless of the NFC setting.
|
||||||
|
|
||||||
If the above is not enough for you, the antenna can be destroyed:
|
If the above is not enough for you, the antenna can be destroyed
|
||||||
|
by cutting the trace labeled "NFC" inside the hole for the MicroSD
|
||||||
- **Mk4**: cut the trace labeled "NFC" inside the hole for the MicroSD card,
|
card. Use the point of a sharp knife to cut and peel up the trace.
|
||||||
using the point of a sharp knife to cut and peel up the trace.
|
|
||||||
- **Mk5**: has no such trace — its antenna is the discrete coil `L6` in the
|
|
||||||
top-right corner, which would have to be physically removed instead.
|
|
||||||
- **Q1**: cut the trace labeled "NFC DATA" under the batteries.
|
|
||||||
|
|
||||||
The NFC traffic is not encrypted and is subject to eavesdropping.
|
The NFC traffic is not encrypted and is subject to eavesdropping.
|
||||||
While the NFC feature is active, your Coldcard can be uniquely
|
While the NFC feature is active, your Coldcard can be uniquely
|
||||||
|
|||||||
@ -34,7 +34,7 @@ The COLDCARD needs a URL prefix. To that it appends some values:
|
|||||||
- when RegTest is enabled, the value will be `XRT`
|
- when RegTest is enabled, the value will be `XRT`
|
||||||
|
|
||||||
We provide a few default URL values to our customers, including one backend we
|
We provide a few default URL values to our customers, including one backend we
|
||||||
will operate on `coldcard.com`. The URL can also be directly entered by the
|
will operate on `colcard.com`. The URL can also be directly entered by the
|
||||||
customer. On the Q, it can be scanned from a QR code.
|
customer. On the Q, it can be scanned from a QR code.
|
||||||
|
|
||||||
For COLDCARD backend, the url used is:
|
For COLDCARD backend, the url used is:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 258 KiB |
@ -11,17 +11,11 @@ The entrypoint makefile for repro builds.
|
|||||||
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
|
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
|
||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
repro: submods-match code-committed
|
|
||||||
repro:
|
repro:
|
||||||
docker build -t coldcard-build - < dockerfile.build
|
docker build -t coldcard-build - < dockerfile.build
|
||||||
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(HW_MODEL) $(PARENT_MKFILE))
|
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(MK_NUM))
|
||||||
```
|
```
|
||||||
|
|
||||||
`$(HW_MODEL)` is the model string (e.g. `mk4`, `q1`) and `$(PARENT_MKFILE)` is the
|
|
||||||
top-level makefile being used (`MK-Makefile` or `Q1-Makefile`). The `submods-match`
|
|
||||||
and `code-committed` prerequisites ensure the submodules and working tree are clean
|
|
||||||
before a repro build.
|
|
||||||
|
|
||||||
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
|
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
|
||||||
|
|
||||||
```stdout
|
```stdout
|
||||||
@ -67,19 +61,19 @@ Successfully installed signit-1.0
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
+ make -f MK-Makefile setup
|
+ make -f MK4-Makefile setup
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
+ make -f MK-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
|
+ make -f MK4-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
signit sign -b l-port/build-COLDCARD_MK4 -m mk4 5.0.7 -o firmware-signed.bin
|
signit sign -b l-port/build-COLDCARD_MK4 -m 4 5.0.7 -o firmware-signed.bin
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
signit sign -m mk4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
|
signit sign -m 4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
|
||||||
You don't have that key (1), so using key zero instead!
|
You don't have that key (1), so using key zero instead!
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -102,7 +96,7 @@ production.bin
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
+ make -f MK-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
|
+ make -f MK4-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -189,7 +183,7 @@ To summarize `check-repro`:
|
|||||||
|
|
||||||
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
|
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
|
||||||
|
|
||||||
- `check` (cli/signit.py: Line 176-243) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
|
- `check` (cli/signit.py: Line 176-241) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
|
||||||
|
|
||||||
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.
|
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.
|
||||||
|
|
||||||
|
|||||||
@ -9,104 +9,56 @@ BIP-322 specification: <https://github.com/bitcoin/bips/blob/master/bip-0322.med
|
|||||||
COLDCARD accepts a specially crafted PSBT file to sign as BIP-322 Proof of Reserves. The PSBT
|
COLDCARD accepts a specially crafted PSBT file to sign as BIP-322 Proof of Reserves. The PSBT
|
||||||
must meet all these requirements:
|
must meet all these requirements:
|
||||||
|
|
||||||
* COLDCARD acts as a BIP-322 PSBT signer. It validates the BIP-322 `to_sign`
|
|
||||||
transaction, shows the message from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE`, and
|
|
||||||
adds signatures to the PSBT. Finalizing and encoding the final BIP-322
|
|
||||||
signature string is the responsibility of the finalizer.
|
|
||||||
* PSBT MUST include `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09`; the value is
|
|
||||||
the exact message shown to the user and signed by BIP-322.
|
|
||||||
* PSBT requires `PSBT_IN_BIP32_DERIVATION` for each input
|
* PSBT requires `PSBT_IN_BIP32_DERIVATION` for each input
|
||||||
* P2SH wrapped segwit addresses MUST have proper redeem script in PSBT: `PSBT_IN_REDEEM_SCRIPT`
|
* P2SH wrapped segwit addresses MUST have proper redeem script in PSBT: `PSBT_IN_REDEEM_SCRIPT`
|
||||||
* P2WSH segwit addresses MUST have proper witness script in PSBT: `PSBT_IN_WITNESS_SCRIPT`
|
* P2WSH segwit addresses MUST have proper witness script in PSBT: `PSBT_IN_WITNESS_SCRIPT`
|
||||||
* PSBT (`to_sign`) MUST have at least one input.
|
* First (0th) input in `to_sign` transaction MUST have full (pre-segwit) UTXO (`PSBT_IN_NON_WITNESS_UTXO`) a.k.a `to_spend`.
|
||||||
* First (0th) input of `to_sign` MUST spend the BIP-322 `to_spend` output.
|
* First (0th) input in `to_sign` `PSBT_IN_NON_WITNESS_UTXO` transaction (`to_spend`) is as defined
|
||||||
* Input 0 MUST include one of `PSBT_IN_NON_WITNESS_UTXO` or `PSBT_IN_WITNESS_UTXO`.
|
in [BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full):
|
||||||
* When input 0 provides `PSBT_IN_WITNESS_UTXO`, COLDCARD reconstructs the
|
|
||||||
expected `to_spend` txid from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and the
|
|
||||||
witness UTXO scriptPubKey.
|
|
||||||
* When input 0 provides `PSBT_IN_NON_WITNESS_UTXO`, it MUST be the BIP-322
|
|
||||||
`to_spend` transaction as defined in
|
|
||||||
[BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full):
|
|
||||||
* 1 input, 1 output
|
* 1 input, 1 output
|
||||||
* output nValue is 0
|
* output nValue is 0
|
||||||
* input prevout hash is 0
|
* input prevout hash is 0
|
||||||
* input prevout n is 0xffffffff
|
* input prevout n is 0xffffffff
|
||||||
* input scriptSig is `OP_0 PUSH32 message_hash`
|
* input scriptSig is `OP_0 PUSH32 message_hash`
|
||||||
|
* PSBT (`to_sign`) MUST have at least one input & first input MUST be `to_spend` full txn
|
||||||
* PSBT (`to_sign`) MUST only have one output with null-data `OP_RETURN`
|
* PSBT (`to_sign`) MUST only have one output with null-data `OP_RETURN`
|
||||||
* `to_sign` transaction version MUST be 0 or 2.
|
|
||||||
* Optionally inputs can be added to `to_sign` for Proof of Reserve signing.
|
* Optionally inputs can be added to `to_sign` for Proof of Reserve signing.
|
||||||
* PSBT MUST be version 0 or 2.
|
* PSBT MUST be version 0.
|
||||||
* Foreign inputs not allowed in POR PSBT.
|
* Foreign inputs not allowed in POR PSBT.
|
||||||
|
|
||||||
The signatures created by the BIP-322 process will never be suitable
|
The signatures created by the BIP-322 process will never be suitable
|
||||||
for a on-chain Bitcoin transaction that could move funds, because
|
for a on-chain Bitcoin transaction that could move funds, because
|
||||||
of these restrictions imposed by BIP-322.
|
of these restrictions imposed by BIP-322.
|
||||||
|
|
||||||
### Output
|
|
||||||
|
|
||||||
COLDCARD always returns a signed PSBT for BIP-322 message signing and Proof of
|
|
||||||
Reserves. It never returns an extracted/finalized transaction for these PSBTs.
|
|
||||||
This is true even when finalization is requested over USB, such as with
|
|
||||||
`ckcc unsigned.psbt --finalize`.
|
|
||||||
|
|
||||||
The signed PSBT is the handoff artifact for the external finalizer/verifier. It
|
|
||||||
keeps the PSBT metadata needed to verify or finalize the BIP-322 signature,
|
|
||||||
including public keys, scripts, partial signatures, and UTXO data. This matters
|
|
||||||
because the address being proven normally commits only to a hash of the public
|
|
||||||
key or script, not the public key or script itself.
|
|
||||||
|
|
||||||
### Proof of Reserves Signing Experience
|
### Proof of Reserves Signing Experience
|
||||||
|
|
||||||
After Coldcard recognizes a BIP-322 PSBT it reads the message from
|
After Coldcard recognizes BIP-322 PoR PSBT it asks the user to
|
||||||
`PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and shows it to the user for approval.
|
import a human-readable message that was used to build `to_spend`
|
||||||
COLDCARD verifies that the message hash matches the input 0 `to_spend`
|
scriptSig. This message must hash exactly the `message_hash` from
|
||||||
commitment before offering to sign.
|
the PSBT, otherwise signing is not offered.
|
||||||
|
|
||||||
When the PSBT contains only input 0, COLDCARD labels the request as
|
|
||||||
`BIP-322 Message`, because it is message signing and does not prove ownership
|
|
||||||
of any additional reserve UTXOs. In that case it does not show transaction
|
|
||||||
input/output counts. When the PSBT contains additional inputs, COLDCARD labels
|
|
||||||
the request as `Proof of Reserves` and shows the reserve amount.
|
|
||||||
|
|
||||||
If the message contains non-ASCII characters, COLDCARD warns that some
|
|
||||||
characters may not be readable on screen.
|
|
||||||
|
|
||||||
Legacy PoR PSBTs without `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` are rejected by
|
|
||||||
this flow.
|
|
||||||
|
|
||||||
Read more [here.](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
|
Read more [here.](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
|
||||||
|
|
||||||
Example screen text for a one-input BIP-322 message signing PSBT:
|
Example screen text:
|
||||||
|
|
||||||
```text
|
|
||||||
BIP-322 Message
|
|
||||||
|
|
||||||
Message:
|
|
||||||
This is the signed message
|
|
||||||
|
|
||||||
Challenge Address:
|
|
||||||
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
|
|
||||||
|
|
||||||
Press ENTER to approve and sign message. Press (2) to explore transaction.
|
|
||||||
CANCEL to abort.
|
|
||||||
```
|
|
||||||
|
|
||||||
Example screen text for a Proof of Reserves PSBT:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Proof of Reserves
|
Proof of Reserves
|
||||||
|
|
||||||
Message:
|
|
||||||
POR
|
|
||||||
|
|
||||||
Amount 0.20000000 BTC
|
Amount 0.20000000 BTC
|
||||||
|
|
||||||
Challenge Address:
|
Message Hash:
|
||||||
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
|
11b5fe357842f5c368d2e3884d6a5ba577e3bc7cde132004f39b8c2a43a9cdec
|
||||||
|
|
||||||
21 inputs
|
Message Challenge:
|
||||||
1 output
|
00140b2537a7d6f3cc668c9e9fa0303ffb3cad6e9b81
|
||||||
|
|
||||||
Press ENTER to approve and sign proof of reserves. Press (2) to explore transaction.
|
21 inputs
|
||||||
CANCEL to abort.
|
1 output
|
||||||
|
|
||||||
|
0.00000000 BTC
|
||||||
|
- OP_RETURN -
|
||||||
|
null-data
|
||||||
|
|
||||||
|
Press ENTER to approve and sign transaction. Press (2) to explore txn
|
||||||
|
outputs. CANCEL to abort.
|
||||||
```
|
```
|
||||||
|
|||||||
@ -92,8 +92,8 @@ increases flexibility and resistance to known plain text attacks.
|
|||||||
| `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words
|
| `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words
|
||||||
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
|
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
|
||||||
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
|
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
|
||||||
| `SE2 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
| `SE2 easy key` | page 15 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
||||||
| `SE2 hard key` | page 15 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
| `SE2 hard key` | page 14 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
||||||
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
|
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
|
||||||
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
|
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
|
||||||
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)
|
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# COLDCARD Mk4/Mk5/Q Security Model
|
# COLDCARD Mk4/Q Security Model
|
||||||
|
|
||||||
## Abstract
|
## Abstract
|
||||||
|
|
||||||
@ -96,10 +96,9 @@ user entered the True PIN. An attacker will only have access to the
|
|||||||
duress wallet. They won't have access to steal the main stash.
|
duress wallet. They won't have access to steal the main stash.
|
||||||
|
|
||||||
The private key can be automatically derived using BIP-85 methods,
|
The private key can be automatically derived using BIP-85 methods,
|
||||||
based on account numbers 1001, 1002, or 1003 for a 24-word duress wallet
|
based on account numbers 1001, 1002, or 1003. Because this is BIP-85
|
||||||
(or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
|
based and uses a 24-word seed, it behaves exactly like a normal
|
||||||
based, it behaves exactly like a normal wallet. Defining a passphrase
|
wallet. Defining a passphrase for the wallet is also possible.
|
||||||
for the wallet is also possible.
|
|
||||||
|
|
||||||
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
|
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
|
||||||
on the blockchain. There is an option to create compatible wallets
|
on the blockchain. There is an option to create compatible wallets
|
||||||
@ -244,7 +243,7 @@ COLDCARD's case to do so, but the option is there if needed.
|
|||||||
|
|
||||||
## SD Card Recovery Mode
|
## SD Card Recovery Mode
|
||||||
|
|
||||||
Mk4/Mk5/Q bootloader is smart enough to be able to read an SD card. You
|
Mk4/Q bootloader is smart enough to be able to read an SD card. You
|
||||||
will only be able to trigger the SD card loading code, if the
|
will only be able to trigger the SD card loading code, if the
|
||||||
COLDCARD was powered down during the upgrade process. At that point,
|
COLDCARD was powered down during the upgrade process. At that point,
|
||||||
the intended firmware image has been lost because it it held in
|
the intended firmware image has been lost because it it held in
|
||||||
|
|||||||
@ -12,7 +12,7 @@ are not discrete and you could be compelled to produce the passphrase.
|
|||||||
|
|
||||||
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
|
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
|
||||||
of storing secrets in two or more parts that look and behave just
|
of storing secrets in two or more parts that look and behave just
|
||||||
like the original secret. One 12-, 18-, or 24-word seed phrase becomes two or more parts
|
like the original secret. One 12 or 24-word seed phrase becomes two or more parts
|
||||||
that are also BIP-39 compatible seeds phrases. These should be backed up in your
|
that are also BIP-39 compatible seeds phrases. These should be backed up in your
|
||||||
preferred method, metal or otherwise. These parts can be individually loaded
|
preferred method, metal or otherwise. These parts can be individually loaded
|
||||||
with honeypot funds as each one has same word length, with the last being
|
with honeypot funds as each one has same word length, with the last being
|
||||||
@ -78,12 +78,10 @@ words right the next day.
|
|||||||
|
|
||||||
When the parts are made deterministically, we take a double-SHA256 over
|
When the parts are made deterministically, we take a double-SHA256 over
|
||||||
a fixed string (`Batshitoshi`), your master secret, and the text
|
a fixed string (`Batshitoshi`), your master secret, and the text
|
||||||
`0 of 4 parts` which changes for each part (the index is 0-based).
|
`1 of 4 parts` which changes for each part.
|
||||||
|
|
||||||
In random mode, we simply pick random bytes (and then double-SHA256
|
In random mode, we simply pick 32 random bytes (and then double-SHA256
|
||||||
them) from the Coldcard's True Random Number Generator (TRNG). The number
|
them) from the Coldcard's True Random Number Generator (TRNG)..
|
||||||
of bytes matches your secret length: 16, 24, or 32 bytes for a 12-, 18-,
|
|
||||||
or 24-word seed respectively.
|
|
||||||
|
|
||||||
This is done to make all but the one part. The final part is the
|
This is done to make all but the one part. The final part is the
|
||||||
value needed to get back to your secret, so it's the XOR of the
|
value needed to get back to your secret, so it's the XOR of the
|
||||||
|
|||||||
77
docs/taproot.md
Normal file
77
docs/taproot.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# 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 2 methods to provide provably unspendable internal key, if users wish to only use tapscript script path.
|
||||||
|
|
||||||
|
1. **(recommended)** Origin-less extended key serialization with H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs) as BIP-32 key and random chaincode.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
### Below option was deprecated in version 6.3.5X & 6.3.5QX
|
||||||
|
2. 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))`
|
||||||
|
|
||||||
|
### Below option were deprecated in version 6.3.5X & 6.3.5QX
|
||||||
|
3. use **static** provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs).
|
||||||
|
|
||||||
|
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`
|
||||||
|
|
||||||
|
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 whole 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.
|
||||||
|
|
||||||
|
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.
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# Temporary Seeds
|
# Temporary Seeds
|
||||||
|
|
||||||
|
|
||||||
[_(new in v5.0.7, requires Mk4, Mk5, or Q)_](upgrade.md)
|
[_(new in v5.0.7, requires Mk4)_](upgrade.md)
|
||||||
|
|
||||||
|
|
||||||
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
|
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
|
||||||
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
|
|||||||
- `24 words`
|
- `24 words`
|
||||||
- `XPRV (BIP-32)`
|
- `XPRV (BIP-32)`
|
||||||
- pick derivation `Index` in next prompt, or just press OK for index 0
|
- pick derivation `Index` in next prompt, or just press OK for index 0
|
||||||
- Press (0) in next prompt to activate derived secret as a temporary seed
|
- Press (2) in next prompt to activate derived secret as a temporary seed
|
||||||
|
|
||||||
* temporary seed can be activated from Duress Wallet
|
* temporary seed can be activated from Duress Wallet
|
||||||
- go to `Settings -> Login Settings -> Trick Pins`
|
- go to `Settings -> Login Settings -> Trick Pins`
|
||||||
@ -66,7 +66,7 @@ Ability to generate and use **Temporary seed** is available on Coldcard when:
|
|||||||
|
|
||||||
# Restore Master
|
# Restore Master
|
||||||
|
|
||||||
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
|
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
|
||||||
|
|
||||||
From version `5.2.0` users no longer need to reboot COLDCARD to return
|
From version `5.2.0` users no longer need to reboot COLDCARD to return
|
||||||
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
|
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
|
||||||
@ -84,7 +84,7 @@ Seed Vault entries can only be deleted in Seed Vault menu.
|
|||||||
|
|
||||||
# Seed Vault
|
# Seed Vault
|
||||||
|
|
||||||
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
|
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
|
||||||
|
|
||||||
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
|
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
|
||||||
recall and later use (AES-256-CTR encrypted with your master seed's key).
|
recall and later use (AES-256-CTR encrypted with your master seed's key).
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
# Firmware Upgrade and Recovery Process
|
# Firmware Upgrade and Recovery Process
|
||||||
|
|
||||||
_This document applies to the Mk4, Mk5, and Q. Earlier COLDCARDs did not use this approach._
|
_This document applies only to the Mk4. Earlier COLDCARDs did not use this approach._
|
||||||
|
|
||||||
On the COLDCARD, we have done away with the slow external SPI flash
|
On the COLDCARD, we have done away with the slow external SPI flash
|
||||||
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we
|
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we
|
||||||
|
|||||||
@ -30,8 +30,8 @@ the correct code.
|
|||||||
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
|
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
|
||||||
- CC creates URL encrypted to the pubkey of server, containing args:
|
- CC creates URL encrypted to the pubkey of server, containing args:
|
||||||
- shared secret for TOTP (same value as held in user's phone)
|
- shared secret for TOTP (same value as held in user's phone)
|
||||||
- the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
|
- the response nonce (16 bytes, or 8 digits for Mk4) to be revealed to the user
|
||||||
to be revealed to the user on successful auth
|
on successful auth
|
||||||
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
|
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
|
||||||
- some text label for what's being approved, which is presented to user so they can pick
|
- some text label for what's being approved, which is presented to user so they can pick
|
||||||
correct 2fa shared secret.
|
correct 2fa shared secret.
|
||||||
@ -82,15 +82,12 @@ the correct code.
|
|||||||
|
|
||||||
## URL Format
|
## URL Format
|
||||||
|
|
||||||
https://coldcard.com/2fa?g={nonce}&ss={shared_secret}&nm={label_text}&q={is_q}
|
https://coldcard.com/2fa?ss={shared_secret}&q={is_q}&g={nonce}&nm={label_text}
|
||||||
|
|
||||||
(the query string is then encrypted to the server's pubkey, so the args above
|
|
||||||
are what is inside the encrypted payload.)
|
|
||||||
|
|
||||||
- `nonce`: text string that is either 8 digits on Mk4, or 64 hex chars on Q
|
|
||||||
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
|
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
|
||||||
- `nm`: human readable label for the transaction/purpose
|
|
||||||
- `is_q`: flag indicating use of QR to provide nonce back to user
|
- `is_q`: flag indicating use of QR to provide nonce back to user
|
||||||
|
- `nonce`: text string that is either 8 digits for Mk4, or hex digits for QR
|
||||||
|
- `nm`: human readable label for the transaction/purpose
|
||||||
|
|
||||||
Server will accept plaintext arguments as above, but normally everything
|
Server will accept plaintext arguments as above, but normally everything
|
||||||
after the question mark is encrypted.
|
after the question mark is encrypted.
|
||||||
|
|||||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 3d1dfa858beb58b8dac37d8c66d7aed2909812f2
|
Subproject commit 6d9f7193b336ab1097c7f941ce8c7e2ae80bfe29
|
||||||
2
external/libngu
vendored
2
external/libngu
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 537519a829259622ea6b0334fbafd6cae852852f
|
Subproject commit b0ce9acffa455d9630c64d3614d0fb9b913c919e
|
||||||
37
releases/EdgeChangeLog.md
Normal file
37
releases/EdgeChangeLog.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# 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.
|
||||||
|
```
|
||||||
|
|
||||||
|
This lists the changes in the most recent EDGE firmware, for each hardware platform.
|
||||||
|
|
||||||
|
# Shared Improvements - Both Mk4 and Q
|
||||||
|
|
||||||
|
- New Feature: Ability to sign MuSig2 UTXOs. Read more [here](https://github.com/Coldcard/firmware/blob/new_edge/docs/musig.md)
|
||||||
|
- New Feature: BIP-322 Proof of Reserves for Miniscript & MuSig2 UTXOs
|
||||||
|
- Bugfix: PSBT global XPUBs validation when signing with specific wallet
|
||||||
|
- Bugfix: Do not allow sighash DEFAULT outside taproot context
|
||||||
|
|
||||||
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
|
## 6.5.0X - 2026-03-24
|
||||||
|
|
||||||
|
- synced with master up to `5.5.0`
|
||||||
|
|
||||||
|
|
||||||
|
# Q Specific Changes
|
||||||
|
|
||||||
|
## 6.5.0QX - 2026-03-24
|
||||||
|
|
||||||
|
- synced with master up to `1.4.0Q`
|
||||||
|
|
||||||
|
|
||||||
|
# Release History
|
||||||
|
|
||||||
|
- [`History-Edge.md`](History-Edge.md)
|
||||||
161
releases/History-Edge.md
Normal file
161
releases/History-Edge.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
## 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.4.1X & 6.4.1QX
|
||||||
|
|
||||||
|
-Bugfix: Multisig migration only worked for K of K multisig wallets (those where M is the same as N)
|
||||||
|
|
||||||
|
|
||||||
|
# Shared Improvements - Both Mk4 and Q
|
||||||
|
|
||||||
|
### WARNING: 6.4.0X is not backwards-compatible with previous EDGE firmware versions.
|
||||||
|
#### 6.4.0X stores multisig wallet internally as Miniscript wallets. Newly created multisig wallets won't be visible if you downgrade after creating them on 6.4.0X. Existing multisig wallets will be converted into Miniscript, yet preserved in old format if downgrade is desired.
|
||||||
|
|
||||||
|
- New Feature: Key Teleport
|
||||||
|
- New Feature: Spending Policy for Miniscript Wallets
|
||||||
|
- New Feature: Internal descriptor cache speeding up sequential operation with miniscript wallets.
|
||||||
|
To take full advantage of the feature work with miniscript wallets sequentially. First, do all operations
|
||||||
|
needed with `wallet1` before changing to `wallet2`.
|
||||||
|
- New Feature: Add ability to import/export [BIP-388](https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki) Wallet Policies.
|
||||||
|
BIP-388 policies are now also used as our wallet serialization format, which optimized setting storage.
|
||||||
|
- New Feature: Sign with specific miniscript wallet. `Settings -> Multisig/Miniscript -> <name> -> Sign PSBT`
|
||||||
|
- New Feature: Miniscript wallet name can be specified for `sign` USB command
|
||||||
|
- New Feature: Rename Miniscript wallet via UX. `Settings -> Multisig/Miniscript -> <wallet> -> Rename`.
|
||||||
|
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
|
||||||
|
Navigate to `Advanced/Tools -> Export Wallet -> Key Expression`
|
||||||
|
- Enhancement: Slightly faster HW accelerated tagged hash
|
||||||
|
- Enhancement: PSBT class optimizations. Ability to sign bigger txn.
|
||||||
|
- Enhancement: Signing TXN UI shows Miniscript wallet name.
|
||||||
|
- Change: Deprecation of legacy mulitsig import format. Ability to import/export in this format was removed.
|
||||||
|
Old functionality - renaming by reimporting descriptor with different name was removed.
|
||||||
|
Use descriptors or BIP-388 wallet policies
|
||||||
|
- Change: Deprecated `p2sh` USB command. Use `miniscript` USB commands to handle multisig wallets.
|
||||||
|
- Change: Descriptor template was remove from Generic JSON export, and `key_exp` was added
|
||||||
|
with BIP-380 extended key expression `[xfp/origin_path]xpub`.
|
||||||
|
- Bugfix: Disjoint derivation in miniscript wallets
|
||||||
|
- Bugfix: Disallow P2SH legacy miniscript
|
||||||
|
- Bugfix: Do not allow to import miniscripts with relative lock without consensus meaning.
|
||||||
|
Only allow to import block-based in range `older(1 - 65535)` & time-based in range `older(4194305 - 4259839)`
|
||||||
|
|
||||||
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
|
## 6.4.0X - 2025-11-20
|
||||||
|
|
||||||
|
- synced with master up to `5.4.5`
|
||||||
|
- Enhancement: Show QR of XOR-split seeds
|
||||||
|
|
||||||
|
|
||||||
|
# Q Specific Changes
|
||||||
|
|
||||||
|
## 6.4.0QX - 2025-11-20
|
||||||
|
|
||||||
|
- synced with master up to `1.3.5Q`
|
||||||
|
|
||||||
|
|
||||||
|
# 6.3.5X & 6.3.5QX Shared Improvements - Both Mk4 and Q
|
||||||
|
|
||||||
|
Change: Allow origin-less extended keys in multisig & miniscript descriptors
|
||||||
|
Change: Static internal keys disallowed - all keys need to be ranged extended keys
|
||||||
|
|
||||||
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
|
- all updates from `5.4.1`
|
||||||
|
|
||||||
|
# Q Specific Changes
|
||||||
|
|
||||||
|
- all updates from version `1.3.1Q`
|
||||||
|
|
||||||
|
|
||||||
|
# 6.3.4X & 6.3.4QX Shared Improvements - Both Mk4 and Q
|
||||||
|
|
||||||
|
- Bugfix: Complex miniscript wallets with keys in policy that are not in strictly ascending order were incorrectly filled
|
||||||
|
upon load from settings. All users on versions `6.2.2X`+ needs to update.
|
||||||
|
- Bugfix: Single key miniscript descriptor support
|
||||||
|
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
|
||||||
|
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
|
||||||
|
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode
|
||||||
|
- Bugfix: Bless Firmware causes hanging progress bar
|
||||||
|
- Bugfix: Prevent yikes in ownership search
|
||||||
|
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
|
||||||
|
|
||||||
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
|
- all updates from `5.4.0`
|
||||||
|
- Enhancement: Export single sig descriptor with simple QR
|
||||||
|
|
||||||
|
# Q Specific Changes
|
||||||
|
|
||||||
|
- all updates from version `1.3.0Q`
|
||||||
|
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||||
|
|
||||||
|
|
||||||
|
## 6.3.3X & 6.3.3QX Shared Improvements - Both Mk4 and Q (2024-07-04)
|
||||||
|
|
||||||
|
- New Feature: Ranged provably unspendable keys and `unspend(` support for Taproot descriptors
|
||||||
|
- 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
|
||||||
@ -192,6 +192,44 @@
|
|||||||
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
|
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
|
||||||
|
|
||||||
|
|
||||||
|
## 1.3.0Q - 2024-09-12
|
||||||
|
|
||||||
|
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
|
||||||
|
descriptor with `multi(...)`. Disabled by default, Enable in
|
||||||
|
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
|
||||||
|
wallets, not new ones.
|
||||||
|
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
|
||||||
|
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
|
||||||
|
instead of the filename. Most useful for USB and NFC imports which have no filename,
|
||||||
|
(name is created from descriptor checksum in those cases).
|
||||||
|
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
|
||||||
|
- Enhancement: upgrade to latest
|
||||||
|
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
|
||||||
|
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
|
||||||
|
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
|
||||||
|
before each signing session.
|
||||||
|
- Enhancement: Allow JSON files in `NFC File Share`.
|
||||||
|
- Change: Do not require descriptor checksum when importing multisig wallets.
|
||||||
|
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
|
||||||
|
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
|
||||||
|
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
|
||||||
|
- Bugfix: Fix display alignment of Seed Vault menu.
|
||||||
|
- Bugfix: Properly handle null data in `OP_RETURN`.
|
||||||
|
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
|
||||||
|
from custom path.
|
||||||
|
- Change: Remove Lamp Test from Debug Options (covered by selftest).
|
||||||
|
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
|
||||||
|
- New Feature: Input backup password from QR scan.
|
||||||
|
- New Feature: (BB)QR file share of arbitrary files.
|
||||||
|
- New Feature: `Create Airgapped` now works with BBQRs.
|
||||||
|
- Change: Default brightness (on battery) adjusted from 80% to 95%.
|
||||||
|
- Bugfix: Properly clear LCD screen after BBQR is shown.
|
||||||
|
- Bugfix: Writing to empty slot B caused broken card reader.
|
||||||
|
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
|
||||||
|
- Bugfix: Stop re-wording UX stories using a regular expression.
|
||||||
|
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
|
||||||
|
|
||||||
|
|
||||||
## 1.2.3Q - 2024-07-05
|
## 1.2.3Q - 2024-07-05
|
||||||
|
|
||||||
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD
|
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD
|
||||||
|
|||||||
@ -4,48 +4,7 @@ This lists the new changes that have not yet been published in a normal release.
|
|||||||
|
|
||||||
# Shared Improvements - Both Mk and Q
|
# Shared Improvements - Both Mk and Q
|
||||||
|
|
||||||
- Change: BIP-322 Proof of Reserves & message signing PSBT requires PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE
|
- tbd
|
||||||
(read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
|
|
||||||
- Enhancement: WIF Store export watch-only descriptor
|
|
||||||
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
|
|
||||||
- Enhancement: Improve USB length validation
|
|
||||||
- Bugfix: Fixes legacy input amount spoofing by rejecting witness-utxo-only PSBT inputs when Coldcard is expected to sign a non-segwit input.
|
|
||||||
When both UTXO fields are present the full non_witness_utxo is now preferred for amount/script lookup. Thanks, @Damir
|
|
||||||
- Bugfix: Emit warning and do not calculate fee for legacy UTXOs with only witness utxo
|
|
||||||
- Bugfix: Disable Virtual Disk and NFC before activating HSM
|
|
||||||
- Bugfix: P2PK signing was broken. Now supports both compressed and uncompressed P2PK spend
|
|
||||||
- Bugfix: Custom address default menu position wrong
|
|
||||||
- Bugfix: Delta Mode Trick PIN was never restored from backup
|
|
||||||
- Bugfix: Proper error message for incorrect 7z headers
|
|
||||||
- Bugfix: Exiting nickname entry with nickname already saved deleted previous nickname
|
|
||||||
- Bugfix: "Send Password" menu item inside Notes & Passwords visibility reversed
|
|
||||||
- Bugfix: Yikes when using "Send Password" on entry with password None field
|
|
||||||
- Bugfix: Do not show "Saving..." UX after failed Notes & Passwords import
|
|
||||||
- Bugfix: Incorrect error message caused by error in Verify/Decrypt Backup
|
|
||||||
- Bugfix: NFC Verify Address raised incorrect error message
|
|
||||||
- Bugfix: Notes & Passwords bulk import JSON with BBQr encoded as text
|
|
||||||
- Bugfix: CCC key C challenge handled bad BIP-39 checksum by crashing the UX; now treated as a wrong attempt (counts toward 3-strike lockout)
|
|
||||||
- Bugfix: CCC magnitude reset from CANCEL on empty input
|
|
||||||
- Bugfix: OP_RETURN in CCC with whitelist enabled caused yikes
|
|
||||||
- Bugfix: TX Explorer crashed on foreign input with non-standard sighash
|
|
||||||
- Bugfix: Malformed JSON message-sign request crashed signing UX
|
|
||||||
- Bugfix: Reject UI-control bytes in JSON / QR text message-signing
|
|
||||||
- Bugfix: Non-standard OP_RETURN outputs shown as "null-data", hiding part of the script
|
|
||||||
- Bugfix: Over-limit CCC address-whitelist import was rejected but still modified the policy
|
|
||||||
- Bugfix: Deleting a file right after renaming it (List Files) blanked the old name, leaving the renamed file
|
|
||||||
- Bugfix: Reordered `multi(...)` multisig with same keys was misreported as name-only change. Now blocked as duplicate.
|
|
||||||
- Bugfix: Max WIF store capacity limit was ignored if saving via QR WIF visualization
|
|
||||||
- Bugfix: Force Seed XOR restore from Temporary Seed menu to remain temporary even when master seed is blank
|
|
||||||
- Bugfix: Q1 seed word entry cursor alignment for 12-word seeds and preserve visible words after failed QR scans
|
|
||||||
- Bugfix: Binary signed-transaction (.txn) failed in NFC/QR file share
|
|
||||||
- Bugfix: yikes in transaction explorer for goto index for tx with only one output
|
|
||||||
- Bugfix: Sending `signmessage` payload encoded as BBQr caused yikes
|
|
||||||
- Bugfix: CCC/SSSP NFC whitelist import caused Yikes
|
|
||||||
- Bugfix: Stricter address ownership validation rejects unrecognized payment addresses before wallet search
|
|
||||||
- Bugfix: Handle malformed NDEF records robustly. Thanks, @Damir
|
|
||||||
- Bugfix: Ignore `bkpw` if added to backup. Thanks [@dmonakhov](https://github.com/dmonakhov)
|
|
||||||
- Bugfix: Keep NFC export tag live for repeated probes
|
|
||||||
- Bugfix: Fix 1of1 multisig signing failure
|
|
||||||
|
|
||||||
# Mk Specific Changes
|
# Mk Specific Changes
|
||||||
|
|
||||||
@ -58,17 +17,6 @@ This lists the new changes that have not yet been published in a normal release.
|
|||||||
|
|
||||||
## 1.4.xQ - 2065-04-xx
|
## 1.4.xQ - 2065-04-xx
|
||||||
|
|
||||||
- New Feature: Secure Notes & Passwords UX groups
|
- tbd
|
||||||
- New Feature: Apply Secure Note text, or Secure Note password as BIP-39 passphrase
|
|
||||||
- Bugfix: Teleporting a multisig PSBT file (without signing it first) sent stale data instead of the selected file
|
|
||||||
- Bugfix: Fix export UX message after teleport PSBT import & sign
|
|
||||||
- Bugfix: BIP-21 QR `amount` rendered with wrong decimal scaling on the Payment Address screen (e.g. `amount=1.1` was shown as `1.00000001 BTC`)
|
|
||||||
- Bugfix: QR scan import (Scan Any QR Code, master/temp seed via QR) now surfaces a clean error story on any parser or seed-loading failure (e.g. wordlist-valid but bad-checksum SeedQR) instead of yikesing the menu task
|
|
||||||
- Bugfix: Yikes when showing "QR too big" for a transaction output alone on an output-explorer page
|
|
||||||
- Bugfix: Yikes receiving a malformed full-backup via Key Teleport
|
|
||||||
- Bugfix: Keyboard debounce could leave a key stuck as "pressed" after release when another key was held
|
|
||||||
- Bugfix: Scanner robustness
|
|
||||||
- Avoid holding the QR scanner reset line low; reset is now only pulsed and then left deasserted.
|
|
||||||
- Recover scanner setup failures by retrying configuration and reinitializing on the next scan when needed.
|
|
||||||
- Prevent delayed scanner sleep commands from racing with a newly started scan.
|
|
||||||
- Improve scanner shutdown/recovery after scan cancel or command timeout.
|
|
||||||
|
|||||||
@ -3,136 +3,52 @@ Hash: SHA256
|
|||||||
|
|
||||||
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
||||||
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
|
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
|
||||||
72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
|
7d9dd67289f717aeb80f13a8e283e2dcc0da3036359afd2a5774dc04a2947680 History-Q.md
|
||||||
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
|
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
|
||||||
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
||||||
|
6dbf9ddb58b4fcc25b084ffd57a0fd7ad56bd0934ac464735420c6c835b31a19 History-Edge.md
|
||||||
|
cb8316310a4c165b26cc68ed584775125d52abc0bf744712662e91ded4286e01 EdgeChangeLog.md
|
||||||
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
|
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
|
||||||
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
|
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
|
||||||
|
400789bfd4fe4912161078b28a547ec029f1d108d109715c57919697746c6c31 2026-03-25T1408-v6.5.0X-mk-coldcard-factory.dfu
|
||||||
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
|
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
|
||||||
2b7b4d95cd5d606b0a32e692db7a27c1d860140c6e919e20ea6672ad6afc3088 2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
a45770254c1fcfa09324cc9ff0d85c4e19559f493ec78c25d1bdf7cabc5cc65e 2026-03-25T1407-v6.5.0QX-q1-coldcard-factory.dfu
|
||||||
15f26aa0b8fe33e29e338b74acc52d5532922af56bcf486e38085de55c86b82a 2026-03-05T2052-v5.5.0-mk-coldcard-factory.dfu
|
|
||||||
b7ce3ba55ae4bb1e1ebe0090507bcada5a4439e57a19552c78da7ef103bd144c 2026-03-05T2051-v1.4.0Q-q1-coldcard.dfu
|
|
||||||
82c2ed5ee5cf75cc8af0f54839b9a1bc8e4a329174657d61861a6576fb6e31d9 2026-03-05T2051-v1.4.0Q-q1-coldcard-factory.dfu
|
|
||||||
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
|
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
|
||||||
|
580acb64157cf3e2167d3afd46e1e406d75c3532356c36b67321cd2f1a218fc8 2025-11-25T1618-v6.4.1X-mk4-coldcard-factory.dfu
|
||||||
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
|
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
|
||||||
|
dc5fcc6a633c2cca1d1d709accc556a3ae4730f1a579062745b830cd5fd07656 2025-11-25T1617-v6.4.1QX-q1-coldcard-factory.dfu
|
||||||
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
|
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
|
||||||
|
993ef645ca83988c576febfaa248c0a5044e948d3d1e4443f31d5f9fd5734fe1 2025-11-20T1602-v6.4.0X-mk4-coldcard-factory.dfu
|
||||||
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
|
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
|
||||||
7076ae29c509d3120db0fae434c132e6abd3fb79c1a2a2f1383ab3b2acaba27c 2025-11-03T1527-v5.4.5-mk4-coldcard.dfu
|
f7e73850b3c3dc33b1cd0fa7a94909931c1a4bbd881a7224a71da77807976640 2025-11-20T1601-v6.4.0QX-q1-coldcard-factory.dfu
|
||||||
00a337888ff86bf875bcfdab7a734981bce29a49f94f3df9f932924765848ab0 2025-11-03T1527-v5.4.5-mk4-coldcard-factory.dfu
|
|
||||||
ff6371545943518eb4eb00ba73b6aa3a5ac4e63459621ecec8a300c28c281b3c 2025-11-03T1525-v1.3.5Q-q1-coldcard.dfu
|
|
||||||
0ce02c8e549cb67b682d621b4a628f3fba2c56350a9ab090b9f08532f49e7afa 2025-11-03T1525-v1.3.5Q-q1-coldcard-factory.dfu
|
|
||||||
8a8c94e5f64d0bfe4914a236fb8a779f956989fe8de998133b85b23920f46283 2025-09-30T1238-v5.4.4-mk4-coldcard.dfu
|
|
||||||
0d0aba89027d5127f74b2a2b777a7c592cba12903a3c4c3ce9b0e060c09dddb7 2025-09-30T1238-v5.4.4-mk4-coldcard-factory.dfu
|
|
||||||
bc9918968b67fefe634342c77513c9c354e7821e9ff002c7e5c8c356d7507892 2025-09-30T1237-v1.3.4Q-q1-coldcard.dfu
|
|
||||||
00cb1fc2ef360aacf48ba8c9dd2167b3f5c5f1241ba1b2b17d61ea1b7bff0a45 2025-09-30T1237-v1.3.4Q-q1-coldcard-factory.dfu
|
|
||||||
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
|
|
||||||
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
|
|
||||||
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
|
|
||||||
9daa2b48abdfa2303a43ee1d0ba3d0d905e7f6286018f44a0dda3c755c46039e 2025-05-14T1343-v1.3.3Q-q1-coldcard-factory.dfu
|
|
||||||
c1202ba30db68a12b882176997f08844da4ec31087ac2f507ea1d4281d2faa9a 2025-04-16T1907-v5.4.2-mk4-coldcard.dfu
|
|
||||||
a82fff91f8da35c122b09d54b01b3d3f3b8825fbc7cddee8e686fd8d57a69285 2025-04-16T1907-v5.4.2-mk4-coldcard-factory.dfu
|
|
||||||
4393c67f8dcbb8890950678f5856d2cca81042b9a447ce149fe624ddfe336a2a 2025-04-16T1906-v1.3.2Q-q1-coldcard.dfu
|
|
||||||
6f2d77f99d61cad9328cc617754aadb3b828150386def41c946d90db8cfb2277 2025-04-16T1906-v1.3.2Q-q1-coldcard-factory.dfu
|
|
||||||
495f37ce7ddaba2e9fc3f03dec582f1646f258a3d0cec5e71c04d127357b2fa3 2025-02-19T1941-v6.3.5X-mk4-coldcard.dfu
|
495f37ce7ddaba2e9fc3f03dec582f1646f258a3d0cec5e71c04d127357b2fa3 2025-02-19T1941-v6.3.5X-mk4-coldcard.dfu
|
||||||
|
580701fb2de24362d8de6cf998d5fd42ca9ab003aff75f3c0140d915a06a6803 2025-02-19T1941-v6.3.5X-mk4-coldcard-factory.dfu
|
||||||
605ebb5acde19447e5c1d7c8cfd0302c89de5c5870d85f06b185ecab3437f94e 2025-02-19T1939-v6.3.5QX-q1-coldcard.dfu
|
605ebb5acde19447e5c1d7c8cfd0302c89de5c5870d85f06b185ecab3437f94e 2025-02-19T1939-v6.3.5QX-q1-coldcard.dfu
|
||||||
|
245db07574a535a3f068ed9a759bf0088f0d0e1e39704a0e0727f90119833602 2025-02-19T1939-v6.3.5QX-q1-coldcard-factory.dfu
|
||||||
eb750a4f095eacc6133b2c8b38fe0738a22b2496a6cdf423ca865acde8c9bc4e 2025-02-13T1415-v5.4.1-mk4-coldcard.dfu
|
eb750a4f095eacc6133b2c8b38fe0738a22b2496a6cdf423ca865acde8c9bc4e 2025-02-13T1415-v5.4.1-mk4-coldcard.dfu
|
||||||
4236453fea241fe044a462a560d8b42df43e560683110306a2714a2ef561eac5 2025-02-13T1415-v5.4.1-mk4-coldcard-factory.dfu
|
4236453fea241fe044a462a560d8b42df43e560683110306a2714a2ef561eac5 2025-02-13T1415-v5.4.1-mk4-coldcard-factory.dfu
|
||||||
2e1aad0a7a3ceb84db34322b54855a0c5496699e46e53606bfa443fcc992adec 2025-02-13T1413-v1.3.1Q-q1-coldcard.dfu
|
2e1aad0a7a3ceb84db34322b54855a0c5496699e46e53606bfa443fcc992adec 2025-02-13T1413-v1.3.1Q-q1-coldcard.dfu
|
||||||
e43932d04bf782f7b9ba218b54f29b9cd361b83ac3aadff9722714bca1ab7ee9 2025-02-13T1413-v1.3.1Q-q1-coldcard-factory.dfu
|
e43932d04bf782f7b9ba218b54f29b9cd361b83ac3aadff9722714bca1ab7ee9 2025-02-13T1413-v1.3.1Q-q1-coldcard-factory.dfu
|
||||||
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
|
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
|
||||||
|
73f31fbcb064a6b763d50852aafcdff01d7ec72906b5cb0af6cf28328fd80a89 2024-12-18T1413-v6.3.4X-mk4-coldcard-factory.dfu
|
||||||
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
|
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
|
||||||
237cfcb3fdf9217550eae1d9ea6fc828c1c8d09470bd60c9f72f9b00a3bb2d11 2024-09-12T1734-v5.4.0-mk4-coldcard.dfu
|
7e284bcead1f9c2f468230a588ddf62064014682772a552d05f453d91d55b6ae 2024-12-18T1407-v6.3.4QX-q1-coldcard-factory.dfu
|
||||||
6d1178f07d543e1777dbbdca41d872b00ca9c40e0c0c1ffb8ef96e19c51daa52 2024-09-12T1734-v5.4.0-mk4-coldcard-factory.dfu
|
|
||||||
d840fa4e83ebc7b0f961f30f68d795bed61271e2314dda4ab0eb0b8bfe7192f4 2024-09-12T1733-v1.3.0Q-q1-coldcard.dfu
|
|
||||||
4db89ecffa1376bfc68a37110c2041a29afe52b005d527ecde701131168fc19c 2024-09-12T1733-v1.3.0Q-q1-coldcard-factory.dfu
|
|
||||||
4d83715772b31643abde3b9a0bb328003f4a31d14e2fe9c1e038077a518acaea 2024-07-05T1348-v5.3.3-mk4-coldcard.dfu
|
|
||||||
020d6d5c3baa724713b2f906112bb95f7eff43c3f5a4f8f11b77d8c2e96ccc88 2024-07-05T1348-v5.3.3-mk4-coldcard-factory.dfu
|
|
||||||
54da941c8df84fcb84adcc62fdd3ee97d1fc12e2a9a648551ca614fcbacade3f 2024-07-05T1342-v1.2.3Q-q1-coldcard.dfu
|
|
||||||
7f704aa37887ed84d6a25f124e9b4a31187430d7cf6b198eb83b86af8ae4e5ea 2024-07-05T1342-v1.2.3Q-q1-coldcard-factory.dfu
|
|
||||||
ddf5ce1ef1ee2e6ba2922b333213d0cb939a2658b294c0f24c0e489de3fe7c75 2024-07-04T1501-v6.3.3X-mk4-coldcard.dfu
|
|
||||||
9a2c5ef80a6f8212caa3b455e203da3549a79b08b473113662cf80fff587566a 2024-07-04T1459-v6.3.3QX-q1-coldcard.dfu
|
|
||||||
a990cc94066486a37071c011cd85a29caed433cb4ca3f1c4dce7f715ef81dc3c 2024-06-26T1741-v5.3.2-mk4-coldcard.dfu
|
|
||||||
218d17069d05c0ec2829e5629c5216121028d15b145c31b552e2f52daa7bf172 2024-06-26T1741-v5.3.2-mk4-coldcard-factory.dfu
|
|
||||||
b87505b407b0477e2d15f71cfb20645ac55ac5b7c74493d25a2c9c97e807b2b3 2024-06-26T1739-v1.2.2Q-q1-coldcard.dfu
|
|
||||||
efff41069f3f82d4e69d08a02a565ae0d2cd55c07dbbbe4c1328e6e3b6d8faa1 2024-06-26T1739-v1.2.2Q-q1-coldcard-factory.dfu
|
|
||||||
90b1edfbe194b093258f9cda8f4add4aa3317e9ea205ff35914da7d91410fdae 2024-05-09T1529-v1.2.1Q-q1-coldcard.dfu
|
|
||||||
c7889532323f7b0c08e84589c7cc756e2c46e209b4eea031bdfef4a633a813c1 2024-05-09T1529-v1.2.1Q-q1-coldcard-factory.dfu
|
|
||||||
ef6526d37bc1a929c94dc8388f3863f6cc1582addf26495f761123f0bfb7aa30 2024-05-09T1527-v5.3.1-mk4-coldcard.dfu
|
|
||||||
98c675e98a18b2437c52e30a9867c271bbca9969771caa34299556ef3fcb1a43 2024-05-09T1527-v5.3.1-mk4-coldcard-factory.dfu
|
|
||||||
c7c79a21c206e8b0e816c86ef1b43cd6932cb767ed97291d5fbc2f0e749f95b7 2024-05-06T1812-v1.2.0Q-q1-coldcard.dfu
|
|
||||||
5c6b69948f0193b3a7bd252195136d6d9f84ab14fbc8c5349150e7d238708c6f 2024-05-06T1812-v1.2.0Q-q1-coldcard-factory.dfu
|
|
||||||
bab6818787eec45ef28b6c297e2504ffd4fa041ab19da8a3fd27543dffe876b8 2024-05-06T1811-v5.3.0-mk4-coldcard.dfu
|
|
||||||
3da458c0dabe9a17eaeb92ee959006a64a3e6838eeb31f887a18840f020ef8b9 2024-05-06T1811-v5.3.0-mk4-coldcard-factory.dfu
|
|
||||||
101f336310b9b460d717d91d2572ea9e9ef7ac3edbdaf132c7c3aa46bb89050a 2024-04-02T1416-v1.1.0Q-q1-coldcard.dfu
|
|
||||||
5d034bc6b1abec49a067a90766bdb769faf9a1b52b2c9b7e541d32484cf783fc 2024-04-02T1416-v1.1.0Q-q1-coldcard-factory.dfu
|
|
||||||
6ea843a56e87d7d811d90be6bfa4703794bbc8318d9709e88ada05740e03b12d 2024-03-14T1419-v1.0.1Q-q1-coldcard.dfu
|
|
||||||
f53c79c64f02dd1e860a8d32f9319edd279485d97f07815b2a1eb180a1305459 2024-03-14T1419-v1.0.1Q-q1-coldcard-factory.dfu
|
|
||||||
122e6d757eb5a8ce073d98a85851f376adec97856336c5a8f05b953b5c87a533 2024-03-10T1537-v1.0.0Q-q1-coldcard.dfu
|
|
||||||
ae04aaac47f07e10143c75b5c772b54739830214c8234356d003137897f3f4f4 2024-03-10T1537-v1.0.0Q-q1-coldcard-factory.dfu
|
|
||||||
6aaa9d5bf1726fe4d4a4834010d9b9b6525e8592bb97945cd08cc728fc884068 2024-03-02T1750-v0.0.8Q-q1-coldcard.dfu
|
|
||||||
a0cd556693fae5b8b03f2a498c0abb1e6d747f91a92bd8f2559a676f8707d840 2024-03-02T1750-v0.0.8Q-q1-coldcard-factory.dfu
|
|
||||||
18fe081d84a950e1fddb2151ad50917697dfc218cd68e2e359229b0bdadbff37 2024-02-26T1442-v0.0.7Q-q1-coldcard.dfu
|
|
||||||
e4f4fe89cf3743d794568fd5b32b14551966139e9199602ea10468f925fab1cf 2024-02-26T1442-v0.0.7Q-q1-coldcard-factory.dfu
|
|
||||||
2dc7a27f43958f2de9851f221183c94258ac915ae43d997b39b644e7b9daff8f 2024-02-22T1423-v0.0.6Q-q1-coldcard.dfu
|
|
||||||
1e4f4d4c04835d78fcc4857d3264034a56dccf594e307d7408d7c4cdcdb0a926 2024-02-22T1423-v0.0.6Q-q1-coldcard-factory.dfu
|
|
||||||
d51573c72d8958ea35357d4e0a36ce6aaa2d05924577efb219e2cc189be63f08 2024-02-16T1635-v0.0.5Q-q1-coldcard.dfu
|
|
||||||
55f4ef9c3ae116f50db938acfc3a4b09717965f82cf6de8cc7385f68cd66d285 2024-02-16T1635-v0.0.5Q-q1-coldcard-factory.dfu
|
|
||||||
8fd1ced0d5e0338d845f6d5ec5ab069a5143cceade02d4f17e86b7d182b489eb 2024-02-15T1843-v0.0.4Q-q1-coldcard.dfu
|
|
||||||
43fac084727b0e69bae7fc040a62854673fd585dc2435d93bf146c80762e41cf 2024-02-15T1843-v0.0.4Q-q1-coldcard-factory.dfu
|
|
||||||
3064bf7f1a039e7cd5c1a13c6aff8cc4338e52ef2177abbdca4b196955f9e434 2024-02-08T2005-v0.0.3Q-q1-coldcard.dfu
|
|
||||||
788e7a1b182f920016617411b875fa7095ae007c6a53fc476afb1c93f0eed1c9 2024-02-08T2005-v0.0.3Q-q1-coldcard-factory.dfu
|
|
||||||
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
|
||||||
7fbed097d2757b21fde920f4b10f5f50d7e1aeca01ff52186dfde4883af5cace 2023-10-10T1735-v5.2.0-mk4-coldcard.dfu
|
1dcfb450f81883afe8f655239f06e238de7bae51e740cd4aa5ae6a0541772ad8 2023-10-26T1343-v6.2.1X-mk4-coldcard-factory.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/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
|
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD7GQACgkQo6MbrVoq
|
||||||
WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
|
WxCMxwf/Q4oCzsi5r/CKHXHMgxO+vZdVS21pbwKBO8myjWkvsje2DKran9djUIj0
|
||||||
GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
|
GO+J9fMYuIRqzKocwh3R7NLRTBYl2CWvNA8X5t6HBgg8GekRonVRtYz3v5layj/I
|
||||||
lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
|
DO13xenkUBsCi0R/pkTVK+flKx720Ph/JEf5yRclhpWPZtuj5FztEFxJ+iwK+ipV
|
||||||
773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
|
OG1JwlMRFoNRKwC+ayp8Fz607dPrI5dSd7TTz02PcCNXkMauQYwhvzxHWF6ExLly
|
||||||
Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
|
ddSmHdBFRxDS8PRokOvXQOQkzsF55aiv+UMt76l37FPmmbHTPCWdrHYguVm/g9Tz
|
||||||
BhA61XO8yazNLVvata611pSTikNnDQ==
|
roezfeiGOcyfvodyr9mjq7PUB75A9g==
|
||||||
=8Ti0
|
=08Ws
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from export import export_contents, make_summary_file, make_descriptor_wallet_ex
|
|||||||
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, make_key_expression_export
|
from export import generate_unchained_export, generate_electrum_wallet, make_key_expression_export
|
||||||
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
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH
|
||||||
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
|
||||||
@ -459,27 +459,21 @@ async def pick_nickname(*a):
|
|||||||
# Value is not stored with normal settings, it's part of "prelogin" settings
|
# Value is not stored with normal settings, it's part of "prelogin" settings
|
||||||
# which are encrypted with zero-key.
|
# which are encrypted with zero-key.
|
||||||
s = SettingsObject.prelogin()
|
s = SettingsObject.prelogin()
|
||||||
k = "nick"
|
nick = s.get('nick', '')
|
||||||
nick = s.get(k, '')
|
|
||||||
|
|
||||||
if not nick:
|
if not nick:
|
||||||
ch = await ux_show_story("You can give this Coldcard a nickname"
|
ch = await ux_show_story('''\
|
||||||
" and it will be shown before login.")
|
You can give this Coldcard a nickname and it will be shown before login.''')
|
||||||
if ch != 'y': return
|
if ch != 'y': return
|
||||||
|
|
||||||
nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")
|
nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")
|
||||||
|
|
||||||
if nn is None or (nick == nn): return # user exit & same value - noop
|
|
||||||
|
|
||||||
from glob import dis
|
from glob import dis
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
dis.busy_bar(True)
|
dis.busy_bar(True)
|
||||||
|
|
||||||
if not nn:
|
nn = nn.strip() if nn else None
|
||||||
s.remove_key(k)
|
s.set('nick', nn)
|
||||||
else:
|
|
||||||
s.set(k, nn.strip())
|
|
||||||
|
|
||||||
s.save()
|
s.save()
|
||||||
dis.busy_bar(False)
|
dis.busy_bar(False)
|
||||||
del s
|
del s
|
||||||
@ -529,7 +523,6 @@ async def new_from_dice(menu, label, item):
|
|||||||
|
|
||||||
async def any_active_duress_ux():
|
async def any_active_duress_ux():
|
||||||
from trick_pins import tp
|
from trick_pins import tp
|
||||||
tp.reload()
|
|
||||||
# if TPs are hidden this msg will not be shown
|
# if TPs are hidden this msg will not be shown
|
||||||
if any(tp.get_duress_pins()):
|
if any(tp.get_duress_pins()):
|
||||||
await ux_show_story('You have one or more duress wallets defined '
|
await ux_show_story('You have one or more duress wallets defined '
|
||||||
@ -762,6 +755,10 @@ async def version_migration():
|
|||||||
# version 5.0.6 is installed
|
# version 5.0.6 is installed
|
||||||
settings.remove_key('vdsk')
|
settings.remove_key('vdsk')
|
||||||
|
|
||||||
|
# 6.4.0 multisig migration
|
||||||
|
from wallet import do_640_multisig_migration
|
||||||
|
await do_640_multisig_migration()
|
||||||
|
|
||||||
async def version_migration_prelogin():
|
async def version_migration_prelogin():
|
||||||
# same, but for setting before login
|
# same, but for setting before login
|
||||||
# these have moved into SE2 for Mk4 and so can be removed
|
# these have moved into SE2 for Mk4 and so can be removed
|
||||||
@ -828,7 +825,7 @@ async def start_login_sequence():
|
|||||||
# PIN again) and if it's a duress wallet, that's cool...
|
# PIN again) and if it's a duress wallet, that's cool...
|
||||||
|
|
||||||
# Do we need to do countdown delay? (real or otherwise)
|
# Do we need to do countdown delay? (real or otherwise)
|
||||||
# - wiping has already occurred if that was selected by trick details
|
# - wiping has already occured if that was selected by trick details
|
||||||
# - delay is variable, stored in tc_arg
|
# - delay is variable, stored in tc_arg
|
||||||
delay = tp.was_countdown_pin()
|
delay = tp.was_countdown_pin()
|
||||||
|
|
||||||
@ -913,6 +910,13 @@ 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 firmware version is qualified for use with wallets (such as"
|
||||||
|
" AnchorWatch, Liana, and Nunchuk) that keep redundant key schemas for recovery"
|
||||||
|
" independent of COLDCARD. We support the very latest Bitcoin innovations"
|
||||||
|
" in the Edge Version.", title="Edge Version")
|
||||||
|
|
||||||
dis.draw_status(xfp=settings.get('xfp'))
|
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,
|
||||||
@ -938,14 +942,12 @@ async def start_login_sequence():
|
|||||||
settings.master_set("seedvault", False)
|
settings.master_set("seedvault", False)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
if version.has_nfc and settings.get('nfc', 0):
|
||||||
from glob import hsm_active
|
|
||||||
if version.has_nfc and settings.get('nfc', 0) and not hsm_active:
|
|
||||||
# Maybe allow NFC now
|
# Maybe allow NFC now
|
||||||
import nfc
|
import nfc
|
||||||
nfc.NFCHandler.startup()
|
nfc.NFCHandler.startup()
|
||||||
|
|
||||||
if settings.get('vidsk', 0) and not hsm_active:
|
if settings.get('vidsk', 0):
|
||||||
# Maybe start virtual disk
|
# Maybe start virtual disk
|
||||||
import vdisk
|
import vdisk
|
||||||
vdisk.VirtDisk()
|
vdisk.VirtDisk()
|
||||||
@ -1068,7 +1070,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,
|
path = path.format(account=acct, coin_type=chain.b44_cointype,
|
||||||
change=0, idx=0)[:-4]
|
change=0, idx=0)[:-4]
|
||||||
@ -1079,7 +1081,7 @@ async def export_xpub(label, _2, item):
|
|||||||
if path != "m":
|
if path != "m":
|
||||||
esc += "1"
|
esc += "1"
|
||||||
msg += "Press (1) to select account other than %s." % (acct or "zero")
|
msg += "Press (1) to select account other than %s." % (acct or "zero")
|
||||||
if addr_fmt != AF_CLASSIC:
|
if addr_fmt not in (AF_CLASSIC, AF_P2TR):
|
||||||
esc += "2"
|
esc += "2"
|
||||||
slp_af = addr_fmt
|
slp_af = addr_fmt
|
||||||
if slip132:
|
if slip132:
|
||||||
@ -1103,10 +1105,8 @@ async def export_xpub(label, _2, item):
|
|||||||
if ch == "2":
|
if ch == "2":
|
||||||
slip132 = not slip132
|
slip132 = not slip132
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: continue
|
|
||||||
pth_split = path.split("/")
|
pth_split = path.split("/")
|
||||||
pth_split[-1] = ("%dh" % acct)
|
pth_split[-1] = ("%dh" % acct)
|
||||||
path = "/".join(pth_split)
|
path = "/".join(pth_split)
|
||||||
@ -1148,16 +1148,15 @@ async def electrum_skeleton(a, b, item):
|
|||||||
|
|
||||||
ch = await ux_show_story(electrum_export_story(title), escape='1')
|
ch = await ux_show_story(electrum_export_story(title), escape='1')
|
||||||
|
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
|
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
|
||||||
arg=(af, acct, title, fname_pat))
|
arg=(af, account_num, title, fname_pat))
|
||||||
for af in chains.SINGLESIG_AF
|
for af in chains.SINGLESIG_AF
|
||||||
]
|
]
|
||||||
the_ux.push(MenuSystem(rv))
|
the_ux.push(MenuSystem(rv))
|
||||||
@ -1178,14 +1177,13 @@ async def ss_descriptor_skeleton(_0, _1, item):
|
|||||||
int_ext, allowed_af, ll, f_pattern, direct_way = item.arg
|
int_ext, allowed_af, ll, f_pattern, direct_way = item.arg
|
||||||
addition = " for " + ll
|
addition = " for " + ll
|
||||||
|
|
||||||
acct = 0
|
account_num = 0
|
||||||
if not direct_way:
|
if not direct_way:
|
||||||
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
|
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
|
||||||
|
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
|
account_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if int_ext is None:
|
if int_ext is None:
|
||||||
@ -1197,12 +1195,12 @@ async def ss_descriptor_skeleton(_0, _1, item):
|
|||||||
int_ext = False if ch == "1" else True
|
int_ext = False if ch == "1" else True
|
||||||
|
|
||||||
if len(allowed_af) == 1:
|
if len(allowed_af) == 1:
|
||||||
await make_descriptor_wallet_export(allowed_af[0], acct, int_ext=int_ext,
|
await make_descriptor_wallet_export(allowed_af[0], account_num, int_ext=int_ext,
|
||||||
fname_pattern=f_pattern, direct_way=direct_way)
|
fname_pattern=f_pattern, direct_way=direct_way)
|
||||||
else:
|
else:
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
|
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
|
||||||
arg=(af, acct, int_ext, f_pattern, direct_way))
|
arg=(af, account_num, int_ext, f_pattern, direct_way))
|
||||||
for af in allowed_af
|
for af in allowed_af
|
||||||
]
|
]
|
||||||
the_ux.push(MenuSystem(rv))
|
the_ux.push(MenuSystem(rv))
|
||||||
@ -1216,22 +1214,23 @@ async def key_expression_skeleton_step2(_1, _2, item):
|
|||||||
async def key_expression_skeleton(_0, _1, item):
|
async def key_expression_skeleton(_0, _1, item):
|
||||||
# Export key expression -> [xfp/d/e/r]xpub
|
# Export key expression -> [xfp/d/e/r]xpub
|
||||||
|
|
||||||
acct = 0
|
acct_num = 0
|
||||||
ch = await ux_show_story("This saves a extended key expression."
|
ch = await ux_show_story("This saves a extended key expression."
|
||||||
+ PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
+ PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
|
acct_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# element on 2nd index is address format for signed exports
|
# element on 2nd index is address format for signed exports
|
||||||
# if multisig key use p2pkh
|
# if multisig key use p2pkh
|
||||||
todo = [
|
todo = [
|
||||||
("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH),
|
("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH),
|
||||||
|
("Taproot P2TR", "m/86h/%dh/%dh", AF_P2TR),
|
||||||
("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC),
|
("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC),
|
||||||
("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH),
|
("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH),
|
||||||
("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC),
|
("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC),
|
||||||
|
("Multi P2TR", "m/48h/%dh/%dh/3h", AF_CLASSIC),
|
||||||
("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC),
|
("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1242,9 +1241,11 @@ async def key_expression_skeleton(_0, _1, item):
|
|||||||
|
|
||||||
ct = chains.current_chain().b44_cointype
|
ct = chains.current_chain().b44_cointype
|
||||||
|
|
||||||
rv = [ MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct), af))
|
rv = [
|
||||||
for label, orig_der, af in todo ]
|
MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct_num), af))
|
||||||
rv += [ MenuItem("Custom Path", menu=doit) ]
|
for label, orig_der, af in todo
|
||||||
|
]
|
||||||
|
rv += [MenuItem("Custom Path", menu=doit)]
|
||||||
|
|
||||||
the_ux.push(MenuSystem(rv))
|
the_ux.push(MenuSystem(rv))
|
||||||
|
|
||||||
@ -1292,15 +1293,14 @@ You can then run the commands in Bitcoin Core's console window, \
|
|||||||
without ever connecting this Coldcard to a computer.\
|
without ever connecting this Coldcard to a computer.\
|
||||||
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
||||||
|
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# no choices to be made, just do it.
|
# no choices to be made, just do it.
|
||||||
await make_bitcoin_core_wallet(acct)
|
await make_bitcoin_core_wallet(account_num)
|
||||||
|
|
||||||
|
|
||||||
async def electrum_skeleton_step2(_1, _2, item):
|
async def electrum_skeleton_step2(_1, _2, item):
|
||||||
@ -1314,14 +1314,13 @@ async def _generic_export(prompt, label, f_pattern):
|
|||||||
# like the Multisig export, make a single JSON file with
|
# like the Multisig export, make a single JSON file with
|
||||||
# basically all useful XPUB's in it.
|
# basically all useful XPUB's in it.
|
||||||
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await export_contents(label, lambda: generate_generic_export(acct),
|
await export_contents(label, lambda: generate_generic_export(account_num),
|
||||||
f_pattern, is_json=True)
|
f_pattern, is_json=True)
|
||||||
|
|
||||||
async def generic_skeleton(*A):
|
async def generic_skeleton(*A):
|
||||||
@ -1366,17 +1365,16 @@ async def unchained_capital_export(*a):
|
|||||||
ch = await ux_show_story('''\
|
ch = await ux_show_story('''\
|
||||||
This saves multisig XPUB information required to setup on the Unchained platform. \
|
This saves multisig XPUB information required to setup on the Unchained platform. \
|
||||||
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
xfp = xfp2str(settings.get('xfp', 0))
|
xfp = xfp2str(settings.get('xfp', 0))
|
||||||
fname = 'unchained-%s.json' % xfp
|
fname = 'unchained-%s.json' % xfp
|
||||||
|
|
||||||
await export_contents('Unchained', lambda: generate_unchained_export(acct),
|
await export_contents('Unchained', lambda: generate_unchained_export(account_num),
|
||||||
fname, is_json=True)
|
fname, is_json=True)
|
||||||
|
|
||||||
|
|
||||||
@ -1406,10 +1404,6 @@ async def import_extended_key_as_secret(extended_key, ephemeral, origin=None):
|
|||||||
await seed.set_ephemeral_seed_extended_key(extended_key, origin=origin)
|
await seed.set_ephemeral_seed_extended_key(extended_key, origin=origin)
|
||||||
else:
|
else:
|
||||||
await seed.set_seed_extended_key(extended_key)
|
await seed.set_seed_extended_key(extended_key)
|
||||||
except ValueError:
|
|
||||||
msg = ("Sorry, wasn't able to find a valid extended private key to import. "
|
|
||||||
"It should be at the start of a line, and probably starts with 'xprv'.")
|
|
||||||
await ux_show_story(title="FAILED", msg=msg)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|
||||||
@ -1625,7 +1619,7 @@ async def qr_share_file(_1, _2, item):
|
|||||||
# it's a txn, and we wrote as hex
|
# it's a txn, and we wrote as hex
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
else:
|
else:
|
||||||
assert data[1:4] == bytes(3)
|
assert data[2:8] == bytes(6)
|
||||||
data = b2a_hex(data).decode()
|
data = b2a_hex(data).decode()
|
||||||
elif data[0:5] == b'psbt\xff':
|
elif data[0:5] == b'psbt\xff':
|
||||||
tc = "P"
|
tc = "P"
|
||||||
@ -1780,7 +1774,6 @@ async def list_files(*A):
|
|||||||
assert s not in new_basename, "illegal char"
|
assert s not in new_basename, "illegal char"
|
||||||
uos.rename(path + "/" + basename, path + "/" + new_basename)
|
uos.rename(path + "/" + basename, path + "/" + new_basename)
|
||||||
basename = new_basename
|
basename = new_basename
|
||||||
fn = path + "/" + basename # keep full path in sync (delete/sign use it)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story("Failed to rename the file. " + str(e),
|
await ux_show_story("Failed to rename the file. " + str(e),
|
||||||
title="Failure")
|
title="Failure")
|
||||||
@ -1979,20 +1972,22 @@ async def batch_sign(_1, _2, item):
|
|||||||
import sys
|
import sys
|
||||||
await ux_show_story("FAILURE: batch sign failed\n\n" + problem_file_line(e))
|
await ux_show_story("FAILURE: batch sign failed\n\n" + problem_file_line(e))
|
||||||
|
|
||||||
|
async def _ready2sign(intro="", probe=True, miniscript_wallet=None):
|
||||||
async def ready2sign(*a):
|
# - if probe=True -> check if any signable in SD card (A slot on Q), if so do it
|
||||||
# Top menu choice of top menu! Signing!
|
# - if probe=False -> offer all enabled import options via UX
|
||||||
# - check if any signable in SD card, if so do it
|
|
||||||
# - if no card, check virtual disk for PSBT
|
# - if no card, check virtual disk for PSBT
|
||||||
# - if still nothing, then talk about USB connection
|
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
from glob import NFC
|
from glob import NFC
|
||||||
|
|
||||||
opt = {}
|
opt = {}
|
||||||
|
choices = []
|
||||||
|
sb_only = False
|
||||||
|
|
||||||
# just check if we have candidates, no UI
|
if probe:
|
||||||
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
# just check if we have candidates, no UI
|
||||||
max_size=MAX_TXN_LEN, taster=is_psbt)
|
sb_only = True
|
||||||
|
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
||||||
|
max_size=MAX_TXN_LEN, taster=is_psbt)
|
||||||
|
|
||||||
if pa.tmp_value:
|
if pa.tmp_value:
|
||||||
title = '[%s]' % xfp2str(settings.get('xfp'))
|
title = '[%s]' % xfp2str(settings.get('xfp'))
|
||||||
@ -2000,21 +1995,13 @@ async def ready2sign(*a):
|
|||||||
title = None
|
title = None
|
||||||
|
|
||||||
if not choices:
|
if not choices:
|
||||||
msg = '''Coldcard is ready to sign spending transactions!
|
|
||||||
|
|
||||||
Put the proposed transaction onto MicroSD card \
|
|
||||||
in PSBT format (Partially Signed Bitcoin Transaction) \
|
|
||||||
or upload a transaction to be signed \
|
|
||||||
from your desktop wallet software or command line tools.'''
|
|
||||||
|
|
||||||
footnotes = ("You will always be prompted to confirm the details "
|
|
||||||
"before any signature is performed.")
|
|
||||||
|
|
||||||
# if we have only one SD card inserted, at this point, we know no PSBTs on them
|
# if we have only one SD card inserted, at this point, we know no PSBTs on them
|
||||||
# as above file_picker already checked
|
# as above file_picker already checked
|
||||||
# if we have both inserted, A was already checked - so only care about B
|
# if we have both inserted, A was already checked - so only care about B
|
||||||
picked = await import_export_prompt("PSBT", is_import=True, intro=msg,
|
footnotes = ("You will always be prompted to confirm the details "
|
||||||
footnotes=footnotes, slot_b_only=True,
|
"before any signature is performed.")
|
||||||
|
picked = await import_export_prompt("PSBT", is_import=True, intro=intro,
|
||||||
|
footnotes=footnotes, slot_b_only=sb_only,
|
||||||
title=title)
|
title=title)
|
||||||
if isinstance(picked, dict):
|
if isinstance(picked, dict):
|
||||||
opt = picked # reset options to what was chosen by user
|
opt = picked # reset options to what was chosen by user
|
||||||
@ -2027,9 +2014,9 @@ from your desktop wallet software or command line tools.'''
|
|||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if NFC and picked == KEY_NFC:
|
if NFC and picked == KEY_NFC:
|
||||||
await NFC.start_psbt_rx()
|
await NFC.start_psbt_rx(miniscript_wallet)
|
||||||
if picked == KEY_QR:
|
if picked == KEY_QR:
|
||||||
await _scan_any_qr()
|
await _scan_any_qr(miniscript_wallet=miniscript_wallet)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2045,10 +2032,23 @@ from your desktop wallet software or command line tools.'''
|
|||||||
|
|
||||||
# start the process
|
# start the process
|
||||||
from auth import sign_psbt_file
|
from auth import sign_psbt_file
|
||||||
|
opt["miniscript_wallet"] = miniscript_wallet
|
||||||
await sign_psbt_file(input_psbt, **opt)
|
await sign_psbt_file(input_psbt, **opt)
|
||||||
|
|
||||||
|
|
||||||
|
async def ready2sign(*a):
|
||||||
|
# Top menu choice of top menu! Signing!
|
||||||
|
# - check if any signable in SD card, if so do it
|
||||||
|
# - if no card, check virtual disk for PSBT
|
||||||
|
|
||||||
|
await _ready2sign('''Coldcard is ready to sign spending transactions!
|
||||||
|
|
||||||
|
Put the proposed transaction onto MicroSD card \
|
||||||
|
in PSBT format (Partially Signed Bitcoin Transaction) \
|
||||||
|
or upload a transaction to be signed \
|
||||||
|
from your desktop wallet software or command line tools.''')
|
||||||
|
|
||||||
|
|
||||||
async def sign_message_on_sd(*a):
|
async def sign_message_on_sd(*a):
|
||||||
# Menu item: choose a file to be signed (as a short text message)
|
# Menu item: choose a file to be signed (as a short text message)
|
||||||
#
|
#
|
||||||
@ -2313,7 +2313,7 @@ async def wipe_address_cache(*a):
|
|||||||
async def wipe_ovc(*a):
|
async def wipe_ovc(*a):
|
||||||
ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
|
ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
|
||||||
This data protects you against specific attacks. Use this only if certain a false-positive \
|
This data protects you against specific attacks. Use this only if certain a false-positive \
|
||||||
has occurred in the detection logic.''')
|
has occured in the detection logic.''')
|
||||||
if not ok: return
|
if not ok: return
|
||||||
|
|
||||||
import history
|
import history
|
||||||
@ -2444,14 +2444,11 @@ async def scan_any_qr(menu, label, item):
|
|||||||
expect_secret, tmp = item.arg
|
expect_secret, tmp = item.arg
|
||||||
await _scan_any_qr(expect_secret, tmp)
|
await _scan_any_qr(expect_secret, tmp)
|
||||||
|
|
||||||
async def _scan_any_qr(expect_secret=False, tmp=False):
|
async def _scan_any_qr(expect_secret=False, tmp=False, miniscript_wallet=None):
|
||||||
from ux_q1 import QRScannerInteraction
|
from ux_q1 import QRScannerInteraction
|
||||||
x = QRScannerInteraction()
|
x = QRScannerInteraction()
|
||||||
try:
|
await x.scan_anything(expect_secret=expect_secret, tmp=tmp,
|
||||||
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
|
miniscript_wallet=miniscript_wallet)
|
||||||
except Exception as e:
|
|
||||||
await ux_show_story(msg="Failed to import from QR.\n\n%s\n%s" % (e, problem_file_line(e)),
|
|
||||||
title="ERROR")
|
|
||||||
|
|
||||||
|
|
||||||
PUSHTX_SUPPLIERS = [
|
PUSHTX_SUPPLIERS = [
|
||||||
|
|||||||
@ -7,27 +7,17 @@
|
|||||||
import chains, stash, version
|
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, ToggleMenuItem
|
||||||
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_CLASSIC
|
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_P2TR, AF_CLASSIC
|
||||||
from multisig import MultisigWallet
|
from wallet 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 msgsign import write_sig_file
|
from msgsign import write_sig_file
|
||||||
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
|
||||||
from utils import show_single_address, problem_file_line, truncate_address
|
from utils import show_single_address, problem_file_line, truncate_address
|
||||||
|
|
||||||
def censor_address(addr):
|
|
||||||
# We don't like to show the user full multisig addresses because we cannot be certain
|
|
||||||
# they could actually be signed. And yet, don't blank too many
|
|
||||||
# spots or else an attacker could grind out a suitable replacement.
|
|
||||||
# 3 chars in the middle hidden by default
|
|
||||||
# censoring can be disabled by msas setting
|
|
||||||
if settings.get("msas", 0):
|
|
||||||
return addr
|
|
||||||
return addr[0:12] + '___' + addr[12+3:]
|
|
||||||
|
|
||||||
class KeypathMenu(MenuSystem):
|
class KeypathMenu(MenuSystem):
|
||||||
def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
|
def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
|
||||||
@ -42,6 +32,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", f=self.done),
|
MenuItem("m", f=self.done),
|
||||||
]
|
]
|
||||||
if self.ranged:
|
if self.ranged:
|
||||||
@ -74,7 +65,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:]
|
||||||
@ -115,7 +106,7 @@ class KeypathMenu(MenuSystem):
|
|||||||
val = item.arg or item.label
|
val = item.arg or item.label
|
||||||
assert val.endswith('/⋯')
|
assert val.endswith('/⋯')
|
||||||
cpath = val[:-2]
|
cpath = val[:-2]
|
||||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
|
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
|
||||||
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
|
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
|
||||||
|
|
||||||
class PickAddrFmtMenu(MenuSystem):
|
class PickAddrFmtMenu(MenuSystem):
|
||||||
@ -126,10 +117,9 @@ class PickAddrFmtMenu(MenuSystem):
|
|||||||
for af in chains.SINGLESIG_AF
|
for af in chains.SINGLESIG_AF
|
||||||
]
|
]
|
||||||
super().__init__(items)
|
super().__init__(items)
|
||||||
# below is sensitive to order in chains.SINGLESIG_AF
|
if path.startswith("m/84h"):
|
||||||
if path.startswith("m/44h"):
|
|
||||||
self.goto_idx(1)
|
self.goto_idx(1)
|
||||||
elif path.startswith("m/49h"):
|
if path.startswith("m/49h"):
|
||||||
self.goto_idx(2)
|
self.goto_idx(2)
|
||||||
|
|
||||||
async def done(self, _1, _2, item):
|
async def done(self, _1, _2, item):
|
||||||
@ -219,10 +209,15 @@ class AddressListMenu(MenuSystem):
|
|||||||
items.append(MenuItem("Account Number", f=self.change_account))
|
items.append(MenuItem("Account Number", f=self.change_account))
|
||||||
items.append(MenuItem("Custom Path", menu=self.make_custom))
|
items.append(MenuItem("Custom Path", menu=self.make_custom))
|
||||||
|
|
||||||
# if they have MS wallets, add those next
|
# if they have miniscript wallets, add those next
|
||||||
for ms in MultisigWallet.iter_wallets():
|
if MiniScriptWallet.exists():
|
||||||
if not ms.addr_fmt: continue
|
items.append(ToggleMenuItem('MS Scripts/Derivs', 'aemscsv',
|
||||||
items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms))
|
['Default Off', 'Enable'], story=(
|
||||||
|
"Enable this option to add script(s) and derivations to the CSV export"
|
||||||
|
" of Multisig/Miniscript wallets. Default is to only export addresses.")))
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
@ -242,15 +237,11 @@ class AddressListMenu(MenuSystem):
|
|||||||
self.goto_idx(axi)
|
self.goto_idx(axi)
|
||||||
|
|
||||||
async def change_account(self, *a):
|
async def change_account(self, *a):
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
self.account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
self.account_num = acct
|
|
||||||
await self.render()
|
await self.render()
|
||||||
|
|
||||||
async def change_start_idx(self, *a):
|
async def change_start_idx(self, *a):
|
||||||
idx = await ux_enter_bip32_index("Start index:", unlimited=True)
|
self.start = await ux_enter_bip32_index("Start index:", unlimited=True)
|
||||||
if idx is None: return
|
|
||||||
self.start = idx
|
|
||||||
await self.render()
|
await self.render()
|
||||||
|
|
||||||
async def pick_single(self, _1, _2, item):
|
async def pick_single(self, _1, _2, item):
|
||||||
@ -258,10 +249,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
|
||||||
@ -287,14 +278,13 @@ Press (3) if you really understand and accept these risks.
|
|||||||
async def show_n_addresses(self, path, addr_fmt, ms_wallet, start=0, n=10, allow_change=True):
|
async def show_n_addresses(self, path, addr_fmt, ms_wallet, start=0, n=10, allow_change=True):
|
||||||
# Displays n addresses by replacing {idx} in path format.
|
# Displays n addresses by replacing {idx} in path format.
|
||||||
# - also for other {account} numbers
|
# - also for other {account} numbers
|
||||||
# - or multisig case
|
# - or miniscript case
|
||||||
from glob import dis, NFC
|
from glob import dis, NFC
|
||||||
from wallet import MAX_BIP32_IDX
|
from wallet import MAX_BIP32_IDX
|
||||||
|
|
||||||
start = self.start
|
start = self.start
|
||||||
allow_qr = (not ms_wallet) or settings.get("msas", 0)
|
|
||||||
|
|
||||||
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))
|
||||||
@ -307,23 +297,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: do not show complete address unless user opt-in
|
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):
|
|
||||||
addr = censor_address(addr)
|
|
||||||
addrs.append(addr)
|
|
||||||
|
|
||||||
if idx == 0 and ms_wallet.N <= 4:
|
|
||||||
msg += '\n'.join(paths) + '\n =>\n'
|
|
||||||
else:
|
|
||||||
msg += '⋯/%d/%d =>\n' % (change, idx)
|
|
||||||
|
|
||||||
msg += show_single_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
|
||||||
@ -341,11 +315,10 @@ Press (3) if you really understand and accept these risks.
|
|||||||
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(
|
export_msg, escape = export_prompt_builder(
|
||||||
'address summary file',
|
'address summary file',
|
||||||
no_qr=not allow_qr,
|
|
||||||
key0=k0, force_prompt=True
|
key0=k0, 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"
|
||||||
|
|
||||||
@ -357,13 +330,14 @@ Press (3) if you really understand and accept these risks.
|
|||||||
if n:
|
if n:
|
||||||
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
|
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
|
||||||
else:
|
else:
|
||||||
escape += "0"
|
if addr_fmt != AF_P2TR:
|
||||||
msg += " Press (0) to sign message with this key."
|
escape += "0"
|
||||||
|
msg += " Press (0) to sign message with this key."
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@ -385,10 +359,9 @@ Press (3) if you really understand and accept these risks.
|
|||||||
|
|
||||||
elif choice == KEY_QR:
|
elif choice == KEY_QR:
|
||||||
from ux import show_qr_codes
|
from ux import show_qr_codes
|
||||||
if allow_qr:
|
addr_fmt = addr_fmt or ms_wallet.addr_fmt
|
||||||
addr_fmt = addr_fmt or ms_wallet.addr_fmt
|
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, is_addrs=True)
|
||||||
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -432,7 +405,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
|
||||||
@ -440,26 +413,11 @@ 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'
|
|
||||||
|
|
||||||
# saver will be None if we don't think it worth saving these addresses
|
# saver will be None if we don't think it worth saving these addresses
|
||||||
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
|
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
|
||||||
|
|
||||||
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, saver=saver):
|
||||||
if saver:
|
yield line
|
||||||
saver(addr, idx)
|
|
||||||
|
|
||||||
# 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, 0) # close cache file
|
saver(None, 0) # close cache file
|
||||||
@ -474,7 +432,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
|||||||
saver = OWNERSHIP.saver(main, change, start, n)
|
saver = OWNERSHIP.saver(main, change, start, n)
|
||||||
|
|
||||||
yield '"Index","Payment Address","Derivation"\n'
|
yield '"Index","Payment Address","Derivation"\n'
|
||||||
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
|
for (idx, addr, deriv) in main.yield_addresses(start, n, change):
|
||||||
if saver:
|
if saver:
|
||||||
saver(addr, idx)
|
saver(addr, idx)
|
||||||
|
|
||||||
@ -512,18 +470,23 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
|||||||
h.update(ep)
|
h.update(ep)
|
||||||
dis.progress_sofar(idx, count or 1)
|
dis.progress_sofar(idx, count or 1)
|
||||||
|
|
||||||
|
sig_nice = None
|
||||||
if ms_wallet:
|
if ms_wallet:
|
||||||
# sign with my key at the same path as first address of export
|
# sign with my key at the same path as first address of export
|
||||||
addr_fmt = AF_CLASSIC
|
addr_fmt = AF_CLASSIC
|
||||||
derive = ms_wallet.get_my_deriv(settings.get('xfp'))
|
derive = ms_wallet.get_my_deriv()
|
||||||
derive += "/%d/%d" % (change, start)
|
derive += "/%d/%d" % (change, start)
|
||||||
else:
|
else:
|
||||||
|
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2TR else addr_fmt
|
||||||
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)
|
||||||
|
|
||||||
await ux_show_story("Address summary file written:\n\n%s\n\nAddress"
|
|
||||||
" signature file written:\n\n%s" % (nice, sig_nice))
|
msg = '''Address summary file written:\n\n%s''' % nice
|
||||||
|
if sig_nice:
|
||||||
|
msg += "\n\nAddress signature file written:\n\n%s" % sig_nice
|
||||||
|
await ux_show_story(msg)
|
||||||
|
|
||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
|
|||||||
507
shared/auth.py
507
shared/auth.py
@ -8,7 +8,8 @@ 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 AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS
|
from ustruct import pack
|
||||||
|
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS, AF_P2TR
|
||||||
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH, AF_P2WPKH_P2SH
|
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH, AF_P2WPKH_P2SH
|
||||||
from sffile import SFFile
|
from sffile import SFFile
|
||||||
from menu import MenuSystem, MenuItem
|
from menu import MenuSystem, MenuItem
|
||||||
@ -131,7 +132,7 @@ Press %s to continue, otherwise %s to cancel.''' % (OK, X)
|
|||||||
|
|
||||||
class ApproveMessageSign(UserAuthorizedAction):
|
class ApproveMessageSign(UserAuthorizedAction):
|
||||||
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
|
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
|
||||||
msg_sign_request=None, allow_tab_nl=False, privkey=None):
|
msg_sign_request=None, only_printable=True, privkey=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
is_json = False
|
is_json = False
|
||||||
|
|
||||||
@ -141,13 +142,17 @@ class ApproveMessageSign(UserAuthorizedAction):
|
|||||||
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
|
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
|
||||||
|
|
||||||
self.text = validate_text_for_signing(
|
self.text = validate_text_for_signing(
|
||||||
text, allow_tab_nl=is_json and allow_tab_nl
|
text, only_printable=not is_json and only_printable
|
||||||
)
|
)
|
||||||
self.subpath = cleanup_deriv_path(subpath)
|
self.subpath = cleanup_deriv_path(subpath)
|
||||||
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
|
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
|
||||||
self.approved_cb = approved_cb
|
self.approved_cb = approved_cb
|
||||||
self.privkey = privkey
|
self.privkey = privkey
|
||||||
|
|
||||||
|
# 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...')
|
||||||
|
|
||||||
@ -203,7 +208,7 @@ def sign_msg(text, subpath, addr_fmt):
|
|||||||
|
|
||||||
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
||||||
msg_sign_request=None, kill_menu=False,
|
msg_sign_request=None, kill_menu=False,
|
||||||
allow_tab_nl=False, privkey=None):
|
only_printable=True, privkey=None):
|
||||||
|
|
||||||
# Ask user if they want to sign some short text message.
|
# Ask user if they want to sign some short text message.
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
@ -213,7 +218,7 @@ async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
|||||||
text, subpath, addr_fmt,
|
text, subpath, addr_fmt,
|
||||||
approved_cb=approved_cb,
|
approved_cb=approved_cb,
|
||||||
msg_sign_request=msg_sign_request,
|
msg_sign_request=msg_sign_request,
|
||||||
allow_tab_nl=allow_tab_nl,
|
only_printable=only_printable,
|
||||||
privkey=privkey
|
privkey=privkey
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -271,7 +276,8 @@ async def try_push_tx(data, txid, txn_sha=None):
|
|||||||
|
|
||||||
class ApproveTransaction(UserAuthorizedAction):
|
class ApproveTransaction(UserAuthorizedAction):
|
||||||
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
|
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
|
||||||
output_encoder=None, filename=None, offset=TXN_INPUT_OFFSET):
|
output_encoder=None, filename=None, miniscript_wallet=None,
|
||||||
|
offset=TXN_INPUT_OFFSET):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.psbt_len = psbt_len
|
self.psbt_len = psbt_len
|
||||||
@ -291,6 +297,57 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.result = None # will be (len, sha256) of the resulting PSBT
|
self.result = None # will be (len, sha256) of the resulting PSBT
|
||||||
self.chain = chains.current_chain()
|
self.chain = chains.current_chain()
|
||||||
|
self.miniscript_wallet = miniscript_wallet
|
||||||
|
|
||||||
|
async def por322_msg_verify(self):
|
||||||
|
# https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c
|
||||||
|
from glob import NFC
|
||||||
|
from ux import import_export_prompt
|
||||||
|
from actions import file_picker
|
||||||
|
ch = await import_export_prompt("message", is_import=True, force_prompt=True,
|
||||||
|
intro="Import msg that hashes to 'to_spend' msg hash.",
|
||||||
|
key0="to input message manually",
|
||||||
|
title="BIP-322 Messsage" if version.has_qwerty else 'BIP-322 MSG',
|
||||||
|
no_qr=not version.has_qwerty)
|
||||||
|
|
||||||
|
# single sha256 of b'BIP0322-signed-message'
|
||||||
|
bip322_tag_hash = b'te\x84\xa1\x87/\xa1\x00AUN\xff\xa08\xd6\x12IB\xddy\xb4\xe5\x8aL\xda\x18N\x13\xdb\xe6,I'
|
||||||
|
|
||||||
|
if ch == KEY_CANCEL:
|
||||||
|
return
|
||||||
|
elif ch == "0":
|
||||||
|
msg = await ux_input_text("", confirm_exit=False)
|
||||||
|
elif ch == KEY_NFC:
|
||||||
|
msg = await NFC.read_bip322_msg()
|
||||||
|
elif ch == KEY_QR:
|
||||||
|
from ux_q1 import QRScannerInteraction
|
||||||
|
msg = await QRScannerInteraction().scan_text('Scan message from a QR code')
|
||||||
|
else:
|
||||||
|
choices = await file_picker(suffix='.txt', ux=False, **ch)
|
||||||
|
target = "%s.txt" % b2a_hex(self.psbt.por322_msg_hash).decode()
|
||||||
|
|
||||||
|
for fname, dir, _ in choices:
|
||||||
|
if target == fname:
|
||||||
|
fn = dir + "/" + fname
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
fn = await file_picker(choices=choices, **ch)
|
||||||
|
|
||||||
|
if not fn: return
|
||||||
|
|
||||||
|
with CardSlot(readonly=True, **ch) as card:
|
||||||
|
with open(fn, 'rt') as fd:
|
||||||
|
msg = fd.read()
|
||||||
|
|
||||||
|
assert msg, "need msg"
|
||||||
|
msg_hash = ngu.hash.sha256t(bip322_tag_hash, msg, True)
|
||||||
|
assert msg_hash == self.psbt.por322_msg_hash, "hash verification failed"
|
||||||
|
ch = await ux_show_story(
|
||||||
|
msg+"\n\nPress %s to approve message, otherwise %s to exit." % (OK, X),
|
||||||
|
title="Message:"
|
||||||
|
)
|
||||||
|
return True if ch == "y" else False
|
||||||
|
|
||||||
|
|
||||||
def render_output(self, o):
|
def render_output(self, o):
|
||||||
# Pretty-print a transactions output.
|
# Pretty-print a transactions output.
|
||||||
@ -355,6 +412,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
# NOTE: psbtObject captures the file descriptor and uses it later
|
# NOTE: psbtObject captures the file descriptor and uses it later
|
||||||
self.psbt = psbtObject.read_psbt(fd)
|
self.psbt = psbtObject.read_psbt(fd)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
|
# sys.print_exception(exc)
|
||||||
if isinstance(exc, MemoryError):
|
if isinstance(exc, MemoryError):
|
||||||
msg = "Transaction is too complex"
|
msg = "Transaction is too complex"
|
||||||
exc = None
|
exc = None
|
||||||
@ -364,29 +422,25 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
return await self.failure(msg, exc)
|
return await self.failure(msg, exc)
|
||||||
|
|
||||||
dis.fullscreen("Validating...")
|
dis.fullscreen("Validating...")
|
||||||
|
self.psbt.active_miniscript = self.miniscript_wallet
|
||||||
|
|
||||||
# Do some analysis/ validation
|
# Do some analysis/ validation
|
||||||
try:
|
try:
|
||||||
await self.psbt.validate() # might do UX: accept multisig import
|
await self.psbt.validate() # might do UX: accept multisig import
|
||||||
dis.progress_sofar(10, 100)
|
|
||||||
|
|
||||||
if not self.psbt.wif_store:
|
|
||||||
self.psbt.consider_keys()
|
|
||||||
dis.progress_sofar(20, 100)
|
|
||||||
|
|
||||||
ccc_c_xfp = CCCFeature.get_xfp() # can be None
|
ccc_c_xfp = CCCFeature.get_xfp() # can be None
|
||||||
self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
|
args = self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
|
||||||
if self.psbt.wif_store:
|
self.psbt.consider_outputs(*args, cosign_xfp=ccc_c_xfp)
|
||||||
self.psbt.consider_keys()
|
del args # not needed anymore
|
||||||
dis.progress_sofar(50, 100)
|
# we can properly assess sighash only after we know
|
||||||
|
# which outputs are change
|
||||||
self.psbt.consider_outputs()
|
|
||||||
dis.progress_sofar(75, 100)
|
|
||||||
|
|
||||||
self.psbt.consider_dangerous_sighash()
|
self.psbt.consider_dangerous_sighash()
|
||||||
dis.progress_sofar(90, 100)
|
|
||||||
|
if self.psbt.session:
|
||||||
|
self.psbt.session.update(pack('<I', self.psbt.lock_time))
|
||||||
|
|
||||||
except FraudulentChangeOutput as exc:
|
except FraudulentChangeOutput as exc:
|
||||||
|
# sys.print_exception(exc)
|
||||||
#print('FraudulentChangeOutput: ' + exc.args[0])
|
#print('FraudulentChangeOutput: ' + exc.args[0])
|
||||||
return await self.failure(exc.args[0], title='Change Fraud')
|
return await self.failure(exc.args[0], title='Change Fraud')
|
||||||
except FatalPSBTIssue as exc:
|
except FatalPSBTIssue as exc:
|
||||||
@ -428,7 +482,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
#
|
#
|
||||||
try:
|
try:
|
||||||
msg = uio.StringIO()
|
msg = uio.StringIO()
|
||||||
is_por = self.psbt.por322 and (self.psbt.num_inputs > 1)
|
|
||||||
|
|
||||||
# mention warning at top
|
# mention warning at top
|
||||||
wl= len(self.psbt.warnings)
|
wl= len(self.psbt.warnings)
|
||||||
@ -438,16 +491,25 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
msg.write('(%d warnings below)\n\n' % wl)
|
msg.write('(%d warnings below)\n\n' % wl)
|
||||||
|
|
||||||
if self.psbt.por322:
|
if self.psbt.por322:
|
||||||
msg.write("%s\n\n" % ("Proof of Reserves" if is_por else "BIP-322 Message"))
|
|
||||||
msg.write("Message:\n%s\n\n" % self.psbt.por322_msg)
|
|
||||||
if is_por:
|
|
||||||
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
|
|
||||||
try:
|
try:
|
||||||
addr = self.chain.render_address(self.psbt.por322_msg_challenge)
|
if not await self.por322_msg_verify():
|
||||||
msg.write("Challenge Address:\n%s\n\n" % show_single_address(addr))
|
self.refused = True
|
||||||
except ValueError:
|
await ux_dramatic_pause("Refused.", 1)
|
||||||
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
|
self.done()
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
return await self.failure("Msg verification failed.", exc)
|
||||||
|
|
||||||
|
msg.write("Proof of Reserves\n\n")
|
||||||
|
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
|
||||||
|
msg.write("Message Hash:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_hash).decode())
|
||||||
|
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
|
||||||
else:
|
else:
|
||||||
|
if self.psbt.active_miniscript:
|
||||||
|
# show name of the multisig/miniscript wallet that we signed with
|
||||||
|
msg.write("Wallet: " + self.psbt.active_miniscript.name + "\n\n")
|
||||||
|
|
||||||
if self.psbt.consolidation_tx:
|
if self.psbt.consolidation_tx:
|
||||||
# consolidating txn that doesn't change balance of account.
|
# consolidating txn that doesn't change balance of account.
|
||||||
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
|
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
|
||||||
@ -460,18 +522,15 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
if fee is not None:
|
if fee is not None:
|
||||||
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
|
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
|
||||||
|
|
||||||
if not self.psbt.por322 or is_por:
|
msg.write(" %d %s\n %d %s\n\n" % (
|
||||||
msg.write(" %d %s\n %d %s\n\n" % (
|
self.psbt.num_inputs,
|
||||||
self.psbt.num_inputs,
|
"input" if self.psbt.num_inputs == 1 else "inputs",
|
||||||
"input" if self.psbt.num_inputs == 1 else "inputs",
|
self.psbt.num_outputs,
|
||||||
self.psbt.num_outputs,
|
"output" if self.psbt.num_outputs == 1 else "outputs",
|
||||||
"output" if self.psbt.num_outputs == 1 else "outputs",
|
))
|
||||||
))
|
|
||||||
|
|
||||||
if not self.psbt.por322:
|
|
||||||
# outputs + change story created here
|
|
||||||
self.output_summary_text(msg)
|
|
||||||
|
|
||||||
|
# outputs + change story created here
|
||||||
|
self.output_summary_text(msg)
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
if self.psbt.ux_notes:
|
if self.psbt.ux_notes:
|
||||||
@ -499,13 +558,8 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
|
|
||||||
if not hsm_active:
|
if not hsm_active:
|
||||||
esc = "2"
|
esc = "2"
|
||||||
noun = "transaction"
|
msg.write("Press %s to approve and sign transaction."
|
||||||
if self.psbt.por322:
|
" Press (2) to explore transaction." % OK)
|
||||||
noun = "proof of reserves" if is_por else "message"
|
|
||||||
|
|
||||||
msg.write("Press %s to approve and sign %s."
|
|
||||||
" Press (2) to explore transaction." % (OK, noun))
|
|
||||||
|
|
||||||
if (self.input_method == "sd") and CardSlot.both_inserted():
|
if (self.input_method == "sd") and CardSlot.both_inserted():
|
||||||
esc += "b"
|
esc += "b"
|
||||||
msg.write(" (B) to write to lower SD slot.")
|
msg.write(" (B) to write to lower SD slot.")
|
||||||
@ -577,7 +631,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
CCCFeature.sign_psbt(self.psbt)
|
CCCFeature.sign_psbt(self.psbt)
|
||||||
|
|
||||||
if SSSPFeature.is_enabled():
|
if SSSPFeature.is_enabled():
|
||||||
# update SSSP block_h even if SSSP blocks and overridden by CCC
|
# capture new min-height for velocity limit
|
||||||
SSSPFeature.update_last_signed(self.psbt)
|
SSSPFeature.update_last_signed(self.psbt)
|
||||||
|
|
||||||
except FraudulentChangeOutput as exc:
|
except FraudulentChangeOutput as exc:
|
||||||
@ -586,6 +640,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
msg = "Transaction is too complex"
|
msg = "Transaction is too complex"
|
||||||
return await self.failure(msg)
|
return await self.failure(msg)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
|
# sys.print_exception(exc)
|
||||||
return await self.failure("Signing failed late", exc)
|
return await self.failure("Signing failed late", exc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -651,8 +706,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
has_change = True
|
has_change = True
|
||||||
total_change += tx_out.nValue
|
total_change += tx_out.nValue
|
||||||
if len(largest_change) < MAX_VISIBLE_CHANGE:
|
if len(largest_change) < MAX_VISIBLE_CHANGE:
|
||||||
_, addr = self.render_output(tx_out)
|
largest_change.append((tx_out.nValue, self.chain.render_address(tx_out.scriptPubKey)))
|
||||||
largest_change.append((tx_out.nValue, addr))
|
|
||||||
if len(largest_change) == MAX_VISIBLE_CHANGE:
|
if len(largest_change) == MAX_VISIBLE_CHANGE:
|
||||||
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
|
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
|
||||||
continue
|
continue
|
||||||
@ -677,9 +731,12 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
continue # too small
|
continue # too small
|
||||||
|
|
||||||
largest.pop(-1)
|
largest.pop(-1)
|
||||||
|
if outp.is_change:
|
||||||
rendered, dest = self.render_output(tx_out)
|
ret = (here, self.chain.render_address(tx_out.scriptPubKey))
|
||||||
largest.insert(keep, (here, dest if outp.is_change else rendered))
|
else:
|
||||||
|
rendered, _ = self.render_output(tx_out)
|
||||||
|
ret = (here, rendered)
|
||||||
|
largest.insert(keep, ret)
|
||||||
|
|
||||||
# foreign outputs (soon to be other people's coins)
|
# foreign outputs (soon to be other people's coins)
|
||||||
visible_out_sum = 0
|
visible_out_sum = 0
|
||||||
@ -718,12 +775,14 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
|
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
|
||||||
|
|
||||||
|
|
||||||
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, input_method="usb", offset=TXN_INPUT_OFFSET):
|
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, miniscript_wallet=None,
|
||||||
|
offset=TXN_INPUT_OFFSET):
|
||||||
# transaction (binary) loaded into PSRAM already, checksum checked
|
# transaction (binary) loaded into PSRAM already, checksum checked
|
||||||
|
# optional miniscript_wallet arg, choose particular enrolled wallet by name to sign
|
||||||
UserAuthorizedAction.check_busy(ApproveTransaction)
|
UserAuthorizedAction.check_busy(ApproveTransaction)
|
||||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||||
psbt_len, flags, psbt_sha=psbt_sha, input_method=input_method,
|
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb",
|
||||||
offset=offset
|
miniscript_wallet=miniscript_wallet, offset=offset
|
||||||
)
|
)
|
||||||
|
|
||||||
# kill any menu stack, and put our thing at the top
|
# kill any menu stack, and put our thing at the top
|
||||||
@ -764,30 +823,30 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
first_time = True
|
first_time = True
|
||||||
msg = None
|
msg = None
|
||||||
title = None
|
title = None
|
||||||
|
base_title = "PSBT " + ("Signed" if psbt.sig_added else "Updated")
|
||||||
|
|
||||||
is_complete = psbt.is_complete()
|
is_complete = psbt.is_complete()
|
||||||
if finalize is not None:
|
if finalize is not None:
|
||||||
# USB case - user can choose whether to attempt finalization
|
# USB case - user can choose whether to attempt finalization
|
||||||
is_complete = finalize
|
is_complete = finalize
|
||||||
|
|
||||||
if psbt.por322:
|
|
||||||
# network txn strips PSBT BIP-32 with paths with pubkey required for verification
|
|
||||||
# overrides --finalize from USB
|
|
||||||
# disable pushTX for BIP-322
|
|
||||||
is_complete = False
|
|
||||||
|
|
||||||
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
|
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
|
||||||
if is_complete:
|
if is_complete:
|
||||||
txid = psbt.finalize(psram)
|
txid = psbt.finalize(psram)
|
||||||
noun = "Finalized TX ready for broadcast"
|
noun = "Finalized TX ready for broadcast"
|
||||||
else:
|
else:
|
||||||
psbt.serialize(psram)
|
psbt.serialize(psram)
|
||||||
noun = "Signed BIP-322 PSBT" if psbt.por322 else "Partly Signed PSBT"
|
noun = "Partly Signed PSBT"
|
||||||
txid = None
|
txid = None
|
||||||
|
|
||||||
data_len = psram.tell()
|
data_len = psram.tell()
|
||||||
data_sha2 = psram.checksum.digest()
|
data_sha2 = psram.checksum.digest()
|
||||||
|
|
||||||
|
# BBQR is at TMP_OUTPUT_OFFSET + 1MB - allowing it in this case would overwrite txn
|
||||||
|
# allow_qr = data_len < (1024*1024)
|
||||||
|
# actual more reasonable limit - as BBQR has some overhead and only 1Mbit of space
|
||||||
|
allow_qr = data_len < (671*1024)
|
||||||
|
|
||||||
if input_method == "usb":
|
if input_method == "usb":
|
||||||
# return result over USB before going to all options
|
# return result over USB before going to all options
|
||||||
tx_req.result = data_len, data_sha2
|
tx_req.result = data_len, data_sha2
|
||||||
@ -798,11 +857,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
|
|
||||||
first_time = False
|
first_time = False
|
||||||
msg = noun + " shared via USB."
|
msg = noun + " shared via USB."
|
||||||
title = "PSBT Signed"
|
title = base_title
|
||||||
|
|
||||||
elif input_method == "kt":
|
|
||||||
first_time = False
|
|
||||||
title = "PSBT Signed"
|
|
||||||
|
|
||||||
if txid and await try_push_tx(data_len, txid, data_sha2):
|
if txid and await try_push_tx(data_len, txid, data_sha2):
|
||||||
# go directly to reexport menu after pushTX
|
# go directly to reexport menu after pushTX
|
||||||
@ -811,17 +866,19 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
|
|
||||||
# for specific cases, key teleport is an option
|
# for specific cases, key teleport is an option
|
||||||
offer_kt = False
|
offer_kt = False
|
||||||
if not is_complete and psbt.active_multisig and version.has_qwerty:
|
if not is_complete and version.has_qwerty and psbt.active_miniscript:
|
||||||
offer_kt = 'use Key Teleport to send PSBT to other co-signers'
|
offer_kt = 'use Key Teleport to send PSBT to other co-signers'
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
ch = None
|
ch = None
|
||||||
if first_time:
|
if first_time:
|
||||||
# first time, assume they want to send out same way it came in -- don't prompt
|
# first time, assume they want to send out same way it came in -- don't prompt
|
||||||
if input_method == "qr":
|
if (input_method == "qr") and allow_qr:
|
||||||
ch = KEY_QR
|
ch = KEY_QR
|
||||||
elif input_method == "nfc":
|
elif input_method == "nfc":
|
||||||
ch = KEY_NFC
|
ch = KEY_NFC
|
||||||
|
elif input_method == "kt":
|
||||||
|
ch = 't'
|
||||||
else:
|
else:
|
||||||
# SD/VDisk
|
# SD/VDisk
|
||||||
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
|
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
|
||||||
@ -841,8 +898,9 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
# In that case this would just return dict and keep producing signed
|
# In that case this would just return dict and keep producing signed
|
||||||
# files on SD infinitely (would never actually prompt).
|
# files on SD infinitely (would never actually prompt).
|
||||||
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
|
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
|
||||||
key6=key6, title=title, force_prompt=not first_time,
|
key6=key6, title=title,
|
||||||
no_qr=not version.has_qwerty)
|
force_prompt=not first_time,
|
||||||
|
no_qr=not version.has_qwerty or not allow_qr)
|
||||||
if ch == KEY_CANCEL:
|
if ch == KEY_CANCEL:
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
break
|
break
|
||||||
@ -853,7 +911,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
|
|
||||||
elif ch == KEY_QR:
|
elif ch == KEY_QR:
|
||||||
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
|
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
|
||||||
msg = txid or noun
|
msg = txid or 'Partly Signed PSBT'
|
||||||
try:
|
try:
|
||||||
if len(here) > 920:
|
if len(here) > 920:
|
||||||
# too big for simple QR - use BBQr instead
|
# too big for simple QR - use BBQr instead
|
||||||
@ -882,14 +940,11 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
|
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
|
||||||
from teleport import kt_send_psbt
|
from teleport import kt_send_psbt
|
||||||
ok = await kt_send_psbt(psbt, data_len, psbt_offset=TXN_OUTPUT_OFFSET)
|
ok = await kt_send_psbt(psbt, data_len, psbt_offset=TXN_OUTPUT_OFFSET)
|
||||||
if ok is None:
|
if ok:
|
||||||
title = "Failed to Teleport"
|
|
||||||
else:
|
|
||||||
title = "Sent by Teleport"
|
title = "Sent by Teleport"
|
||||||
_, num_sigs_needed = ok
|
else:
|
||||||
if num_sigs_needed > 0:
|
title = "Failed to Teleport"
|
||||||
s, aux = ("", "is") if num_sigs_needed == 1 else ("s", "are")
|
|
||||||
msg = "%d more signature%s %s still required." % (num_sigs_needed, s, aux)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -900,7 +955,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
|
|
||||||
input_method = None
|
input_method = None
|
||||||
first_time = False
|
first_time = False
|
||||||
title = "PSBT Signed"
|
title = base_title
|
||||||
|
|
||||||
async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None):
|
async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None):
|
||||||
# Saving a PSBT from PSRAM to something disk-like.
|
# Saving a PSBT from PSRAM to something disk-like.
|
||||||
@ -1030,7 +1085,8 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False):
|
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False,
|
||||||
|
miniscript_wallet=None):
|
||||||
# sign a PSBT file found on a MicroSD card
|
# sign a PSBT file found on a MicroSD card
|
||||||
# - or from VirtualDisk (mk4)
|
# - or from VirtualDisk (mk4)
|
||||||
# - to re-use reading/decoding logic, pass just_read
|
# - to re-use reading/decoding logic, pass just_read
|
||||||
@ -1088,6 +1144,7 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=Fal
|
|||||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||||
psbt_len, input_method="vdisk" if force_vdisk else "sd",
|
psbt_len, input_method="vdisk" if force_vdisk else "sd",
|
||||||
filename=filename, output_encoder=output_encoder,
|
filename=filename, output_encoder=output_encoder,
|
||||||
|
miniscript_wallet=miniscript_wallet,
|
||||||
)
|
)
|
||||||
if ux_abort:
|
if ux_abort:
|
||||||
# needed for auto vdisk mode
|
# needed for auto vdisk mode
|
||||||
@ -1318,57 +1375,35 @@ class ShowPKHAddress(ShowAddressBase):
|
|||||||
sp=self.subpath)
|
sp=self.subpath)
|
||||||
|
|
||||||
|
|
||||||
class ShowP2SHAddress(ShowAddressBase):
|
class ShowMiniscriptAddress(ShowAddressBase):
|
||||||
|
|
||||||
def setup(self, ms, addr_fmt, xfp_paths, witdeem_script):
|
def setup(self, msc, change, idx):
|
||||||
|
self.msc = msc
|
||||||
|
self.change = change
|
||||||
|
self.idx = idx
|
||||||
|
|
||||||
self.witdeem_script = witdeem_script
|
d = self.msc.to_descriptor().derive(None, change=change).derive(idx)
|
||||||
self.addr_fmt = addr_fmt
|
self.address = self.msc.chain.render_address(d.script_pubkey())
|
||||||
self.ms = ms
|
self.addr_fmt = self.msc.addr_fmt
|
||||||
|
|
||||||
# calculate all the pubkeys involved.
|
|
||||||
self.subpath_help = ms.validate_script(witdeem_script, xfp_paths=xfp_paths)
|
|
||||||
|
|
||||||
self.address = ms.chain.p2sh_address(addr_fmt, witdeem_script)
|
|
||||||
|
|
||||||
def get_msg(self):
|
def get_msg(self):
|
||||||
return '''\
|
return '''\
|
||||||
{addr}
|
{addr}
|
||||||
|
|
||||||
Wallet:
|
Wallet:
|
||||||
|
|
||||||
{name}
|
{name}
|
||||||
{M} of {N}
|
|
||||||
|
|
||||||
Paths:
|
Index:
|
||||||
|
{idx}
|
||||||
|
|
||||||
{sp}'''.format(addr=show_single_address(self.address), name=self.ms.name,
|
Change:
|
||||||
M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help))
|
{change}'''.format(addr=show_single_address(self.address), name=self.msc.name,
|
||||||
|
idx=self.idx, change=bool(self.change))
|
||||||
|
|
||||||
def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script):
|
|
||||||
# Show P2SH address to user, also returns it.
|
|
||||||
# - first need to find appropriate multisig wallet associated
|
|
||||||
# - they must provide full redeem script, and we will re-verify it and check pubkeys inside it
|
|
||||||
|
|
||||||
from multisig import MultisigWallet
|
|
||||||
|
|
||||||
try:
|
|
||||||
assert addr_format in SUPPORTED_ADDR_FORMATS
|
|
||||||
assert addr_format & AFC_SCRIPT
|
|
||||||
except:
|
|
||||||
raise AssertionError('Unknown/unsupported addr format')
|
|
||||||
|
|
||||||
# Search for matching multisig wallet that we must already know about
|
|
||||||
xs = list(xfp_paths)
|
|
||||||
xs.sort()
|
|
||||||
|
|
||||||
ms = MultisigWallet.find_match(M, N, xs)
|
|
||||||
assert ms, 'Multisig wallet with those fingerprints not found'
|
|
||||||
assert ms.M == M
|
|
||||||
assert ms.N == N
|
|
||||||
|
|
||||||
|
def start_show_miniscript_address(msc, change, index):
|
||||||
UserAuthorizedAction.check_busy(ShowAddressBase)
|
UserAuthorizedAction.check_busy(ShowAddressBase)
|
||||||
UserAuthorizedAction.active_request = ShowP2SHAddress(ms, addr_format, xfp_paths, witdeem_script)
|
UserAuthorizedAction.active_request = ShowMiniscriptAddress(msc, change, index)
|
||||||
|
|
||||||
# kill any menu stack, and put our thing at the top
|
# kill any menu stack, and put our thing at the top
|
||||||
abort_and_goto(UserAuthorizedAction.active_request)
|
abort_and_goto(UserAuthorizedAction.active_request)
|
||||||
@ -1376,6 +1411,7 @@ def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script):
|
|||||||
# provide the value back to attached desktop
|
# provide the value back to attached desktop
|
||||||
return UserAuthorizedAction.active_request.address
|
return UserAuthorizedAction.active_request.address
|
||||||
|
|
||||||
|
|
||||||
def show_address(addr_format, subpath, restore_menu=False):
|
def show_address(addr_format, subpath, restore_menu=False):
|
||||||
try:
|
try:
|
||||||
assert addr_format in SUPPORTED_ADDR_FORMATS
|
assert addr_format in SUPPORTED_ADDR_FORMATS
|
||||||
@ -1403,64 +1439,112 @@ 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 wallet 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()
|
approved = await ms.confirm_import()
|
||||||
|
if not approved:
|
||||||
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 wallet import MiniscriptMenu
|
||||||
|
while 1:
|
||||||
|
top = the_ux.top_of_stack()
|
||||||
|
if not top: break
|
||||||
|
if not isinstance(top, MiniscriptMenu):
|
||||||
|
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, desc_obj=None):
|
||||||
|
# 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 wallet import MiniScriptWallet
|
||||||
|
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
dis.fullscreen('Wait...') # needed
|
dis.fullscreen('Wait...')
|
||||||
dis.busy_bar(True)
|
dis.busy_bar(True)
|
||||||
|
|
||||||
|
bip388 = False
|
||||||
try:
|
try:
|
||||||
if sf_len:
|
if desc_obj:
|
||||||
with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd:
|
# caller is sending us already validated descriptor object
|
||||||
config = fd.read(sf_len).decode()
|
assert name
|
||||||
|
msc = MiniScriptWallet.from_descriptor_obj(name, desc_obj)
|
||||||
|
else:
|
||||||
|
if sf_len:
|
||||||
|
with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd:
|
||||||
|
config = fd.read(sf_len).decode()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
j_conf = ujson.loads(config)
|
j_conf = ujson.loads(config)
|
||||||
assert "desc" in j_conf, "'desc' key required"
|
if "desc_template" in j_conf and "keys_info" in j_conf:
|
||||||
config = j_conf["desc"]
|
assert "name" in j_conf
|
||||||
assert config, "'desc' empty"
|
config = j_conf
|
||||||
|
bip388 = True
|
||||||
|
else:
|
||||||
|
assert "desc" in j_conf, "'desc' key required"
|
||||||
|
config = j_conf["desc"]
|
||||||
|
assert config, "'desc' empty"
|
||||||
|
|
||||||
if "name" in j_conf:
|
if "name" in j_conf:
|
||||||
# name from json has preference over filenames and desc checksum
|
# name from json has preference over filenames and desc checksum
|
||||||
name = j_conf["name"]
|
name = j_conf["name"]
|
||||||
assert 2 <= len(name) <= 40, "'name' length"
|
assert 2 <= len(name) <= 40, "'name' length"
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
|
|
||||||
# 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)
|
msc = MiniScriptWallet.from_file(config, name=name, bip388=bip388)
|
||||||
|
|
||||||
UserAuthorizedAction.active_request = NewEnrollRequest(ms)
|
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
|
||||||
@ -1471,9 +1555,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__()
|
||||||
@ -1559,9 +1643,6 @@ class TXExplorer:
|
|||||||
self.qr_msgs = []
|
self.qr_msgs = []
|
||||||
self.title = None
|
self.title = None
|
||||||
|
|
||||||
def can_goto_idx(self):
|
|
||||||
return self.max_items > 1
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def start(cls, user_auth_action):
|
async def start(cls, user_auth_action):
|
||||||
rv = [
|
rv = [
|
||||||
@ -1574,7 +1655,6 @@ class TXExplorer:
|
|||||||
def make_ux_msg(self, offset, count):
|
def make_ux_msg(self, offset, count):
|
||||||
from glob import dis
|
from glob import dis
|
||||||
dis.fullscreen('Wait...')
|
dis.fullscreen('Wait...')
|
||||||
esc = "4"+KEY_QR
|
|
||||||
rv = ""
|
rv = ""
|
||||||
qrs = []
|
qrs = []
|
||||||
change = []
|
change = []
|
||||||
@ -1583,29 +1663,18 @@ class TXExplorer:
|
|||||||
rv += item
|
rv += item
|
||||||
dis.progress_sofar(idx-offset+1, count)
|
dis.progress_sofar(idx-offset+1, count)
|
||||||
|
|
||||||
hints = []
|
rv += 'Press RIGHT to see next group'
|
||||||
if end < self.max_items:
|
|
||||||
hints.append('RIGHT to see next group')
|
|
||||||
esc += KEY_RIGHT + "9"
|
|
||||||
if offset:
|
if offset:
|
||||||
hints.append('LEFT to go back')
|
rv += ', LEFT to go back'
|
||||||
esc += KEY_LEFT + "7"
|
|
||||||
|
|
||||||
if self.can_goto_idx():
|
rv += ", (2) to go to index"
|
||||||
hints.append("(2) to go to index")
|
|
||||||
esc += "2"
|
|
||||||
|
|
||||||
if not version.has_qwerty:
|
if not version.has_qwerty:
|
||||||
# Q has hint key
|
# Q has hint key
|
||||||
hints.append("(4) to show QR code")
|
rv += ", (4) to show QR code"
|
||||||
|
rv += ('. %s to quit.' % X)
|
||||||
|
|
||||||
if hints:
|
return rv, qrs, change, end
|
||||||
rv += 'Press ' + ', '.join(hints)
|
|
||||||
rv += ('. %s to quit.' % X)
|
|
||||||
else:
|
|
||||||
rv += 'Press %s to quit.' % X
|
|
||||||
|
|
||||||
return rv, qrs, change, end, esc
|
|
||||||
|
|
||||||
|
|
||||||
async def explore(self, *a):
|
async def explore(self, *a):
|
||||||
@ -1614,10 +1683,11 @@ class TXExplorer:
|
|||||||
# - shows all inputs: utxo amount and address, txid & tx index.
|
# - shows all inputs: utxo amount and address, txid & tx index.
|
||||||
|
|
||||||
start = 0
|
start = 0
|
||||||
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
|
msg, addrs, change, end = self.make_ux_msg(start, self.n)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
ch = await ux_show_story(msg, title=self.title, hint_icons=KEY_QR, escape=esc)
|
ch = await ux_show_story(msg, title=self.title, escape='2479'+KEY_RIGHT+KEY_LEFT+KEY_QR,
|
||||||
|
hint_icons=KEY_QR)
|
||||||
if ch == 'x':
|
if ch == 'x':
|
||||||
del msg
|
del msg
|
||||||
return
|
return
|
||||||
@ -1639,16 +1709,17 @@ class TXExplorer:
|
|||||||
else:
|
else:
|
||||||
# go forwards
|
# go forwards
|
||||||
start += self.n
|
start += self.n
|
||||||
elif (ch == "2") and (self.max_items > 1):
|
elif ch == "2":
|
||||||
max_v = self.max_items - 1
|
max_v = self.max_items - 1
|
||||||
res = await ux_enter_number("Start Idx (0-%d):" % max_v, max_value=max_v)
|
res = await ux_enter_number("Start Idx (0-%d):" % max_v, max_value=max_v,
|
||||||
|
can_cancel=True)
|
||||||
if res is None: continue
|
if res is None: continue
|
||||||
start = res
|
start = res
|
||||||
else:
|
else:
|
||||||
# nothing changed - do not recalc msg
|
# nothing changed - do not recalc msg
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
|
msg, addrs, change, end = self.make_ux_msg(start, self.n)
|
||||||
|
|
||||||
|
|
||||||
class TXOutExplorer(TXExplorer):
|
class TXOutExplorer(TXExplorer):
|
||||||
@ -1699,11 +1770,9 @@ class TXInpExplorer(TXExplorer):
|
|||||||
item += "=== UTXO ===\n\n%s %s\n\n%s\n\n" % (val, unit, spk)
|
item += "=== UTXO ===\n\n%s %s\n\n%s\n\n" % (val, unit, spk)
|
||||||
if addr:
|
if addr:
|
||||||
item += show_single_address(addr) + "\n\n"
|
item += show_single_address(addr) + "\n\n"
|
||||||
|
item += "Address Format: %s\n\n" % chains.AF_TO_STR_AF[inp.af]
|
||||||
qr_items.append(addr)
|
qr_items.append(addr)
|
||||||
|
|
||||||
if inp.addr_fmt is not None:
|
|
||||||
item += "Address Format: %s\n\n" % chains.addr_fmt_str(inp.addr_fmt)
|
|
||||||
|
|
||||||
if self.user_auth_action.psbt.txn_version >= 2:
|
if self.user_auth_action.psbt.txn_version >= 2:
|
||||||
has_rtl = inp.has_relative_timelock(txin)
|
has_rtl = inp.has_relative_timelock(txin)
|
||||||
if has_rtl:
|
if has_rtl:
|
||||||
@ -1715,45 +1784,56 @@ class TXInpExplorer(TXExplorer):
|
|||||||
|
|
||||||
item += "Input has relative %s\n\n" % msg
|
item += "Input has relative %s\n\n" % msg
|
||||||
|
|
||||||
|
|
||||||
psbt_item = ""
|
psbt_item = ""
|
||||||
if inp.required_key:
|
if inp.sp_idxs:
|
||||||
ws = self.user_auth_action.psbt.wif_store
|
ws = self.user_auth_action.psbt.wif_store
|
||||||
our = [inp.required_key] if isinstance(inp.required_key, bytes) else inp.required_key
|
psbt_item += "Our key%s:\n\n" % ("s" if len(inp.sp_idxs) > 1 else "")
|
||||||
psbt_item += "Our key%s:\n\n" % ("s" if len(our) > 1 else "")
|
for i in inp.sp_idxs:
|
||||||
wif_note = "(WIF Store)"
|
# get node required
|
||||||
for k in our:
|
if inp.taproot_subpaths:
|
||||||
pubkey = b2a_hex(k).decode()
|
pubk = inp.taproot_subpaths[i][0]
|
||||||
pth = inp.subpaths.get(k)
|
sp = inp.taproot_subpaths[i][1][2]
|
||||||
note = ""
|
|
||||||
if pth:
|
|
||||||
label = keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0]))
|
|
||||||
if ws and k in ws:
|
|
||||||
note = "\n" + wif_note
|
|
||||||
psbt_item += "%s:\n%s%s\n\n" % (label, pubkey, note)
|
|
||||||
else:
|
else:
|
||||||
psbt_item += "%s\n%s\n\n" % (pubkey, wif_note)
|
pubk = inp.subpaths[i][0]
|
||||||
|
sp = inp.subpaths[i][1]
|
||||||
|
|
||||||
if inp.is_multisig:
|
pth = inp.parse_xfp_path(sp)
|
||||||
|
k = inp.get(pubk)
|
||||||
|
ws_note = "\n(WIF Store)" if (ws and k in ws) else ""
|
||||||
|
psbt_item += "%s:\n%s%s\n\n" % (keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0])),
|
||||||
|
b2a_hex(k).decode(), ws_note)
|
||||||
|
|
||||||
|
M = None
|
||||||
|
if inp.is_miniscript:
|
||||||
ks_coord = inp.witness_script or inp.redeem_script
|
ks_coord = inp.witness_script or inp.redeem_script
|
||||||
if ks_coord:
|
if ks_coord:
|
||||||
ks = self.user_auth_action.psbt.get(ks_coord)
|
ks = inp.get(ks_coord)
|
||||||
|
|
||||||
from multisig import disassemble_multisig_mn
|
from psbt import disassemble_multisig_mn
|
||||||
try:
|
try:
|
||||||
M, N = disassemble_multisig_mn(ks)
|
M, N = disassemble_multisig_mn(ks)
|
||||||
psbt_item += "Multisig: %dof%d\n\n" % (M, N)
|
psbt_item += "Multisig: %dof%d\n\n" % (M, N)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
if inp.part_sigs:
|
if inp.is_musig:
|
||||||
# do not show XFPs in case input is fully signed --> elif
|
psbt_item += "MuSig2\n\n"
|
||||||
|
|
||||||
|
if inp.part_sigs or inp.taproot_script_sigs:
|
||||||
|
# do not show XFPs in case input is fully signed
|
||||||
# only part_sig should be available, as we haven't signed yet so added_sigs empty
|
# only part_sig should be available, as we haven't signed yet so added_sigs empty
|
||||||
done = []
|
done = []
|
||||||
for pk, pth in inp.subpaths.items():
|
if inp.part_sigs:
|
||||||
if pk in inp.part_sigs:
|
signed = {inp.get(k) for k, _ in inp.part_sigs}
|
||||||
done.append(xfp2str(pth[0]))
|
for pk, pth in inp.subpaths:
|
||||||
|
if inp.get(pk) in signed:
|
||||||
|
done.append(xfp2str(inp.parse_xfp_path(pth)[0]))
|
||||||
|
else: # inp.taproot_script_sigs
|
||||||
|
signed = {xo for xo, _ in inp.get_taproot_script_sigs()}
|
||||||
|
for pk, val in inp.taproot_subpaths:
|
||||||
|
if inp.get(pk) in signed:
|
||||||
|
done.append(xfp2str(inp.parse_xfp_path(val[2])[0]))
|
||||||
|
|
||||||
if inp.fully_signed:
|
if inp.fully_signed or (M and (len(done) >= M)):
|
||||||
psbt_item += "Input fully signed.\n\n"
|
psbt_item += "Input fully signed.\n\n"
|
||||||
else:
|
else:
|
||||||
psbt_item += "Already signed:\n"
|
psbt_item += "Already signed:\n"
|
||||||
@ -1761,14 +1841,15 @@ class TXInpExplorer(TXExplorer):
|
|||||||
psbt_item += " %s\n" % xfp
|
psbt_item += " %s\n" % xfp
|
||||||
psbt_item += "\n"
|
psbt_item += "\n"
|
||||||
|
|
||||||
if inp.sighash and (inp.sighash != SIGHASH_ALL):
|
if inp.sighash is not None:
|
||||||
# only show sighash value to the user if it is non-standard
|
# only show sighash value to the user if it is non-standard for particular script type
|
||||||
psbt_item += "sighash: %s\n\n" % {
|
if (inp.af == AF_P2TR and inp.sighash != 0) or (inp.af != AF_P2TR and inp.sighash != 1):
|
||||||
1: "ALL", 2: "NONE", 3: "SINGLE",
|
psbt_item += "sighash: %s\n\n" % {
|
||||||
1 | 0x80: "ALL|ANYONECANPAY",
|
0: "DEFAULT", 1: "ALL", 2: "NONE", 3: "SINGLE",
|
||||||
2 | 0x80: "NONE|ANYONECANPAY",
|
1 | 0x80: "ALL|ANYONECANPAY",
|
||||||
3 | 0x80: "SINGLE|ANYONECANPAY",
|
2 | 0x80: "NONE|ANYONECANPAY",
|
||||||
}.get(inp.sighash, "0x%02x (non-standard)" % inp.sighash)
|
3 | 0x80: "SINGLE|ANYONECANPAY",
|
||||||
|
}[inp.sighash]
|
||||||
|
|
||||||
if psbt_item:
|
if psbt_item:
|
||||||
psbt_item = "=== PSBT ===\n\n" + psbt_item
|
psbt_item = "=== PSBT ===\n\n" + psbt_item
|
||||||
|
|||||||
@ -49,7 +49,7 @@ def render_backup_contents(bypass_tmp=False):
|
|||||||
if sv.mode == 'words':
|
if sv.mode == 'words':
|
||||||
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
||||||
|
|
||||||
elif sv.mode == 'master':
|
if sv.mode == 'master':
|
||||||
ADD('bip32_master_key', b2a_hex(sv.raw))
|
ADD('bip32_master_key', b2a_hex(sv.raw))
|
||||||
|
|
||||||
ADD('chain', chain.ctype)
|
ADD('chain', chain.ctype)
|
||||||
@ -76,12 +76,7 @@ def render_backup_contents(bypass_tmp=False):
|
|||||||
current_tmp = pa.tmp_value[:]
|
current_tmp = pa.tmp_value[:]
|
||||||
pa.tmp_value = None
|
pa.tmp_value = None
|
||||||
# we also need correct settings from main seed
|
# we also need correct settings from main seed
|
||||||
if sv.mode == 'words':
|
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
||||||
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
|
||||||
else:
|
|
||||||
assert sv.mode == "xprv"
|
|
||||||
nv = stash.SecretStash.encode(xprv=sv.node)
|
|
||||||
|
|
||||||
settings.set_key(nv)
|
settings.set_key(nv)
|
||||||
settings.load()
|
settings.load()
|
||||||
stash.blank_object(nv)
|
stash.blank_object(nv)
|
||||||
@ -206,13 +201,6 @@ def restore_from_dict_ll(vals, raw):
|
|||||||
|
|
||||||
k = key[8:]
|
k = key[8:]
|
||||||
|
|
||||||
if k == 'bkpw':
|
|
||||||
# never import a cached backup password from a backup file.
|
|
||||||
# write-side (render_backup_contents) strips bkpw, so a present
|
|
||||||
# value means a tampered/crafted file trying to fixate the
|
|
||||||
# password used for all FUTURE backups - drop it.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if k == 'sd2fa':
|
if k == 'sd2fa':
|
||||||
# do NOT restore sd2fa as SD card can be lost or damaged
|
# do NOT restore sd2fa as SD card can be lost or damaged
|
||||||
# new version of firmware 5.1.3+ will not back sd2fa
|
# new version of firmware 5.1.3+ will not back sd2fa
|
||||||
@ -303,7 +291,7 @@ async def restore_tmp_from_dict_ll(vals, raw):
|
|||||||
if not k[:8] == "setting.":
|
if not k[:8] == "setting.":
|
||||||
continue
|
continue
|
||||||
key = k[8:]
|
key = k[8:]
|
||||||
if key in ["multisig"]:
|
if key == "miniscript":
|
||||||
# whitelist
|
# whitelist
|
||||||
settings.set(key, v)
|
settings.set(key, v)
|
||||||
|
|
||||||
@ -589,11 +577,7 @@ async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
|
|||||||
# give them a menu to pick from, and start picking
|
# give them a menu to pick from, and start picking
|
||||||
if usb:
|
if usb:
|
||||||
# we're not originating from a menu
|
# we're not originating from a menu
|
||||||
words = await seed.WordNestMenu.get_n_words(num_pw_words)
|
words = await seed.WordNestMenu.get_n_words(12)
|
||||||
if len(words) != num_pw_words:
|
|
||||||
seed.WordNestMenu.pop_all()
|
|
||||||
return
|
|
||||||
|
|
||||||
await done(words)
|
await done(words)
|
||||||
else:
|
else:
|
||||||
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
||||||
|
|||||||
@ -138,15 +138,12 @@ async def batt_idle_logout():
|
|||||||
# - even before login
|
# - even before login
|
||||||
import glob
|
import glob
|
||||||
from uasyncio import sleep_ms
|
from uasyncio import sleep_ms
|
||||||
from glob import settings, dis, SCAN
|
from glob import settings, dis
|
||||||
import utime
|
import utime
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
await sleep_ms(20000) # 20 seconds
|
await sleep_ms(20000) # 20 seconds
|
||||||
|
|
||||||
if SCAN.busy_scanning:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if get_batt_level() is None:
|
if get_batt_level() is None:
|
||||||
# on USB power
|
# on USB power
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -54,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
|
|||||||
if char_len > actual:
|
if char_len > actual:
|
||||||
need += 1
|
need += 1
|
||||||
|
|
||||||
# Challenge: the final QR might have just a few chars in it, if we redistribute
|
# Challenge: the final QR might have just a a few chars in it, if we redistribute
|
||||||
# the data into the other parts, then each QR can have more forward error correction
|
# the data into the other parts, then each QR can have more forward error correction
|
||||||
# and be more robust. Must respect split_mod alignment tho.
|
# and be more robust. Must respect split_mod alignment tho.
|
||||||
level = ceil(char_len / need)
|
level = ceil(char_len / need)
|
||||||
@ -439,18 +439,5 @@ class BBQrPsramStorage(BBQrStorage):
|
|||||||
from glob import PSRAM
|
from glob import PSRAM
|
||||||
return PSRAM.read_at(0, self.final_size)
|
return PSRAM.read_at(0, self.final_size)
|
||||||
|
|
||||||
def finalize(self):
|
|
||||||
self._finalize()
|
|
||||||
|
|
||||||
if self.hdr.encoding == 'Z':
|
|
||||||
self.zlib_decompress()
|
|
||||||
|
|
||||||
# PSBT-typed BBQrs end up at PSRAM[0..size]
|
|
||||||
# skip a redundant PSRAM->heap->PSRAM round-trip
|
|
||||||
if self.hdr.file_type == 'P':
|
|
||||||
return self.hdr.file_type, self.final_size, 'PSRAM'
|
|
||||||
|
|
||||||
return self.hdr.file_type, self.final_size, self.get_buffer()
|
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
||||||
#
|
|
||||||
# AUTO-generated.
|
|
||||||
#
|
|
||||||
# Updated: 2026-06-19 14:13:23 UTC
|
|
||||||
|
|
||||||
BLOCK_HEIGHT = 932301
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
1062
shared/bsms.py
Normal file
1062
shared/bsms.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,6 @@
|
|||||||
# - "hobbled" refers to less-than full control over Coldcard, even though you have main PIN
|
# - "hobbled" refers to less-than full control over Coldcard, even though you have main PIN
|
||||||
#
|
#
|
||||||
import gc, chains, version, ngu, web2fa, bip39, re
|
import gc, chains, version, ngu, web2fa, bip39, re
|
||||||
from ubinascii import hexlify as b2a_hex
|
|
||||||
from chains import NLOCK_IS_TIME
|
from chains import NLOCK_IS_TIME
|
||||||
from utils import swab32, xfp2str, truncate_address, deserialize_secret, show_single_address
|
from utils import swab32, xfp2str, truncate_address, deserialize_secret, show_single_address
|
||||||
from glob import settings, dis
|
from glob import settings, dis
|
||||||
@ -62,20 +61,19 @@ class SpendingPolicy(dict):
|
|||||||
self.update(v.items()) # mpy bugfix, when called with SpendingPolicy
|
self.update(v.items()) # mpy bugfix, when called with SpendingPolicy
|
||||||
|
|
||||||
|
|
||||||
def _save_policy(self, master_only=True):
|
def _save_policy(self):
|
||||||
# serialize the spending policy, save it
|
# serialize the spending policy, save it
|
||||||
v = dict(settings.master_get(self.nvkey, {}))
|
v = dict(settings.master_get(self.nvkey, {}))
|
||||||
v['pol'] = self.copy()
|
v['pol'] = self.copy()
|
||||||
settings.master_set(self.nvkey, v, master_only=master_only)
|
settings.master_set(self.nvkey, v, master_only=True)
|
||||||
|
|
||||||
def update_policy_key(self, _quiet=False, _master_only=True, **kws):
|
def update_policy_key(self, _quiet=False, **kws):
|
||||||
# Update a few elements of the spending policy
|
# Update a few elements of the spending policy
|
||||||
# - all changes are saved immediately (which is a little slow/visible)
|
# - all changes are saved immediately (which is a little slow/visible)
|
||||||
if not _quiet:
|
if not _quiet:
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
|
|
||||||
self.update(kws)
|
self.update(kws)
|
||||||
self._save_policy(_master_only)
|
self._save_policy()
|
||||||
|
|
||||||
def meets_policy(self, psbt):
|
def meets_policy(self, psbt):
|
||||||
# Does policy allow signing this? Else raise why. Return T if web2fa required.
|
# Does policy allow signing this? Else raise why. Return T if web2fa required.
|
||||||
@ -123,10 +121,7 @@ class SpendingPolicy(dict):
|
|||||||
for idx, txo in psbt.output_iter():
|
for idx, txo in psbt.output_iter():
|
||||||
out = psbt.outputs[idx]
|
out = psbt.outputs[idx]
|
||||||
if not out.is_change: # ignore change
|
if not out.is_change: # ignore change
|
||||||
try:
|
addr = c.render_address(txo.scriptPubKey)
|
||||||
addr = c.render_address(txo.scriptPubKey)
|
|
||||||
except ValueError:
|
|
||||||
addr = str(b2a_hex(txo.scriptPubKey), 'ascii')
|
|
||||||
if addr not in wl:
|
if addr not in wl:
|
||||||
raise SpendPolicyViolation("whitelist: " + addr)
|
raise SpendPolicyViolation("whitelist: " + addr)
|
||||||
|
|
||||||
@ -161,8 +156,7 @@ class SpendingPolicy(dict):
|
|||||||
# always update last block height, even if velocity isn't enabled yet
|
# always update last block height, even if velocity isn't enabled yet
|
||||||
# - attacker might have changed to testnet, but there is no
|
# - attacker might have changed to testnet, but there is no
|
||||||
# reason to ever lower block height. strictly ascending
|
# reason to ever lower block height. strictly ascending
|
||||||
# allow update block_h from temporary seed
|
self.update_policy_key(_quiet=True, block_h=psbt.lock_time)
|
||||||
self.update_policy_key(_quiet=True, _master_only=False, block_h=psbt.lock_time)
|
|
||||||
|
|
||||||
class SSSPFeature:
|
class SSSPFeature:
|
||||||
# Using setting value "sssp"
|
# Using setting value "sssp"
|
||||||
@ -176,6 +170,8 @@ class SSSPFeature:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def update_last_signed(cls, psbt):
|
def update_last_signed(cls, psbt):
|
||||||
# new PSBT has been completely signed successfully.
|
# new PSBT has been completely signed successfully.
|
||||||
|
if not cls.is_enabled():
|
||||||
|
return
|
||||||
pol = cls.get_policy()
|
pol = cls.get_policy()
|
||||||
pol.update_last_signed(psbt)
|
pol.update_last_signed(psbt)
|
||||||
|
|
||||||
@ -236,12 +232,7 @@ class CCCFeature:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def words_check(cls, words):
|
def words_check(cls, words):
|
||||||
# Test if words provided are right
|
# Test if words provided are right
|
||||||
try:
|
enc = seed_words_to_encoded_secret(words)
|
||||||
# a2b_words with checksum check
|
|
||||||
enc = seed_words_to_encoded_secret(words)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
exp = cls.get_encoded_secret()
|
exp = cls.get_encoded_secret()
|
||||||
return enc == exp
|
return enc == exp
|
||||||
|
|
||||||
@ -317,7 +308,7 @@ class CCCFeature:
|
|||||||
if not cls.is_enabled():
|
if not cls.is_enabled():
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
ms = psbt.active_multisig
|
ms = psbt.active_miniscript
|
||||||
if not ms:
|
if not ms:
|
||||||
# not multisig, so ignore/permit
|
# not multisig, so ignore/permit
|
||||||
return False, False
|
return False, False
|
||||||
@ -326,7 +317,7 @@ class CCCFeature:
|
|||||||
# don't try to sign; maybe show warning?
|
# don't try to sign; maybe show warning?
|
||||||
|
|
||||||
xfp = cls.get_xfp()
|
xfp = cls.get_xfp()
|
||||||
if xfp not in ms.xfp_paths:
|
if xfp not in [i[0] for i in ms.to_descriptor().xfp_paths()]:
|
||||||
# does not involve us
|
# does not involve us
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
@ -375,7 +366,7 @@ class CCCConfigMenu(MenuSystem):
|
|||||||
self.replace_items(tmp)
|
self.replace_items(tmp)
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
from multisig import MultisigWallet, make_ms_wallet_menu
|
from wallet import MiniScriptWallet, make_miniscript_wallet_menu
|
||||||
|
|
||||||
my_xfp = CCCFeature.get_xfp()
|
my_xfp = CCCFeature.get_xfp()
|
||||||
items = [
|
items = [
|
||||||
@ -389,10 +380,13 @@ class CCCConfigMenu(MenuSystem):
|
|||||||
|
|
||||||
# look for wallets that are defined related to CCC feature, shortcut to them
|
# look for wallets that are defined related to CCC feature, shortcut to them
|
||||||
count = 0
|
count = 0
|
||||||
for ms in MultisigWallet.get_all():
|
for i, ms in enumerate(MiniScriptWallet.iter_wallets()):
|
||||||
if my_xfp in ms.xfp_paths:
|
if not ms.m_n: # basic multisig check
|
||||||
items.append(MenuItem('↳ %d/%d: %s' % (ms.M, ms.N, ms.name),
|
continue
|
||||||
menu=make_ms_wallet_menu, arg=ms.storage_idx))
|
if my_xfp in [i[0] for i in ms.xfp_paths()]:
|
||||||
|
M, N = ms.m_n
|
||||||
|
items.append(MenuItem('↳ %d/%d: %s' % (M, N, ms.name),
|
||||||
|
menu=make_miniscript_wallet_menu, arg=(i,ms)))
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=count))
|
items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=count))
|
||||||
@ -467,8 +461,8 @@ class CCCConfigMenu(MenuSystem):
|
|||||||
xfp = CCCFeature.get_xfp()
|
xfp = CCCFeature.get_xfp()
|
||||||
enc = CCCFeature.get_encoded_secret()
|
enc = CCCFeature.get_encoded_secret()
|
||||||
|
|
||||||
from multisig import export_multisig_xpubs
|
from wallet import export_miniscript_xpubs
|
||||||
await export_multisig_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
|
await export_miniscript_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
|
||||||
|
|
||||||
async def build_2ofN(self, m, l, i):
|
async def build_2ofN(self, m, l, i):
|
||||||
count = i.arg
|
count = i.arg
|
||||||
@ -594,12 +588,11 @@ class SPAddrWhitelist(MenuSystem):
|
|||||||
if choice == KEY_CANCEL:
|
if choice == KEY_CANCEL:
|
||||||
return
|
return
|
||||||
elif choice == KEY_NFC:
|
elif choice == KEY_NFC:
|
||||||
res = await NFC.read_address()
|
addr = await NFC.read_address()
|
||||||
if not res:
|
if not addr:
|
||||||
# error already displayed in nfc.py
|
# error already displayed in nfc.py
|
||||||
return
|
return
|
||||||
|
|
||||||
_, addr, _ = res
|
|
||||||
await self.add_addresses([addr])
|
await self.add_addresses([addr])
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -661,12 +654,11 @@ class SPAddrWhitelist(MenuSystem):
|
|||||||
|
|
||||||
async def add_addresses(self, more_addrs):
|
async def add_addresses(self, more_addrs):
|
||||||
# add new entries, if unique; preserve ordering
|
# add new entries, if unique; preserve ordering
|
||||||
# - work on a copy and check the limit *before* committing: the list
|
addrs = self.policy.get('addrs', [])
|
||||||
# from get('addrs') is the live, settings-backed one
|
|
||||||
addrs = list(self.policy.get('addrs', []))
|
|
||||||
new = []
|
new = []
|
||||||
for a in more_addrs:
|
for a in more_addrs:
|
||||||
if a not in addrs and a not in new:
|
if a not in addrs:
|
||||||
|
addrs.append(a)
|
||||||
new.append(a)
|
new.append(a)
|
||||||
|
|
||||||
if not new:
|
if not new:
|
||||||
@ -674,10 +666,10 @@ class SPAddrWhitelist(MenuSystem):
|
|||||||
'\n\n'.join(show_single_address(a) for a in more_addrs))
|
'\n\n'.join(show_single_address(a) for a in more_addrs))
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(addrs) + len(new) > MAX_WHITELIST:
|
if len(addrs) > MAX_WHITELIST:
|
||||||
return await self.maxed_out()
|
return await self.maxed_out()
|
||||||
|
|
||||||
self.policy.update_policy_key(addrs=addrs + new)
|
self.policy.update_policy_key(addrs=addrs)
|
||||||
self.update_contents()
|
self.update_contents()
|
||||||
|
|
||||||
if len(new) > 1:
|
if len(new) > 1:
|
||||||
@ -757,11 +749,15 @@ class SpendingPolicyMenu(MenuSystem):
|
|||||||
# Looks decent on both Q and Mk4...
|
# Looks decent on both Q and Mk4...
|
||||||
was = self.policy.get('mag', 0)
|
was = self.policy.get('mag', 0)
|
||||||
val = await ux_enter_number('Transaction Max:', max_value=int(1e8),
|
val = await ux_enter_number('Transaction Max:', max_value=int(1e8),
|
||||||
value=(was or ''))
|
can_cancel=True, value=(was or ''))
|
||||||
if val is None: return
|
|
||||||
|
|
||||||
args = dict(mag=val)
|
args = dict(mag=val)
|
||||||
msg = "Did not change" if val == was else "You have set the"
|
if (val is None) or (val == was):
|
||||||
|
msg = "Did not change"
|
||||||
|
val = was
|
||||||
|
else:
|
||||||
|
msg = "You have set the"
|
||||||
|
unchanged = False
|
||||||
|
|
||||||
if not val:
|
if not val:
|
||||||
msg = "No check for maximum transaction size will be done. "
|
msg = "No check for maximum transaction size will be done. "
|
||||||
@ -1096,7 +1092,7 @@ is locked into a special mode that restricts seed access, backups, settings and
|
|||||||
First step is to define a new PIN code that is used when you want to bypass or \
|
First step is to define a new PIN code that is used when you want to bypass or \
|
||||||
disable this feature.
|
disable this feature.
|
||||||
''',
|
''',
|
||||||
title="Spending Policy" if version.has_qwerty else "Spend Policy")
|
title="Spending Policy")
|
||||||
|
|
||||||
if ch != 'y':
|
if ch != 'y':
|
||||||
# just a tourist
|
# just a tourist
|
||||||
|
|||||||
205
shared/chains.py
205
shared/chains.py
@ -5,16 +5,17 @@
|
|||||||
import ngu
|
import ngu
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from public_constants import AF_BARE_PK, AF_CLASSIC, AF_P2WPKH, AF_P2TR
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_BARE_PK
|
||||||
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_BECH32, AFC_SCRIPT
|
||||||
from block_height import BLOCK_HEIGHT
|
from public_constants import TAPROOT_LEAF_TAPSCRIPT, TAPROOT_LEAF_MASK
|
||||||
from serializations import hash160, ser_compact_size, disassemble
|
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
|
||||||
|
from precomp_tag_hash import TAP_TWEAK_H, TAP_LEAF_H
|
||||||
|
|
||||||
# DO NOT CHANGE ORDER! PickAddrFmtMenu.__init__ expects correct order
|
|
||||||
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
|
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2TR, AF_P2WPKH_P2SH)
|
||||||
|
|
||||||
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||||
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
|
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
|
||||||
@ -32,11 +33,30 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
|
|||||||
NLOCK_IS_TIME = const(500000000)
|
NLOCK_IS_TIME = const(500000000)
|
||||||
|
|
||||||
|
|
||||||
|
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.hash.sha256t(TAP_TWEAK_H, actual_tweak, True)
|
||||||
|
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.hash.sha256t(TAP_LEAF_H, tapscript_serialize(script, leaf_version), True)
|
||||||
|
|
||||||
|
|
||||||
class ChainsBase:
|
class ChainsBase:
|
||||||
|
|
||||||
curve = 'secp256k1'
|
curve = 'secp256k1'
|
||||||
menu_name = None # use 'name' if this isn't defined
|
menu_name = None # use 'name' if this isn't defined
|
||||||
core_name = None # name of chain's "core" p2p software
|
|
||||||
ccc_min_block = 0
|
ccc_min_block = 0
|
||||||
|
|
||||||
# b44_cointype comes from
|
# b44_cointype comes from
|
||||||
@ -71,16 +91,6 @@ class ChainsBase:
|
|||||||
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
|
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
|
||||||
return node.serialize(cls.slip132[addr_fmt].pub, False)
|
return node.serialize(cls.slip132[addr_fmt].pub, False)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def deserialize_node(cls, text, addr_fmt):
|
|
||||||
# xpub/xprv to object
|
|
||||||
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
|
|
||||||
node = ngu.hdnode.HDNode()
|
|
||||||
version = node.deserialize(text)
|
|
||||||
assert (version == cls.slip132[addr_fmt].pub) \
|
|
||||||
or (version == cls.slip132[addr_fmt].priv)
|
|
||||||
return node
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
|
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
|
||||||
digest = None
|
digest = None
|
||||||
@ -104,7 +114,10 @@ class ChainsBase:
|
|||||||
else:
|
else:
|
||||||
assert pubkey
|
assert pubkey
|
||||||
keyhash = ngu.hash.hash160(pubkey)
|
keyhash = ngu.hash.hash160(pubkey)
|
||||||
if addr_fmt == AF_CLASSIC:
|
if addr_fmt == AF_P2TR:
|
||||||
|
assert len(pubkey) == 32 # internal
|
||||||
|
spk = b'\x51\x20' + taptweak(pubkey)
|
||||||
|
elif addr_fmt == AF_CLASSIC:
|
||||||
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
||||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||||
redeem_script = b'\x00\x14' + keyhash
|
redeem_script = b'\x00\x14' + keyhash
|
||||||
@ -116,29 +129,6 @@ class ChainsBase:
|
|||||||
|
|
||||||
return spk, digest
|
return spk, digest
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def p2sh_address(cls, addr_fmt, witdeem_script):
|
|
||||||
# Multisig and general P2SH support
|
|
||||||
# - witdeem => witness script for segwit, or redeem script otherwise
|
|
||||||
# - redeem script can be generated from witness script if needed.
|
|
||||||
# - this function needs a witdeem script to be provided, not simple to make
|
|
||||||
# - more verification needed to prove it's change/included address (NOT HERE)
|
|
||||||
# - reference: <https://bitcoincore.org/en/segwit_wallet_dev/>
|
|
||||||
# - returns: str(address)
|
|
||||||
|
|
||||||
assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
|
|
||||||
_, digest = cls.script_pubkey(addr_fmt, script=witdeem_script)
|
|
||||||
|
|
||||||
if addr_fmt == AF_P2WSH:
|
|
||||||
# bech32 encoded segwit p2sh
|
|
||||||
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
|
|
||||||
else:
|
|
||||||
# segwit p2wsh encoded as classic P2SH
|
|
||||||
# and P2SH classic
|
|
||||||
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
|
|
||||||
|
|
||||||
return addr
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pubkey_to_address(cls, pubkey, addr_fmt):
|
def pubkey_to_address(cls, pubkey, addr_fmt):
|
||||||
# - renders a pubkey to an address
|
# - renders a pubkey to an address
|
||||||
@ -150,6 +140,9 @@ class ChainsBase:
|
|||||||
@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
|
||||||
@ -157,7 +150,7 @@ class ChainsBase:
|
|||||||
return node.addr_help(cls.b58_addr[0])
|
return node.addr_help(cls.b58_addr[0])
|
||||||
|
|
||||||
if addr_fmt & AFC_SCRIPT:
|
if addr_fmt & AFC_SCRIPT:
|
||||||
# use p2sh_address() instead.
|
# use chain.render_address
|
||||||
raise ValueError(hex(addr_fmt))
|
raise ValueError(hex(addr_fmt))
|
||||||
|
|
||||||
# so must be P2PKH, fetch it.
|
# so must be P2PKH, fetch it.
|
||||||
@ -254,7 +247,7 @@ class ChainsBase:
|
|||||||
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
|
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
|
||||||
|
|
||||||
# segwit v0 (P2WPKH, P2WSH)
|
# segwit v0 (P2WPKH, P2WSH)
|
||||||
if ll in (22, 34) and script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
|
if script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
|
||||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
|
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
|
||||||
|
|
||||||
# segwit v1 (P2TR) and later segwit version
|
# segwit v1 (P2TR) and later segwit version
|
||||||
@ -265,40 +258,56 @@ class ChainsBase:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def op_return(cls, script):
|
def op_return(cls, script):
|
||||||
try:
|
# returns decoded string op return data if script is op return otherwise None
|
||||||
gen = disassemble(script)
|
gen = disassemble(script)
|
||||||
item, opcode = next(gen)
|
script_type = next(gen)
|
||||||
except (StopIteration, ValueError):
|
if OP_RETURN not in script_type:
|
||||||
return None
|
return
|
||||||
|
|
||||||
if opcode != OP_RETURN:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
data = next(gen)[0]
|
||||||
data, opcode = next(gen)
|
if data:
|
||||||
except StopIteration:
|
return data
|
||||||
return b"" # bare OP_RETURN
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
return b""
|
||||||
next(gen)
|
|
||||||
return None # extra ops/pushes -> raw script display
|
|
||||||
except StopIteration: pass
|
|
||||||
|
|
||||||
except ValueError:
|
@classmethod
|
||||||
return None
|
def possible_address_fmt(cls, addr):
|
||||||
|
# Given a text (serialized) address, return what
|
||||||
|
# address format applies to the address, but
|
||||||
|
# for AF_P2SH case, could be: AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH. .. we don't know
|
||||||
|
hrp = cls.bech32_hrp + "1"
|
||||||
|
if addr.startswith(hrp):
|
||||||
|
if addr.startswith(hrp+'p'):
|
||||||
|
# segwit v1 (any ver=1 script or address, but for now just taproot...)
|
||||||
|
return AF_P2TR
|
||||||
|
elif addr.startswith(hrp+'q'):
|
||||||
|
# segwit v0
|
||||||
|
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = ngu.codecs.b58_decode(addr)
|
||||||
|
except ValueError:
|
||||||
|
# not base58, not an error
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if raw[0] == cls.b58_addr[0]:
|
||||||
|
return AF_CLASSIC
|
||||||
|
if raw[0] == cls.b58_script[0]:
|
||||||
|
return AF_P2SH
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
return data
|
|
||||||
if data is None and opcode == 0:
|
|
||||||
return b"" # OP_RETURN OP_0
|
|
||||||
return None
|
|
||||||
|
|
||||||
class BitcoinMain(ChainsBase):
|
class BitcoinMain(ChainsBase):
|
||||||
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
|
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
|
||||||
ctype = 'BTC'
|
ctype = 'BTC'
|
||||||
name = 'Bitcoin Mainnet'
|
name = 'Bitcoin Mainnet'
|
||||||
ccc_min_block = BLOCK_HEIGHT
|
ccc_min_block = 939464 # Mar 5/2026
|
||||||
|
|
||||||
slip132 = {
|
slip132 = {
|
||||||
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||||
@ -306,6 +315,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'
|
||||||
@ -327,6 +337,7 @@ class BitcoinTestnet(ChainsBase):
|
|||||||
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'
|
||||||
@ -367,6 +378,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]
|
||||||
|
|
||||||
@ -394,6 +412,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
|
||||||
]
|
]
|
||||||
|
|
||||||
STD_DERIVATIONS = {
|
STD_DERIVATIONS = {
|
||||||
@ -401,21 +421,38 @@ STD_DERIVATIONS = {
|
|||||||
"p2sh-p2wpkh": CommonDerivations[1][1],
|
"p2sh-p2wpkh": CommonDerivations[1][1],
|
||||||
"p2wpkh-p2sh": CommonDerivations[1][1],
|
"p2wpkh-p2sh": CommonDerivations[1][1],
|
||||||
"p2wpkh": CommonDerivations[2][1],
|
"p2wpkh": CommonDerivations[2][1],
|
||||||
|
"p2tr": CommonDerivations[3][1],
|
||||||
}
|
}
|
||||||
|
|
||||||
MS_STD_DERIVATIONS = {
|
MS_STD_DERIVATIONS = {
|
||||||
("p2sh", "m/45h", AF_P2SH),
|
("p2sh", "m/45h", AF_P2SH),
|
||||||
("p2sh_p2wsh", "m/48h/{coin}h/{acct_num}h/1h", AF_P2WSH_P2SH),
|
("p2sh_p2wsh", "m/48h/{coin}h/{acct_num}h/1h", AF_P2WSH_P2SH),
|
||||||
("p2wsh", "m/48h/{coin}h/{acct_num}h/2h", AF_P2WSH),
|
("p2wsh", "m/48h/{coin}h/{acct_num}h/2h", AF_P2WSH),
|
||||||
|
('p2tr', "m/48h/{coin}h/{acct_num}h/3h", AF_P2TR),
|
||||||
|
}
|
||||||
|
|
||||||
|
AF_TO_STR_AF = {
|
||||||
|
AF_BARE_PK: "p2pk",
|
||||||
|
AF_CLASSIC: "p2pkh",
|
||||||
|
AF_P2TR: "p2tr",
|
||||||
|
AF_P2WPKH: "p2wpkh",
|
||||||
|
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
|
||||||
|
AF_P2SH: "p2sh",
|
||||||
|
AF_P2WSH: "p2wsh",
|
||||||
|
AF_P2WSH_P2SH: "p2sh-p2wsh",
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
# integers are coming from USB
|
||||||
try:
|
try:
|
||||||
if isinstance(addr_fmt, int):
|
if isinstance(addr_fmt, int):
|
||||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
||||||
return addr_fmt
|
return addr_fmt
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
|
addr_fmt = AF_TO_STR_AF[addr_fmt] # just for error msg
|
||||||
|
except: pass
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
addr_fmt = addr_fmt.lower()
|
addr_fmt = addr_fmt.lower()
|
||||||
@ -425,11 +462,12 @@ 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
|
raise ValueError
|
||||||
except ValueError:
|
except ValueError:
|
||||||
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 af_to_bip44_purpose(addr_fmt):
|
def af_to_bip44_purpose(addr_fmt):
|
||||||
@ -437,26 +475,19 @@ def af_to_bip44_purpose(addr_fmt):
|
|||||||
# - single signature only
|
# - single signature only
|
||||||
return {AF_CLASSIC: 44,
|
return {AF_CLASSIC: 44,
|
||||||
AF_P2WPKH_P2SH: 49,
|
AF_P2WPKH_P2SH: 49,
|
||||||
AF_P2WPKH: 84}[addr_fmt]
|
AF_P2WPKH: 84,
|
||||||
|
AF_P2TR: 86}[addr_fmt]
|
||||||
|
|
||||||
def addr_fmt_label(addr_fmt):
|
def addr_fmt_label(addr_fmt):
|
||||||
# Text used in menus
|
return {
|
||||||
return {AF_CLASSIC: "Classic P2PKH",
|
AF_CLASSIC: "Classic P2PKH",
|
||||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||||
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
|
AF_P2WPKH: "Segwit P2WPKH",
|
||||||
|
AF_P2TR: "Taproot P2TR",
|
||||||
|
AF_P2WSH: "Segwit P2WSH",
|
||||||
def addr_fmt_str(addr_fmt):
|
AF_P2WSH_P2SH: "P2SH-P2WSH",
|
||||||
# Short string codes used for address format (industry standard)
|
AF_P2SH: "Legacy P2SH",
|
||||||
return {AF_CLASSIC: "p2pkh",
|
}[addr_fmt]
|
||||||
AF_BARE_PK: "p2pk",
|
|
||||||
AF_P2SH: "p2sh",
|
|
||||||
AF_P2TR: "p2tr",
|
|
||||||
AF_P2WPKH: "p2wpkh",
|
|
||||||
AF_P2WSH: "p2wsh",
|
|
||||||
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
|
|
||||||
AF_P2WSH_P2SH: "p2sh-p2wsh"}[addr_fmt]
|
|
||||||
|
|
||||||
def verify_recover_pubkey(sig, digest):
|
def verify_recover_pubkey(sig, digest):
|
||||||
# verifies a message digest against a signature and recovers
|
# verifies a message digest against a signature and recovers
|
||||||
|
|||||||
@ -51,7 +51,9 @@ def decode_utf_16_le(s):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def read_var64(f):
|
def read_var64(f):
|
||||||
# Decode their silly 64-bit encoding.
|
'''
|
||||||
|
Decode their silly 64-bit encoding.
|
||||||
|
'''
|
||||||
first = ord(f.read(1))
|
first = ord(f.read(1))
|
||||||
if first < 128:
|
if first < 128:
|
||||||
return first
|
return first
|
||||||
@ -98,7 +100,7 @@ def check_file_headers(f):
|
|||||||
# assume f is seekable
|
# assume f is seekable
|
||||||
fh = FileHeader.read(f)
|
fh = FileHeader.read(f)
|
||||||
|
|
||||||
if not fh.has_good_magic():
|
if not fh.has_good_magic:
|
||||||
raise ValueError("Bad magic bytes")
|
raise ValueError("Bad magic bytes")
|
||||||
|
|
||||||
# read only first header
|
# read only first header
|
||||||
@ -111,21 +113,22 @@ def check_file_headers(f):
|
|||||||
if sh.size > 10000:
|
if sh.size > 10000:
|
||||||
raise ValueError("Second header too big")
|
raise ValueError("Second header too big")
|
||||||
|
|
||||||
# FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
|
# capture this spot
|
||||||
# SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
|
# TODO 'data_start' unused
|
||||||
# after those two calls, f.tell() is always start_pos + 32
|
data_start = f.tell() # expect 0x20
|
||||||
# assert f.tell() == 0x20 # expect 0x20
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f.seek(sh.offset, 1)
|
f.seek(sh.offset, 1)
|
||||||
th = f.read(sh.size)
|
th = f.read(sh.size)
|
||||||
assert len(th) == sh.size, "Truncated file?"
|
if len(th) != sh.size:
|
||||||
|
raise IndexError("Truncated file?")
|
||||||
|
|
||||||
# Look for properties about compression. this could be
|
# Look for properties about compression. this could be
|
||||||
# faked-out but good enough for now
|
# faked-out but good enough for now
|
||||||
assert b'\x24\x06\xf1\x07\x01' in th, "Not marked as AES+SHA encrypted?"
|
if b'\x24\x06\xf1\x07\x01' not in th:
|
||||||
|
raise RuntimeError("Not marked as AES+SHA encrypted?")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Confused file? %s" % e)
|
raise ValueError("Confused file? %s" % e.message)
|
||||||
|
|
||||||
if masked_crc(th) != sh.crc:
|
if masked_crc(th) != sh.crc:
|
||||||
raise ValueError("Trailing header has wrong CRC")
|
raise ValueError("Trailing header has wrong CRC")
|
||||||
@ -171,6 +174,7 @@ class FileHeader(object):
|
|||||||
|
|
||||||
def actual_crc(self):
|
def actual_crc(self):
|
||||||
return masked_crc(self.bits)
|
return masked_crc(self.bits)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||||
@ -209,7 +213,6 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
|||||||
def actual_crc(self):
|
def actual_crc(self):
|
||||||
return masked_crc(self.bits)
|
return masked_crc(self.bits)
|
||||||
|
|
||||||
|
|
||||||
class Builder(object):
|
class Builder(object):
|
||||||
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
|
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
|
||||||
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine
|
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine
|
||||||
|
|||||||
@ -11,15 +11,6 @@ from bbqr import TYPE_LABELS
|
|||||||
from utils import decode_bip21_text
|
from utils import decode_bip21_text
|
||||||
|
|
||||||
|
|
||||||
def decode_qr_text(got):
|
|
||||||
if isinstance(got, str):
|
|
||||||
return got
|
|
||||||
|
|
||||||
try:
|
|
||||||
return got.decode()
|
|
||||||
except UnicodeError:
|
|
||||||
raise QRDecodeExplained('UTF-8 decode failed')
|
|
||||||
|
|
||||||
def decode_seed_qr(data):
|
def decode_seed_qr(data):
|
||||||
# SeedQR: 4 digit groups of index into word list
|
# SeedQR: 4 digit groups of index into word list
|
||||||
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
||||||
@ -48,8 +39,6 @@ def decode_secret(got):
|
|||||||
# - xprv / tprv
|
# - xprv / tprv
|
||||||
# - words (either full or prefixes, case insensitive)
|
# - words (either full or prefixes, case insensitive)
|
||||||
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
|
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
|
||||||
# - word lists are NOT BIP-39-checksum-validated here. Callers that
|
|
||||||
# require a valid seed must run bip39.a2b_words(...)
|
|
||||||
|
|
||||||
if len(got) > 300:
|
if len(got) > 300:
|
||||||
raise ValueError("Too big.")
|
raise ValueError("Too big.")
|
||||||
@ -62,7 +51,7 @@ def decode_secret(got):
|
|||||||
# xprv or tprv: private key import for sure
|
# xprv or tprv: private key import for sure
|
||||||
# - verify checksum is right
|
# - verify checksum is right
|
||||||
try:
|
try:
|
||||||
ngu.codecs.b58_decode(got)
|
raw = ngu.codecs.b58_decode(got)
|
||||||
except:
|
except:
|
||||||
raise ValueError('corrupt xprv?')
|
raise ValueError('corrupt xprv?')
|
||||||
|
|
||||||
@ -74,7 +63,7 @@ def decode_secret(got):
|
|||||||
kp, testnet, compressed = decode_wif(got)
|
kp, testnet, compressed = decode_wif(got)
|
||||||
return 'wif', (got, kp, compressed, testnet)
|
return 'wif', (got, kp, compressed, testnet)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
taste = got.strip().lower()
|
taste = got.strip().lower()
|
||||||
|
|
||||||
if taste.isdigit():
|
if taste.isdigit():
|
||||||
@ -119,8 +108,11 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
|||||||
return got.decode()
|
return got.decode()
|
||||||
|
|
||||||
if ty == 'P':
|
if ty == 'P':
|
||||||
# `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
|
# may already be in PSRAM, avoid a copy here
|
||||||
# otherwise it's real bytes
|
from glob import PSRAM
|
||||||
|
if PSRAM.is_at(got, 0):
|
||||||
|
got = 'PSRAM' # see qr_psbt_sign()
|
||||||
|
|
||||||
return 'psbt', (None, final_size, got)
|
return 'psbt', (None, final_size, got)
|
||||||
|
|
||||||
elif ty == 'T':
|
elif ty == 'T':
|
||||||
@ -128,10 +120,9 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
|||||||
|
|
||||||
elif ty == 'U':
|
elif ty == 'U':
|
||||||
# continue thru code below for TEXT
|
# continue thru code below for TEXT
|
||||||
got = decode_qr_text(got)
|
pass
|
||||||
|
|
||||||
elif ty == 'J':
|
elif ty == 'J':
|
||||||
got = decode_qr_text(got)
|
|
||||||
what = "json"
|
what = "json"
|
||||||
if "msg" in got:
|
if "msg" in got:
|
||||||
what = "smsg"
|
what = "smsg"
|
||||||
@ -196,7 +187,12 @@ def decode_short_text(got):
|
|||||||
# - if bad checksum on bitcoin addr, we treat as text... since might be
|
# - if bad checksum on bitcoin addr, we treat as text... since might be
|
||||||
# return: what-it-is, (tuple)
|
# return: what-it-is, (tuple)
|
||||||
|
|
||||||
got = decode_qr_text(got)
|
if not isinstance(got, str):
|
||||||
|
# decode utf-8
|
||||||
|
try:
|
||||||
|
got = got.decode()
|
||||||
|
except UnicodeError:
|
||||||
|
raise QRDecodeExplained('UTF-8 decode failed')
|
||||||
|
|
||||||
# might be a PSBT?
|
# might be a PSBT?
|
||||||
if len(got) > 100:
|
if len(got) > 100:
|
||||||
@ -219,26 +215,9 @@ def decode_short_text(got):
|
|||||||
# was something else.
|
# was something else.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# multisig descriptor
|
from descriptor import Descriptor
|
||||||
# multi( catches both multi( and sortedmulti(
|
if Descriptor.is_descriptor(got):
|
||||||
if ("multi(" in got):
|
return 'minisc', (got,)
|
||||||
return 'multi', (got,)
|
|
||||||
|
|
||||||
if ("\n" in got) and ('pub' in got):
|
|
||||||
# legacy multisig import/export format
|
|
||||||
# [0-9a-fA-F]{8}\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]{107}
|
|
||||||
# above is more precise BUT counted repetitions not supported in mpy
|
|
||||||
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
|
|
||||||
rgx = ure.compile(cc_ms_pat)
|
|
||||||
# go line by line and match above, once 2 matches observed - considered multisig
|
|
||||||
# important to not use ure.search for big strings (can run out of stack);
|
|
||||||
# a real line here is a "<8-hex xfp>: <xpub>" key (~121 chars)
|
|
||||||
c = 0 # match count
|
|
||||||
for l in got.split("\n"):
|
|
||||||
if len(l) <= 150 and rgx.search(l):
|
|
||||||
c += 1
|
|
||||||
if c > 1:
|
|
||||||
return 'multi', (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
|
||||||
|
|||||||
620
shared/desc_utils.py
Normal file
620
shared/desc_utils.py
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
# (c) Copyright 2020 by Stepan Snigirev, see <https://github.com/diybitcoinhardware/embit/blob/master/LICENSE>
|
||||||
|
#
|
||||||
|
# Changes (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||||
|
#
|
||||||
|
import ngu, chains, ustruct, stash
|
||||||
|
from io import BytesIO
|
||||||
|
from public_constants import MAX_PATH_DEPTH
|
||||||
|
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'
|
||||||
|
|
||||||
|
# sha256(b"MuSig2MuSig2MuSig2")
|
||||||
|
MUSIG_CHAIN_CODE = b'\x86\x80\x87\xca\x02\xa6\xf9t\xc4Y\x89$\xc3kWv-2\xcbEqqg\xe3\x00b,qg\xe3\x89e'
|
||||||
|
|
||||||
|
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 read_until(s, chars=b",)(#"):
|
||||||
|
res = b""
|
||||||
|
while True:
|
||||||
|
chunk = s.read(1)
|
||||||
|
if len(chunk) == 0:
|
||||||
|
return res, None
|
||||||
|
if chunk in chars:
|
||||||
|
return res, chunk
|
||||||
|
res += chunk
|
||||||
|
|
||||||
|
|
||||||
|
def musig_synthetic_node(agg_pk_bytes):
|
||||||
|
assert len(agg_pk_bytes) == 33 # need non-xonly pubkey
|
||||||
|
node = ngu.hdnode.HDNode()
|
||||||
|
node.from_chaincode_pubkey(MUSIG_CHAIN_CODE, agg_pk_bytes)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class KeyOriginInfo:
|
||||||
|
def __init__(self, fingerprint: bytes, derivation: list, cc_fp=None):
|
||||||
|
self.fingerprint = fingerprint
|
||||||
|
self.derivation = derivation
|
||||||
|
self._cc_fp = cc_fp
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.psbt_derivation() == other.psbt_derivation()
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(tuple(self.psbt_derivation()))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cc_fp(self):
|
||||||
|
if self._cc_fp is None:
|
||||||
|
self._cc_fp = ustruct.unpack('<I', self.fingerprint)[0]
|
||||||
|
return self._cc_fp
|
||||||
|
|
||||||
|
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
|
||||||
|
assert len(derivation) <= MAX_PATH_DEPTH, "origin too deep"
|
||||||
|
return cls(xfp, derivation)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
rv = "%s" % b2a_hex(self.fingerprint).decode()
|
||||||
|
if self.derivation:
|
||||||
|
rv += "/%s" % keypath_to_str(self.derivation, prefix='', skip=0)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.indexes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def not_hardened(x):
|
||||||
|
assert (b"'" not in x) and (b"h" not in x), "Cannot use hardened sub derivation path"
|
||||||
|
|
||||||
|
def get_ext_int(self):
|
||||||
|
return self.indexes[self.multi_path_index]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, s):
|
||||||
|
err = "Malformed key derivation"
|
||||||
|
multi_i = None
|
||||||
|
idxs = []
|
||||||
|
while True:
|
||||||
|
got, char = read_until(s, b"<,)/")
|
||||||
|
if char == b"<":
|
||||||
|
assert multi_i is None, "too many multipaths"
|
||||||
|
ext_num, char = read_until(s, b";")
|
||||||
|
assert char, err
|
||||||
|
cls.not_hardened(ext_num)
|
||||||
|
int_num, char = read_until(s, b">")
|
||||||
|
assert char, err
|
||||||
|
assert b";" not in int_num, "Solved cardinality > 2"
|
||||||
|
cls.not_hardened(int_num)
|
||||||
|
|
||||||
|
assert int_num != ext_num # cannot be the same
|
||||||
|
multi_i = len(idxs)
|
||||||
|
idxs.append((int(ext_num.decode()), int(int_num.decode())))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# char in "/),"
|
||||||
|
if got == b"*":
|
||||||
|
# every derivation has to end with wildcard (only ranged keys allowed)
|
||||||
|
idxs.append(WILDCARD)
|
||||||
|
break
|
||||||
|
elif got:
|
||||||
|
cls.not_hardened(got)
|
||||||
|
idxs.append(int(got.decode()))
|
||||||
|
|
||||||
|
# comma and parenthesis not allowed in subderivation, marker of the end
|
||||||
|
if char in b",)": break
|
||||||
|
|
||||||
|
assert idxs[-1] == WILDCARD, "All keys must be ranged"
|
||||||
|
if idxs == [0, WILDCARD]:
|
||||||
|
# normalize and instead save as <0;1> as change derivation was not provided
|
||||||
|
obj = cls()
|
||||||
|
else:
|
||||||
|
|
||||||
|
assert multi_i is not None, "need multipath"
|
||||||
|
assert len(idxs[multi_i]) == 2, "wrong multipath"
|
||||||
|
|
||||||
|
obj = cls(tuple(idxs))
|
||||||
|
obj.multi_path_index = multi_i
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def to_string(self, external=True, internal=True):
|
||||||
|
res = []
|
||||||
|
for i in self.indexes:
|
||||||
|
if isinstance(i, tuple):
|
||||||
|
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 der_index(self, idx, change=False):
|
||||||
|
if isinstance(idx, list):
|
||||||
|
for i in idx:
|
||||||
|
mp_i = self.multi_path_index or 0
|
||||||
|
if i in self.indexes[mp_i]:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
elif idx is None:
|
||||||
|
# derive according to key subderivation if any
|
||||||
|
if self is None:
|
||||||
|
idx = 1 if change else 0
|
||||||
|
else:
|
||||||
|
if self.multi_path_index is not None:
|
||||||
|
ext, inter = self.indexes[self.multi_path_index]
|
||||||
|
idx = inter if change else ext
|
||||||
|
|
||||||
|
return idx
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedKey:
|
||||||
|
def __init__(self, node, origin, derivation=None, taproot=False, chain_type=None):
|
||||||
|
self.origin = origin
|
||||||
|
self.node = node
|
||||||
|
self.derivation = derivation or KeyDerivationInfo()
|
||||||
|
self.taproot = taproot
|
||||||
|
self.chain_type = chain_type
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.node.pubkey()) + hash(self.derivation)
|
||||||
|
|
||||||
|
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_key(cls, key_str):
|
||||||
|
assert key_str[1:4].lower() == b"pub", "only extended pubkeys allowed"
|
||||||
|
# extended key
|
||||||
|
# or xpub or tpub as we use descriptors (SLIP-132 NOT allowed)
|
||||||
|
hint = key_str[0:1].lower()
|
||||||
|
if hint == b"x":
|
||||||
|
chain_type = "BTC"
|
||||||
|
elif hint == b"t":
|
||||||
|
chain_type = "XTN"
|
||||||
|
else:
|
||||||
|
# slip (ignore any implied address format)
|
||||||
|
chain_type = "BTC" if hint in b"yz" else "XTN"
|
||||||
|
|
||||||
|
node = ngu.hdnode.HDNode()
|
||||||
|
node.deserialize(key_str)
|
||||||
|
try:
|
||||||
|
assert node.privkey() is None, "no privkeys"
|
||||||
|
except ValueError:
|
||||||
|
# ValueError is thrown from libngu if key is public
|
||||||
|
pass
|
||||||
|
|
||||||
|
return node, chain_type
|
||||||
|
|
||||||
|
def validate(self, my_xfp, disable_checks=False):
|
||||||
|
assert self.chain_type == chains.current_key_chain().ctype, "wrong chain"
|
||||||
|
|
||||||
|
# xfp is always available, even if key was serialized without origin info
|
||||||
|
# upon parse root origin info is generated from key itself
|
||||||
|
xfp = self.origin.cc_fp
|
||||||
|
is_mine = (xfp == my_xfp)
|
||||||
|
|
||||||
|
# raises ValueError on invalid pubkey (should be in libngu)
|
||||||
|
# invalid public key not allowed even with disable checks
|
||||||
|
ngu.secp256k1.pubkey(self.node.pubkey())
|
||||||
|
|
||||||
|
if not disable_checks:
|
||||||
|
depth = self.node.depth()
|
||||||
|
# we now allow blinded keys that have depth X but derivation len is 0,
|
||||||
|
# where only fingerprint constitutes key origin
|
||||||
|
# only check if derivation length is greater than 0
|
||||||
|
if self.origin.derivation:
|
||||||
|
assert len(self.origin.derivation) == depth, \
|
||||||
|
"deriv len != xpub depth (xfp=%s)" % xfp2str(xfp)
|
||||||
|
if depth == 0:
|
||||||
|
# blinded keys allowed
|
||||||
|
# assert not self.node.parent_fp()
|
||||||
|
# assert self.node.child_number()[0] == 0
|
||||||
|
assert swab32(self.node.my_fp()) == xfp, "master xfp mismatch"
|
||||||
|
elif depth == 1:
|
||||||
|
target = swab32(self.node.parent_fp())
|
||||||
|
assert xfp == target, 'xfp depth=1 wrong'
|
||||||
|
|
||||||
|
if is_mine:
|
||||||
|
# it's supposed to be my key, so I should be able to generate pubkey
|
||||||
|
# - might indicate collision on xfp value between co-signers,
|
||||||
|
# and that's not supported
|
||||||
|
deriv = self.origin.str_derivation()
|
||||||
|
with stash.SensitiveValues() as sv:
|
||||||
|
chk_node = sv.derive_path(deriv)
|
||||||
|
assert self.node.pubkey() == chk_node.pubkey(), \
|
||||||
|
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
|
||||||
|
|
||||||
|
return is_mine
|
||||||
|
|
||||||
|
def derive(self, idx=None, change=False):
|
||||||
|
if self.derivation:
|
||||||
|
idx = self.derivation.der_index(idx, change)
|
||||||
|
else:
|
||||||
|
assert idx
|
||||||
|
|
||||||
|
new_node = self.node.copy()
|
||||||
|
new_node.derive(idx, False)
|
||||||
|
if self.origin:
|
||||||
|
origin = KeyOriginInfo(self.origin.fingerprint, self.origin.derivation + [idx],
|
||||||
|
self.origin.cc_fp)
|
||||||
|
else:
|
||||||
|
origin = KeyOriginInfo(self.origin.fingerprint, [idx], self.origin.cc_fp)
|
||||||
|
|
||||||
|
new_der = None
|
||||||
|
if self.derivation:
|
||||||
|
new_der = KeyDerivationInfo(self.derivation.indexes[1:])
|
||||||
|
|
||||||
|
return type(self)(new_node, origin, new_der, taproot=self.taproot)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_from(cls, s, taproot=False, musig=False):
|
||||||
|
first = s.read(1)
|
||||||
|
origin = None
|
||||||
|
|
||||||
|
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",)/")
|
||||||
|
if musig and char not in b",)":
|
||||||
|
assert b"musig(" not in k, "nested musig not allowed"
|
||||||
|
assert char != b"/", "key derivation not allowed inside musig"
|
||||||
|
|
||||||
|
der = None
|
||||||
|
if char == b"/":
|
||||||
|
der = KeyDerivationInfo.parse(s)
|
||||||
|
if char is not None:
|
||||||
|
s.seek(-1, 1)
|
||||||
|
|
||||||
|
# parse key
|
||||||
|
node, chain_type = cls.parse_key(k)
|
||||||
|
if origin is None:
|
||||||
|
cc_fp = swab32(node.my_fp())
|
||||||
|
origin = KeyOriginInfo(ustruct.pack('<I', cc_fp), [], cc_fp)
|
||||||
|
return cls(node, origin, der, chain_type=chain_type, taproot=taproot)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_cc_data(cls, xfp, deriv, xpub):
|
||||||
|
xfp_str = xfp if isinstance(xfp, str) else xfp2str(xfp)
|
||||||
|
koi = KeyOriginInfo.from_string("%s/%s" % (xfp_str, deriv.replace("m/", "")))
|
||||||
|
node, chain_type = cls.parse_key(xpub.encode())
|
||||||
|
|
||||||
|
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_cc_json(cls, vals, af_str):
|
||||||
|
key_exp = af_str + "_key_exp"
|
||||||
|
if key_exp in vals:
|
||||||
|
# new firmware, prefer key expression
|
||||||
|
return cls.from_string(vals[key_exp])
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
node, _, _, _ = chains.slip132_deserialize(vals[af_str])
|
||||||
|
ek = chains.current_chain().serialize_public(node)
|
||||||
|
return cls.from_cc_data(vals["xfp"], vals["%s_deriv" % af_str], ek)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_psbt_xpub(cls, ek_bytes, xfp_path):
|
||||||
|
xfp, *path = xfp_path
|
||||||
|
koi = KeyOriginInfo(a2b_hex(xfp2str(xfp)), path)
|
||||||
|
# TODO this should be done by C code, no need to base58 encode/decode
|
||||||
|
# byte-serialized key should be decodable
|
||||||
|
ek = ngu.codecs.b58_encode(ek_bytes)
|
||||||
|
node, chain_type = cls.parse_key(ek.encode())
|
||||||
|
|
||||||
|
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_provably_unspendable(self):
|
||||||
|
if PROVABLY_UNSPENDABLE == self.node.pubkey():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefix(self):
|
||||||
|
if self.origin and self.origin.derivation:
|
||||||
|
return "[%s]" % self.origin
|
||||||
|
# jut a bare [xfp]key - omit origin info (jut xfp)
|
||||||
|
# or no origin at all
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def key_bytes(self):
|
||||||
|
kb = self.node.pubkey()
|
||||||
|
if self.taproot:
|
||||||
|
# xonly
|
||||||
|
kb = kb[1:]
|
||||||
|
return kb
|
||||||
|
|
||||||
|
def extended_public_key(self):
|
||||||
|
return chains.current_chain().serialize_public(self.node)
|
||||||
|
|
||||||
|
def to_string(self, external=True, internal=True):
|
||||||
|
key = self.prefix
|
||||||
|
key += self.extended_public_key()
|
||||||
|
if self.derivation and (external or internal):
|
||||||
|
key += "/" + self.derivation.to_string(external, internal)
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, s):
|
||||||
|
s = BytesIO(s.encode())
|
||||||
|
return cls.read_from(s)
|
||||||
|
|
||||||
|
|
||||||
|
class MusigKey:
|
||||||
|
def __init__(self, keys, der=None, node=None):
|
||||||
|
self.keys = keys
|
||||||
|
self.derivation = der or KeyDerivationInfo()
|
||||||
|
self._node = node
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return 33 # length + <32:xonly>
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.node.pubkey()) + hash(self.derivation)
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return self.key_bytes()
|
||||||
|
|
||||||
|
def compile(self):
|
||||||
|
d = self.serialize()
|
||||||
|
return ser_compact_size(len(d)) + d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node(self):
|
||||||
|
if self._node is None:
|
||||||
|
self._node = musig_synthetic_node(self.aggregate_pubkey().to_bytes())
|
||||||
|
return self._node
|
||||||
|
|
||||||
|
def validate(self, my_xfp, disable_checks=False):
|
||||||
|
has_mine = 0
|
||||||
|
for k in self.keys:
|
||||||
|
assert not k.is_provably_unspendable, "unspendable key inside musig"
|
||||||
|
if k.validate(my_xfp, disable_checks):
|
||||||
|
has_mine += 1
|
||||||
|
|
||||||
|
assert len(self.keys) == len(set(self.keys)), "musig keys not unique"
|
||||||
|
assert has_mine <= 1, "multiple own keys in musig"
|
||||||
|
return has_mine
|
||||||
|
|
||||||
|
def key_bytes(self):
|
||||||
|
return ngu.secp256k1.pubkey(self.node.pubkey()).to_xonly().to_bytes()
|
||||||
|
|
||||||
|
def aggregate_pubkey(self):
|
||||||
|
keyagg_cache = ngu.secp256k1.MusigKeyAggCache()
|
||||||
|
secp_pubkeys = [ngu.secp256k1.pubkey(k.node.pubkey()) for k in self.keys]
|
||||||
|
ngu.secp256k1.musig_pubkey_agg(secp_pubkeys, keyagg_cache)
|
||||||
|
return keyagg_cache.agg_pubkey()
|
||||||
|
|
||||||
|
def to_string(self, external=True, internal=True):
|
||||||
|
base = "musig(%s)" % (",".join([k.to_string(False, False) for k in self.keys]))
|
||||||
|
base += "/" + self.derivation.to_string(external, internal)
|
||||||
|
return base
|
||||||
|
|
||||||
|
def derive(self, idx=None, change=False):
|
||||||
|
idx = self.derivation.der_index(idx, change)
|
||||||
|
new_node = self.node.copy()
|
||||||
|
new_node.derive(idx, False)
|
||||||
|
|
||||||
|
return type(self)(self.keys, KeyDerivationInfo(self.derivation.indexes[1:]),
|
||||||
|
node=new_node)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_provably_unspendable(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_from(cls, s, taproot=True):
|
||||||
|
assert taproot, "musig in non-taproot context"
|
||||||
|
assert s.read(6) == b"musig(", "not musig()"
|
||||||
|
|
||||||
|
der = None
|
||||||
|
keys = []
|
||||||
|
while True:
|
||||||
|
k = ExtendedKey.read_from(s, taproot=taproot, musig=True)
|
||||||
|
k.der = None
|
||||||
|
k.taproot = taproot
|
||||||
|
# already verified that no der present in keys
|
||||||
|
k.derivation = None
|
||||||
|
keys.append(k)
|
||||||
|
c = s.read(1)
|
||||||
|
if c == b")":
|
||||||
|
sep = s.read(1)
|
||||||
|
if sep == b"/":
|
||||||
|
der = KeyDerivationInfo.parse(s)
|
||||||
|
|
||||||
|
s.seek(-1, 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
assert c == b","
|
||||||
|
|
||||||
|
return cls(keys, der)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, s):
|
||||||
|
s = BytesIO(s.encode())
|
||||||
|
return cls.read_from(s)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyExpression:
|
||||||
|
@classmethod
|
||||||
|
def read_from(cls, s, taproot=False):
|
||||||
|
is_musig = (s.read(6) == b"musig(")
|
||||||
|
s.seek(-6, 1)
|
||||||
|
if is_musig:
|
||||||
|
return MusigKey.read_from(s, taproot=taproot)
|
||||||
|
else:
|
||||||
|
return ExtendedKey.read_from(s, taproot=taproot)
|
||||||
|
|
||||||
|
|
||||||
|
def bip388_wallet_policy_to_descriptor(desc_tmplt, keys_info):
|
||||||
|
for i in range(len(keys_info) - 1, -1, -1):
|
||||||
|
k_str = keys_info[i]
|
||||||
|
ph = "@%d" % i
|
||||||
|
desc_tmplt = desc_tmplt.replace(ph, k_str)
|
||||||
|
return desc_tmplt.replace("/**", "/<0;1>/*")
|
||||||
|
|
||||||
|
|
||||||
|
def bip388_validate_policy(desc_tmplt, keys_info):
|
||||||
|
s = BytesIO(desc_tmplt)
|
||||||
|
r = []
|
||||||
|
while True:
|
||||||
|
g1, char = read_until(s, b"@")
|
||||||
|
if not char:
|
||||||
|
# no more - done
|
||||||
|
break
|
||||||
|
|
||||||
|
# key derivation info required for policy
|
||||||
|
g2, char = read_until(s, b"/")
|
||||||
|
assert char, "key derivation missing"
|
||||||
|
if g1.endswith(b"musig("):
|
||||||
|
# key derivations not allowed inside musig
|
||||||
|
assert b"/" not in g2
|
||||||
|
assert g2[-1:] == b")"
|
||||||
|
|
||||||
|
for i, num in enumerate(g2[:-1].split(b",")):
|
||||||
|
if i:
|
||||||
|
# 0th element has @ already removed
|
||||||
|
assert num[0:1] == b"@"
|
||||||
|
num = num[1:]
|
||||||
|
|
||||||
|
num = int(num.decode())
|
||||||
|
if num not in r:
|
||||||
|
r.append(num)
|
||||||
|
|
||||||
|
else:
|
||||||
|
num = int(g2.decode())
|
||||||
|
if num not in r:
|
||||||
|
r.append(num)
|
||||||
|
|
||||||
|
assert s.read(1) in b"<*", "need multipath"
|
||||||
|
|
||||||
|
|
||||||
|
assert len(r) == len(keys_info), "Invalid policy"
|
||||||
|
assert r == list(range(len(r))), "Out of order"
|
||||||
@ -1,251 +1,333 @@
|
|||||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
# (c) Copyright 2020 by Stepan Snigirev, see <https://github.com/diybitcoinhardware/embit/blob/master/LICENSE>
|
||||||
#
|
#
|
||||||
# descriptor.py - Bitcoin Core's descriptors and their specialized checksums.
|
# Changes (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||||
#
|
#
|
||||||
# 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 utils import xfp2str, swab32
|
||||||
MULTI_FMT_TO_SCRIPT = {
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
|
||||||
AF_P2SH: "sh(%s)",
|
from public_constants import AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, MAX_TR_SIGNERS
|
||||||
AF_P2WSH_P2SH: "sh(wsh(%s))",
|
from desc_utils import (parse_desc_str, append_checksum, descriptor_checksum,
|
||||||
AF_P2WSH: "wsh(%s)",
|
KeyExpression, ExtendedKey, MusigKey)
|
||||||
None: "wsh(%s)",
|
from miniscript import Miniscript
|
||||||
# hack for tests
|
from precomp_tag_hash import TAP_BRANCH_H
|
||||||
"p2sh": "sh(%s)",
|
|
||||||
"p2sh-p2wsh": "sh(wsh(%s))",
|
|
||||||
"p2wsh-p2sh": "sh(wsh(%s))",
|
|
||||||
"p2wsh": "wsh(%s)",
|
|
||||||
}
|
|
||||||
|
|
||||||
SINGLE_FMT_TO_SCRIPT = {
|
|
||||||
AF_P2WPKH: "wpkh(%s)",
|
|
||||||
AF_CLASSIC: "pkh(%s)",
|
|
||||||
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
|
|
||||||
None: "wpkh(%s)",
|
|
||||||
"p2pkh": "pkh(%s)",
|
|
||||||
"p2wpkh": "wpkh(%s)",
|
|
||||||
"p2sh-p2wpkh": "sh(wpkh(%s))",
|
|
||||||
"p2wpkh-p2sh": "sh(wpkh(%s))",
|
|
||||||
}
|
|
||||||
|
|
||||||
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
|
|
||||||
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
||||||
|
|
||||||
try:
|
|
||||||
from utils import xfp2str, str2xfp
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
import struct
|
|
||||||
from binascii import unhexlify as a2b_hex
|
|
||||||
from binascii import hexlify as b2a_hex
|
|
||||||
# assuming not micro python
|
|
||||||
def xfp2str(xfp):
|
|
||||||
# Standardized way to show an xpub's fingerprint... it's a 4-byte string
|
|
||||||
# and not really an integer. Used to show as '0x%08x' but that's wrong endian.
|
|
||||||
return b2a_hex(struct.pack('<I', xfp)).decode().upper()
|
|
||||||
|
|
||||||
def str2xfp(txt):
|
|
||||||
# Inverse of xfp2str
|
|
||||||
return struct.unpack('<I', a2b_hex(txt))[0]
|
|
||||||
|
|
||||||
|
|
||||||
class WrongCheckSumError(Exception):
|
class Tapscript:
|
||||||
pass
|
def __init__(self, tree):
|
||||||
|
self.tree = tree # miniscript or (tapscript, tapscript)
|
||||||
|
self._merkle_root = None
|
||||||
|
self._processed_tree = None
|
||||||
|
|
||||||
|
def iter_leaves(self):
|
||||||
|
if isinstance(self.tree, Miniscript):
|
||||||
|
yield self.tree
|
||||||
|
else:
|
||||||
|
for ts in self.tree:
|
||||||
|
yield from ts.iter_leaves()
|
||||||
|
|
||||||
def polymod(c, val):
|
@property
|
||||||
c0 = c >> 35
|
def merkle_root(self):
|
||||||
c = ((c & 0x7ffffffff) << 5) ^ val
|
if not self._merkle_root:
|
||||||
if (c0 & 1):
|
self._processed_tree, self._merkle_root = self.process_tree()
|
||||||
c ^= 0xf5dee51989
|
return self._merkle_root
|
||||||
if (c0 & 2):
|
|
||||||
c ^= 0xa9fdca3312
|
|
||||||
if (c0 & 4):
|
|
||||||
c ^= 0x1bab10e32d
|
|
||||||
if (c0 & 8):
|
|
||||||
c ^= 0x3706b1677a
|
|
||||||
if (c0 & 16):
|
|
||||||
c ^= 0x644d626ffd
|
|
||||||
|
|
||||||
return c
|
def derive(self, idx, key_map, change=False):
|
||||||
|
if isinstance(self.tree, Miniscript):
|
||||||
|
tree = self.tree.derive(idx, key_map, change=change)
|
||||||
|
else:
|
||||||
|
l, r = self.tree
|
||||||
|
tree = [l.derive(idx, key_map, change=change),
|
||||||
|
r.derive(idx, key_map, change=change)]
|
||||||
|
|
||||||
def descriptor_checksum(desc):
|
return type(self)(tree)
|
||||||
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)
|
def process_tree(self):
|
||||||
cls = cls * 3 + (pos >> 5)
|
if isinstance(self.tree, Miniscript):
|
||||||
clscount += 1
|
script = self.tree.compile()
|
||||||
if clscount == 3:
|
h = chains.tapleaf_hash(script)
|
||||||
c = polymod(c, cls)
|
return [(chains.TAPROOT_LEAF_TAPSCRIPT, script, bytes())], h
|
||||||
cls = 0
|
|
||||||
clscount = 0
|
|
||||||
|
|
||||||
if clscount > 0:
|
l, r = self.tree
|
||||||
c = polymod(c, cls)
|
left, left_h = l.process_tree()
|
||||||
for j in range(0, 8):
|
right, right_h = r.process_tree()
|
||||||
c = polymod(c, 0)
|
left = [(version, script, control + right_h) for version, script, control in left]
|
||||||
c ^= 1
|
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
|
||||||
|
|
||||||
rv = ''
|
h = ngu.hash.sha256t(TAP_BRANCH_H, left_h + right_h, True)
|
||||||
for j in range(0, 8):
|
return left + right, h
|
||||||
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
|
|
||||||
|
|
||||||
return rv
|
# UNUSED - using above proces tree cached result to dump scripts to CSV
|
||||||
|
# def script_tree(self):
|
||||||
|
# if isinstance(self.tree, Miniscript):
|
||||||
|
# return b2a_hex(chains.tapscript_serialize(self.tree.compile())).decode()
|
||||||
|
#
|
||||||
|
# l, r = self.tree
|
||||||
|
# return "{" + l.script_tree() + "," +r.script_tree() + "}"
|
||||||
|
|
||||||
def append_checksum(desc):
|
@classmethod
|
||||||
return desc + "#" + descriptor_checksum(desc)
|
def read_from(cls, s):
|
||||||
|
c = s.read(1)
|
||||||
|
assert len(c)
|
||||||
|
if c == b"{": # more than one miniscript
|
||||||
|
left = cls.read_from(s)
|
||||||
|
c = s.read(1)
|
||||||
|
if c == b"}":
|
||||||
|
return left
|
||||||
|
if c != b",":
|
||||||
|
raise ValueError("Invalid tapscript: expected ','")
|
||||||
|
|
||||||
|
right = cls.read_from(s)
|
||||||
|
if s.read(1) != b"}":
|
||||||
|
raise ValueError("Invalid tapscript: expected '}'")
|
||||||
|
|
||||||
def parse_desc_str(string):
|
return cls((left, right))
|
||||||
"""Remove comments, empty lines and strip line. Produce single line string"""
|
|
||||||
res = ""
|
|
||||||
for l in string.split("\n"):
|
|
||||||
strip_l = l.strip()
|
|
||||||
if not strip_l:
|
|
||||||
continue
|
|
||||||
if strip_l.startswith("#"):
|
|
||||||
continue
|
|
||||||
res += strip_l
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
s.seek(-1, 1)
|
||||||
|
ms = Miniscript.read_from(s, taproot=True)
|
||||||
|
return cls(ms)
|
||||||
|
|
||||||
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
|
def to_string(self, external=True, internal=True):
|
||||||
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
|
if isinstance(self.tree, Miniscript):
|
||||||
if addr_fmt == AF_P2WSH_P2SH:
|
return self.tree.to_string(external, internal)
|
||||||
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
|
|
||||||
elif addr_fmt == AF_P2WSH:
|
l, r = self.tree
|
||||||
descriptor_template = "wsh(sortedmulti(M,%s,...))"
|
return ("{" + l.to_string(external,internal) + ","
|
||||||
elif addr_fmt == AF_P2SH:
|
+ r.to_string(external, internal) + "}")
|
||||||
descriptor_template = "sh(sortedmulti(M,%s,...))"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
descriptor_template = descriptor_template % key_exp
|
|
||||||
return descriptor_template
|
|
||||||
|
|
||||||
|
|
||||||
class Descriptor:
|
class Descriptor:
|
||||||
__slots__ = (
|
def __init__(self, key=None, miniscript=None, tapscript=None, addr_fmt=None, keys=None):
|
||||||
"keys",
|
if addr_fmt in [AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH]:
|
||||||
"addr_fmt",
|
assert miniscript
|
||||||
)
|
assert not key
|
||||||
|
else:
|
||||||
|
# single-sig + taproot/tapscript
|
||||||
|
assert miniscript is None
|
||||||
|
assert key
|
||||||
|
|
||||||
def __init__(self, keys, addr_fmt):
|
self.key = key
|
||||||
self.keys = keys
|
self.miniscript = miniscript
|
||||||
|
self.tapscript = tapscript
|
||||||
self.addr_fmt = addr_fmt
|
self.addr_fmt = addr_fmt
|
||||||
|
# cached keys
|
||||||
|
self._keys = keys
|
||||||
|
|
||||||
@staticmethod
|
def validate(self, disable_checks=False):
|
||||||
def checksum_check(desc_w_checksum , csum_required=False):
|
# should only be run once while importing wallet
|
||||||
try:
|
from glob import settings
|
||||||
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
|
c = 0
|
||||||
def parse_key_orig_info(key):
|
has_mine = 0
|
||||||
# key origin info is required for our MultisigWallet
|
err_top_B = "Top level miniscript should be 'B'"
|
||||||
close_index = key.find("]")
|
max_signers = 20
|
||||||
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:]
|
|
||||||
return key_orig_info, key
|
|
||||||
|
|
||||||
@staticmethod
|
if self.tapscript:
|
||||||
def parse_key_derivation_info(key):
|
assert self.key # internal key (would fail during parse)
|
||||||
invalid_subderiv_msg = "Invalid subderivation path - only 0/* or <0;1>/* allowed"
|
max_signers = MAX_TR_SIGNERS
|
||||||
slash_split = key.split("/")
|
for l in self.tapscript.iter_leaves():
|
||||||
assert len(slash_split) > 1, invalid_subderiv_msg
|
assert l.type == "B", err_top_B
|
||||||
if all(["h" not in elem and "'" not in elem for elem in slash_split[1:]]):
|
l.verify()
|
||||||
assert slash_split[-1] == "*", invalid_subderiv_msg
|
l.is_sane(taproot=True)
|
||||||
assert slash_split[-2] in ["0", "<0;1>", "<1;0>"], invalid_subderiv_msg
|
# cannot have same keys in single miniscript
|
||||||
assert len(slash_split[1:]) == 2, invalid_subderiv_msg
|
# provably unspendable taproot internal key is not covered here
|
||||||
return slash_split[0]
|
assert len(l.keys) == len(set(l.keys)), "Insane"
|
||||||
else:
|
|
||||||
raise ValueError("Cannot use hardened sub derivation path")
|
|
||||||
|
|
||||||
def checksum(self):
|
elif self.miniscript:
|
||||||
return descriptor_checksum(self._serialize())
|
assert self.key is None
|
||||||
|
assert self.miniscript.type == "B", err_top_B
|
||||||
|
self.miniscript.verify()
|
||||||
|
self.miniscript.is_sane(taproot=False)
|
||||||
|
# cannot have same keys in single miniscript
|
||||||
|
assert len(self.miniscript.keys) == len(set(self.miniscript.keys)), "Insane"
|
||||||
|
|
||||||
def serialize_keys(self, internal=False, int_ext=False):
|
my_xfp = settings.get('xfp', 0)
|
||||||
result = []
|
ext_nums = set()
|
||||||
for xfp, deriv, xpub in self.keys:
|
int_nums = set()
|
||||||
if deriv[0] == "m":
|
for k in self.keys:
|
||||||
# get rid of 'm'
|
has_mine += k.validate(my_xfp, disable_checks)
|
||||||
deriv = deriv[1:]
|
ext, int = k.derivation.get_ext_int()
|
||||||
elif deriv[0] != "/":
|
ext_nums.add(ext)
|
||||||
# input "84'/0'/0'" would lack slash separtor with xfp
|
int_nums.add(int)
|
||||||
deriv = "/" + deriv
|
c += 1
|
||||||
if not isinstance(xfp, str):
|
|
||||||
xfp = xfp2str(xfp)
|
if not self.tapscript and not self.is_basic_multisig:
|
||||||
koi = xfp + deriv
|
# this is non-taproot Miniscript
|
||||||
# normalize xpub to use h for hardened instead of '
|
# Miniscript expressions can only be used in wsh or tr.
|
||||||
key_str = "[%s]%s" % (koi.lower(), xpub)
|
assert self.addr_fmt != AF_P2SH, "Miniscript in legacy P2SH not allowed"
|
||||||
if int_ext:
|
|
||||||
key_str = key_str + "/" + "<0;1>" + "/" + "*"
|
assert ext_nums.isdisjoint(int_nums), "Non-disjoint multipath"
|
||||||
|
assert c <= max_signers, "max signers"
|
||||||
|
|
||||||
|
assert has_mine > 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
|
||||||
|
|
||||||
|
def bip388_wallet_policy(self):
|
||||||
|
# Return compact descriptor (BIP-388 style) template and key info
|
||||||
|
# - only same origin keys
|
||||||
|
keys_info = OrderedDict()
|
||||||
|
|
||||||
|
for k in self.keys:
|
||||||
|
ks = k.keys if isinstance(k, MusigKey) else [k]
|
||||||
|
|
||||||
|
for kk in ks:
|
||||||
|
pk = kk.node.pubkey()
|
||||||
|
if pk not in keys_info:
|
||||||
|
keys_info[pk] = kk.to_string(external=False, internal=False)
|
||||||
|
|
||||||
|
desc_tmplt = self.to_string(checksum=False).replace("/<0;1>/*", "/**")
|
||||||
|
|
||||||
|
keys_info = list(keys_info.values())
|
||||||
|
for i, k_str in enumerate(keys_info):
|
||||||
|
desc_tmplt = desc_tmplt.replace(k_str, '@%d' % i)
|
||||||
|
|
||||||
|
return desc_tmplt, keys_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def script_len(self):
|
||||||
|
if self.is_taproot:
|
||||||
|
return 34 # OP_1 <32:xonly>
|
||||||
|
if self.miniscript:
|
||||||
|
return len(self.miniscript)
|
||||||
|
if self.addr_fmt == AF_P2WPKH:
|
||||||
|
return 22 # 00 <20:pkh>
|
||||||
|
return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG
|
||||||
|
|
||||||
|
def xfp_paths(self, skip_unspend_ik=False):
|
||||||
|
res = []
|
||||||
|
for k in self.keys:
|
||||||
|
if self.is_taproot and k.is_provably_unspendable and skip_unspend_ik:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(k, MusigKey):
|
||||||
|
agg_k = [swab32(k.node.my_fp())]
|
||||||
|
# even if dupes - add
|
||||||
|
res.append(agg_k)
|
||||||
|
|
||||||
|
for kk in k.keys:
|
||||||
|
psbt_der = kk.origin.psbt_derivation()
|
||||||
|
if psbt_der not in res:
|
||||||
|
res.append(psbt_der)
|
||||||
else:
|
else:
|
||||||
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
|
res.append(k.origin.psbt_derivation())
|
||||||
result.append(key_str.replace("'", "h"))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _serialize(self, internal=False, int_ext=False):
|
return res
|
||||||
"""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):
|
@property
|
||||||
"""Serialize with checksum"""
|
def is_segwit_v0(self):
|
||||||
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
|
return self.addr_fmt in [AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH]
|
||||||
|
|
||||||
@classmethod
|
@property
|
||||||
def parse(cls, desc_w_checksum):
|
def is_segwit(self):
|
||||||
# remove garbage
|
return self.is_taproot or self.is_segwit_v0
|
||||||
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
|
@property
|
||||||
elif desc.startswith("wpkh("):
|
def is_taproot(self):
|
||||||
addr_fmt = AF_P2WPKH
|
return self.addr_fmt == AF_P2TR
|
||||||
tmp_desc = desc.replace("wpkh(", "")
|
|
||||||
tmp_desc = tmp_desc.rstrip(")")
|
|
||||||
|
|
||||||
# wrapped segwit
|
@property
|
||||||
elif desc.startswith("sh(wpkh("):
|
def is_legacy_sh(self):
|
||||||
addr_fmt = AF_P2WPKH_P2SH
|
return self.addr_fmt in [AF_P2SH, AF_P2WSH_P2SH, AF_P2WPKH_P2SH]
|
||||||
tmp_desc = desc.replace("sh(wpkh(", "")
|
|
||||||
tmp_desc = tmp_desc.rstrip("))")
|
@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._keys:
|
||||||
|
return self._keys
|
||||||
|
|
||||||
|
if self.is_taproot:
|
||||||
|
# internal is always first
|
||||||
|
# use ordered dict as order preserving set
|
||||||
|
keys = OrderedDict()
|
||||||
|
# add internal key (whether musig or not)
|
||||||
|
keys[self.key] = None
|
||||||
|
|
||||||
|
if self.tapscript:
|
||||||
|
# taptree keys
|
||||||
|
for lv in self.tapscript.iter_leaves():
|
||||||
|
for k in lv.keys:
|
||||||
|
keys[k] = None
|
||||||
|
|
||||||
|
self._keys = list(keys)
|
||||||
|
|
||||||
|
elif self.miniscript:
|
||||||
|
self._keys = self.miniscript.keys
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unsupported descriptor. Supported: pkh(), wpkh(), sh(wpkh()).")
|
# single-sig
|
||||||
|
self._keys = [self.key]
|
||||||
|
|
||||||
koi, key = cls.parse_key_orig_info(tmp_desc)
|
return self._keys
|
||||||
if key[0:4] not in ["tpub", "xpub"]:
|
|
||||||
raise ValueError("Only extended public keys are supported")
|
|
||||||
|
|
||||||
xpub = cls.parse_key_derivation_info(key)
|
def derive(self, idx=None, change=False):
|
||||||
xfp = str2xfp(koi[:8])
|
if self.is_taproot:
|
||||||
origin_deriv = "m" + koi[8:]
|
# derive keys first
|
||||||
|
# duplicate keys can be may be found in different leaves
|
||||||
|
# use map to derive each key just once
|
||||||
|
derived_keys = OrderedDict()
|
||||||
|
for i, k in enumerate(self.keys):
|
||||||
|
if not isinstance(k, MusigKey):
|
||||||
|
dk = k.derive(idx, change=change)
|
||||||
|
dk.taproot = self.is_taproot
|
||||||
|
derived_keys[k] = dk
|
||||||
|
|
||||||
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
|
derived_tapsript = None
|
||||||
|
if self.tapscript:
|
||||||
|
derived_tapsript = self.tapscript.derive(idx, derived_keys, change=change)
|
||||||
|
|
||||||
|
return type(self)(self.key.derive(idx, change=change),
|
||||||
|
tapscript=derived_tapsript, addr_fmt=self.addr_fmt,
|
||||||
|
keys=list(derived_keys.values()))
|
||||||
|
|
||||||
|
if self.miniscript:
|
||||||
|
return type(self)(
|
||||||
|
None,
|
||||||
|
self.miniscript.derive(idx, change=change),
|
||||||
|
addr_fmt=self.addr_fmt,
|
||||||
|
)
|
||||||
|
|
||||||
|
# single-sig
|
||||||
|
return type(self)(self.key.derive(idx, change=change))
|
||||||
|
|
||||||
|
def script_pubkey(self, compiled_scr=None):
|
||||||
|
if self.is_taproot:
|
||||||
|
tweak = None
|
||||||
|
if self.tapscript:
|
||||||
|
tweak = self.tapscript.merkle_root
|
||||||
|
output_pubkey = chains.taptweak(self.key.serialize(), tweak)
|
||||||
|
return b"\x51\x20" + output_pubkey
|
||||||
|
|
||||||
|
if self.is_legacy_sh:
|
||||||
|
if self.miniscript:
|
||||||
|
# caller may have already built a script
|
||||||
|
scr = compiled_scr or self.miniscript.compile()
|
||||||
|
redeem_scr = scr
|
||||||
|
if self.addr_fmt == AF_P2WSH_P2SH:
|
||||||
|
redeem_scr = b"\x00\x20" + ngu.hash.sha256s(scr)
|
||||||
|
else:
|
||||||
|
redeem_scr = b"\x00\x14" + ngu.hash.hash160(self.key.node.pubkey())
|
||||||
|
|
||||||
|
return b"\xa9\x14" + ngu.hash.hash160(redeem_scr) + b"\x87"
|
||||||
|
|
||||||
|
if self.addr_fmt == AF_P2WSH:
|
||||||
|
# witness script p2wsh only
|
||||||
|
return b"\x00\x20" + ngu.hash.sha256s(compiled_scr or self.miniscript.compile())
|
||||||
|
|
||||||
|
if self.addr_fmt == AF_P2WPKH:
|
||||||
|
return b"\x00\x14" + ngu.hash.hash160(self.key.serialize())
|
||||||
|
|
||||||
|
# p2pkh
|
||||||
|
assert self.addr_fmt == AF_CLASSIC
|
||||||
|
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):
|
||||||
@ -266,142 +348,131 @@ 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 ValueError("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):
|
||||||
|
start = s.read(8)
|
||||||
|
af = AF_CLASSIC
|
||||||
|
internal_key = None
|
||||||
|
tapscript = None
|
||||||
|
if start.startswith(b"tr("):
|
||||||
|
af = AF_P2TR
|
||||||
|
s.seek(-5, 1)
|
||||||
|
internal_key = KeyExpression.read_from(s, 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("):
|
||||||
|
af = AF_P2WSH_P2SH
|
||||||
|
s.seek(-1, 1)
|
||||||
|
elif start.startswith(b"wsh("):
|
||||||
|
af = AF_P2WSH
|
||||||
|
s.seek(-4, 1)
|
||||||
|
elif start.startswith(b"sh(wpkh("):
|
||||||
|
af = AF_P2WPKH_P2SH
|
||||||
|
elif start.startswith(b"wpkh("):
|
||||||
|
af = AF_P2WPKH
|
||||||
|
s.seek(-3, 1)
|
||||||
|
elif start.startswith(b"pkh("):
|
||||||
|
s.seek(-4, 1)
|
||||||
|
elif start.startswith(b"sh("):
|
||||||
|
af = AF_P2SH
|
||||||
|
s.seek(-5, 1)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid descriptor")
|
||||||
|
|
||||||
|
miniscript = None
|
||||||
|
if af == AF_P2TR:
|
||||||
|
key = internal_key
|
||||||
|
nbrackets = 1
|
||||||
|
elif af in [AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH]:
|
||||||
|
miniscript = Miniscript.read_from(s)
|
||||||
|
key = internal_key
|
||||||
|
nbrackets = 1 + int(af == AF_P2WSH_P2SH)
|
||||||
|
else:
|
||||||
|
key = ExtendedKey.read_from(s, taproot=False)
|
||||||
|
nbrackets = 1 + int(af == AF_P2WPKH_P2SH)
|
||||||
|
|
||||||
|
end = s.read(nbrackets)
|
||||||
|
if end != b")" * nbrackets:
|
||||||
|
raise ValueError("Invalid descriptor")
|
||||||
|
|
||||||
|
desc = cls(key, miniscript, tapscript, af)
|
||||||
|
return desc
|
||||||
|
|
||||||
|
def to_string(self, external=True, internal=True, checksum=True):
|
||||||
|
if self.is_taproot:
|
||||||
|
desc = "tr(%s" % self.key.to_string(external, internal)
|
||||||
|
if self.tapscript:
|
||||||
|
desc += ","
|
||||||
|
tree = self.tapscript.to_string(external, internal)
|
||||||
|
desc += tree
|
||||||
|
|
||||||
|
res = desc + ")"
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self.miniscript is not None:
|
||||||
|
res = self.miniscript.to_string(external, internal)
|
||||||
|
if self.addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
|
||||||
|
res = "wsh(%s)" % res
|
||||||
|
else:
|
||||||
|
if self.addr_fmt in [AF_P2WPKH, AF_P2WPKH_P2SH]:
|
||||||
|
res = "wpkh(%s)" % self.key.to_string(external, internal)
|
||||||
|
else:
|
||||||
|
res = "pkh(%s)" % self.key.to_string(external, internal)
|
||||||
|
|
||||||
|
if self.is_legacy_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
|
# EOF
|
||||||
|
|||||||
@ -149,6 +149,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 version.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".
|
||||||
|
|||||||
@ -124,7 +124,8 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
|||||||
|
|
||||||
msg = "Password Index?" if picked == 7 else "Index Number?"
|
msg = "Password Index?" if picked == 7 else "Index Number?"
|
||||||
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
|
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
|
||||||
if index is None: return
|
if index is None:
|
||||||
|
return
|
||||||
|
|
||||||
dis.fullscreen("Working...")
|
dis.fullscreen("Working...")
|
||||||
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
||||||
@ -291,7 +292,7 @@ async def password_entry(*args, **kwargs):
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
the_ux.pop()
|
the_ux.pop()
|
||||||
index = await ux_enter_bip32_index("Password Index?")
|
index = await ux_enter_bip32_index("Password Index?", can_cancel=True)
|
||||||
if index is None:
|
if index is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
136
shared/export.py
136
shared/export.py
@ -5,11 +5,11 @@
|
|||||||
import stash, chains, version, ujson, ngu
|
import stash, chains, version, ujson, ngu
|
||||||
from uio import StringIO
|
from uio import StringIO
|
||||||
from ucollections import OrderedDict
|
from ucollections import OrderedDict
|
||||||
from utils import xfp2str, swab32
|
from utils import xfp2str, swab32, problem_file_line
|
||||||
from ux import ux_show_story, import_export_prompt
|
from ux import ux_show_story, import_export_prompt
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from msgsign import write_sig_file
|
from msgsign 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
|
||||||
from exceptions import QRTooBigError
|
from exceptions import QRTooBigError
|
||||||
@ -55,7 +55,10 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
|
|||||||
# len() is O(1)
|
# len() is O(1)
|
||||||
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
|
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
|
||||||
|
|
||||||
sig = not (derive is None and addr_fmt is None)
|
if addr_fmt == AF_P2TR:
|
||||||
|
sig = None
|
||||||
|
else:
|
||||||
|
sig = not (derive is None and addr_fmt is None)
|
||||||
|
|
||||||
ch = direct_way # set it to direct way only once, outside the loop
|
ch = direct_way # set it to direct way only once, outside the loop
|
||||||
while True:
|
while True:
|
||||||
@ -96,7 +99,7 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
|
|||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story('Failed to write!\n\n' + str(e))
|
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|
||||||
# both exceptions & success gets here
|
# both exceptions & success gets here
|
||||||
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
|
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
|
||||||
@ -171,7 +174,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 addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
|
if addr_fmt not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
|
||||||
yield ("%s => %s ##SLIP-132##\n" % (
|
yield ("%s => %s ##SLIP-132##\n" % (
|
||||||
hard_sub, chain.serialize_public(node, addr_fmt)))
|
hard_sub, chain.serialize_public(node, addr_fmt)))
|
||||||
|
|
||||||
@ -188,18 +191,12 @@ be needed for different systems.
|
|||||||
|
|
||||||
yield ('\n\n')
|
yield ('\n\n')
|
||||||
|
|
||||||
from multisig import MultisigWallet
|
from wallet import MiniScriptWallet
|
||||||
if MultisigWallet.exists():
|
if MiniScriptWallet.exists():
|
||||||
yield '\n# Your Multisig Wallets\n\n'
|
yield '\n# Your Multisig/Miniscript Wallets\n\n'
|
||||||
|
|
||||||
for ms in MultisigWallet.get_all():
|
for msc in MiniScriptWallet.iter_wallets():
|
||||||
fp = StringIO()
|
yield msc.to_string() + "\n---\n"
|
||||||
|
|
||||||
ms.render_export(fp)
|
|
||||||
print("\n---\n", file=fp)
|
|
||||||
|
|
||||||
yield fp.getvalue()
|
|
||||||
del fp
|
|
||||||
|
|
||||||
|
|
||||||
async def make_summary_file(fname_pattern='public.txt'):
|
async def make_summary_file(fname_pattern='public.txt'):
|
||||||
@ -223,10 +220,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
|
||||||
@ -242,7 +240,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.
|
||||||
@ -257,13 +258,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)
|
||||||
@ -272,44 +275,63 @@ 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, ExtendedKey
|
||||||
|
|
||||||
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 = ExtendedKey.from_cc_data(xfp, derive_v0, xpub_v0)
|
||||||
|
desc_v0 = Descriptor(key=key0, addr_fmt=AF_P2WPKH)
|
||||||
|
|
||||||
|
key1 = ExtendedKey.from_cc_data(xfp, derive_v1, xpub_v1)
|
||||||
|
desc_v1 = Descriptor(key=key1, 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.
|
||||||
@ -369,7 +391,7 @@ 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, ExtendedKey
|
||||||
|
|
||||||
chain = chains.current_chain()
|
chain = chains.current_chain()
|
||||||
master_xfp = settings.get("xfp")
|
master_xfp = settings.get("xfp")
|
||||||
@ -383,12 +405,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
|
||||||
@ -397,24 +421,25 @@ 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:
|
key = ExtendedKey.from_cc_data(master_xfp, dd, xp)
|
||||||
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
|
key_exp = key.to_string(external=False, internal=False)
|
||||||
else:
|
|
||||||
desc = Descriptor(keys=[(master_xfp, dd, xp)], addr_fmt=fmt).serialize(int_ext=True)
|
|
||||||
|
|
||||||
OWNERSHIP.note_wallet_used(fmt, account_num)
|
|
||||||
|
|
||||||
rv[name] = OrderedDict(name=atype,
|
rv[name] = OrderedDict(name=atype,
|
||||||
xfp=xfp,
|
xfp=xfp,
|
||||||
deriv=dd,
|
deriv=dd,
|
||||||
xpub=xp,
|
xpub=xp,
|
||||||
desc=desc)
|
key_exp=key_exp)
|
||||||
|
|
||||||
if zp and zp != xp:
|
if zp and zp != xp:
|
||||||
rv[name]['_pub'] = zp
|
rv[name]['_pub'] = zp
|
||||||
|
|
||||||
if not is_ms:
|
if not is_ms:
|
||||||
|
desc_obj = Descriptor(key=key, addr_fmt=fmt)
|
||||||
|
rv[name]['desc'] = desc_obj.to_string()
|
||||||
|
|
||||||
|
OWNERSHIP.note_wallet_used(fmt, account_num)
|
||||||
|
|
||||||
# bonus/check: first non-change address: 0/0
|
# bonus/check: first non-change address: 0/0
|
||||||
node.derive(0, False).derive(0, False)
|
node.derive(0, False).derive(0, False)
|
||||||
rv[name]['first'] = chain.address(node, fmt)
|
rv[name]['first'] = chain.address(node, fmt)
|
||||||
@ -466,7 +491,7 @@ def generate_electrum_wallet(addr_type, account_num):
|
|||||||
|
|
||||||
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", direct_way=None):
|
fname_pattern="descriptor.txt", direct_way=None):
|
||||||
from descriptor import Descriptor
|
from descriptor import Descriptor, ExtendedKey
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
dis.fullscreen('Generating...')
|
dis.fullscreen('Generating...')
|
||||||
@ -479,25 +504,28 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
|||||||
|
|
||||||
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 = ExtendedKey.from_cc_data(xfp, derive, xpub)
|
||||||
|
desc = Descriptor(key=key, 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)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from glob import settings
|
|||||||
from actions import *
|
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 wallet import make_miniscript_menu, import_miniscript_nfc
|
||||||
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
|
||||||
@ -20,10 +20,11 @@ from paper import make_paper_wallet
|
|||||||
from trick_pins import TrickPinMenu
|
from trick_pins import TrickPinMenu
|
||||||
from tapsigner import import_tapsigner_backup_file
|
from tapsigner import import_tapsigner_backup_file
|
||||||
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
|
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
|
||||||
from wif import WIFStoreMenu
|
from wif import WIFStore
|
||||||
|
|
||||||
# useful shortcut keys
|
# useful shortcut keys
|
||||||
from charcodes import KEY_QR, KEY_NFC
|
from charcodes import KEY_QR, KEY_NFC
|
||||||
|
from public_constants import AF_P2WPKH_P2SH, AF_P2WPKH
|
||||||
|
|
||||||
|
|
||||||
# Optional feature: HSM, depends on hardware
|
# Optional feature: HSM, depends on hardware
|
||||||
@ -104,7 +105,7 @@ def hsm_available():
|
|||||||
def qr_and_ms():
|
def qr_and_ms():
|
||||||
# has QR scanner, and at least one MS wallet
|
# has QR scanner, and at least one MS wallet
|
||||||
if not version.has_qr: return False
|
if not version.has_qr: return False
|
||||||
return bool(settings.get('multisig', False))
|
return bool(settings.get('miniscript', False))
|
||||||
|
|
||||||
def has_pushtx_url():
|
def has_pushtx_url():
|
||||||
# they want to use PushTX feature
|
# they want to use PushTX feature
|
||||||
@ -184,8 +185,8 @@ SettingsMenu = [
|
|||||||
# xxxxxxxxxxxxxxxx
|
# xxxxxxxxxxxxxxxx
|
||||||
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
||||||
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
|
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
|
||||||
NonDefaultMenuItem('Multisig Wallets', 'multisig',
|
NonDefaultMenuItem('Multisig/Miniscript', 'miniscript',
|
||||||
menu=make_multisig_menu, predicate=has_secrets, shortcut='m'),
|
menu=make_miniscript_menu, predicate=has_secrets, shortcut="m"),
|
||||||
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),
|
||||||
@ -215,6 +216,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"+("(BIP-86)" if version.has_qwerty else "(86)"), f=export_xpub, arg=86),
|
||||||
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49),
|
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(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),
|
||||||
@ -255,7 +257,7 @@ FileMgmtMenu = [
|
|||||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
|
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
|
||||||
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
||||||
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
|
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
|
||||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_has_secrets, f=kt_send_file_psbt),
|
MenuItem('Teleport Multisig/Miniscript PSBT', predicate=qr_and_has_secrets, f=kt_send_file_psbt),
|
||||||
MenuItem('List Files', f=list_files),
|
MenuItem('List Files', f=list_files),
|
||||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
MenuItem('Verify Sig File', f=verify_sig_file),
|
||||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
||||||
@ -393,7 +395,7 @@ NFCToolsMenu = [
|
|||||||
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
||||||
MenuItem('Verify Address', f=nfc_address_verify),
|
MenuItem('Verify Address', f=nfc_address_verify),
|
||||||
MenuItem('File Share', f=nfc_share_file),
|
MenuItem('File Share', f=nfc_share_file),
|
||||||
MenuItem('Import Multisig', f=import_multisig_nfc),
|
MenuItem('Import Miniscript', f=import_miniscript_nfc),
|
||||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -425,7 +427,7 @@ AdvancedNormalMenu = [
|
|||||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||||
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
|
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
|
||||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||||
MenuItem('WIF Store', menu=WIFStoreMenu.make),
|
MenuItem('WIF Store', menu=WIFStore.make_menu),
|
||||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
||||||
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||||
]
|
]
|
||||||
@ -488,7 +490,7 @@ NormalSystem = [
|
|||||||
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("secnap", False)),
|
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
|
||||||
MenuItem('Type Passwords', f=password_entry, shortcut='e',
|
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',
|
||||||
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
|
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
|
||||||
@ -547,12 +549,12 @@ HobbledAdvancedMenu = [
|
|||||||
# xxxxxxxxxxxxxxxx
|
# xxxxxxxxxxxxxxxx
|
||||||
MenuItem("File Management", menu=HobbledFileMgmtMenu),
|
MenuItem("File Management", menu=HobbledFileMgmtMenu),
|
||||||
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
||||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
|
MenuItem('Teleport Multisig/Miniscript PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
|
||||||
MenuItem("View Identity", f=view_ident),
|
MenuItem("View Identity", f=view_ident),
|
||||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
|
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
|
||||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
|
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
|
||||||
MenuItem('WIF Store', menu=WIFStoreMenu.make, predicate=sssp_related_keys),
|
MenuItem('WIF Store', menu=WIFStore.make_menu, predicate=sssp_related_keys),
|
||||||
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
|
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
|
||||||
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
|
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -29,4 +29,9 @@ NFC = None
|
|||||||
# QR scanner (Q1 only)
|
# QR scanner (Q1 only)
|
||||||
SCAN = None
|
SCAN = None
|
||||||
|
|
||||||
|
# Multisig/Miniscript descriptor cache
|
||||||
|
# mapping from unique wallet name to Descriptor object
|
||||||
|
# cache size = 1
|
||||||
|
DESC_CACHE = {}
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -5,15 +5,14 @@
|
|||||||
# 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 ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
||||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
|
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
||||||
from utils import cleanup_payment_address
|
from utils import cleanup_payment_address
|
||||||
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 wallet 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:
|
||||||
@ -179,7 +178,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 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
|
||||||
@ -220,10 +219,10 @@ class ApprovalRule:
|
|||||||
# redundant w/ code in pop_int() above
|
# redundant w/ code in pop_int() above
|
||||||
assert 1 <= self.min_users <= len(self.users), "range"
|
assert 1 <= self.min_users <= len(self.users), "range"
|
||||||
|
|
||||||
# if specified, 'wallet' must be an existing multisig wallet's name
|
# if specified, 'wallet' must be an existing miniscript 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()]
|
msc_names = [msc.name for msc in MiniScriptWallet.iter_wallets()]
|
||||||
assert self.wallet in names, "unknown MS wallet: "+self.wallet
|
assert self.wallet in msc_names, "unknown wallet: " + self.wallet
|
||||||
|
|
||||||
# patterns must be valid
|
# patterns must be valid
|
||||||
for p in self.patterns:
|
for p in self.patterns:
|
||||||
@ -267,9 +266,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 miniscript wallet "%s"' % self.wallet
|
||||||
|
|
||||||
if self.users:
|
if self.users:
|
||||||
rv += ' may be authorized by '
|
rv += ' may be authorized by '
|
||||||
@ -310,12 +309,11 @@ class ApprovalRule:
|
|||||||
# Does this rule apply to this PSBT file?
|
# Does this rule apply to this PSBT file?
|
||||||
if self.wallet:
|
if self.wallet:
|
||||||
# rule limited to one wallet
|
# rule limited to one wallet
|
||||||
if psbt.active_multisig:
|
if psbt.active_miniscript:
|
||||||
# if multisig signing, might need to match specific wallet name
|
assert self.wallet == psbt.active_miniscript.name, 'wrong miniscript wallet'
|
||||||
assert self.wallet == psbt.active_multisig.name, 'wrong wallet'
|
|
||||||
else:
|
else:
|
||||||
# non multisig, but does this rule apply to all wallets or single-singers
|
# not miniscript, 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'
|
||||||
@ -351,7 +349,7 @@ class ApprovalRule:
|
|||||||
# we are verifying the whole consensus-encoded txout
|
# we are verifying the whole consensus-encoded txout
|
||||||
txo_bytes = CTxOut(txo.nValue, txo.scriptPubKey).serialize()
|
txo_bytes = CTxOut(txo.nValue, txo.scriptPubKey).serialize()
|
||||||
digest = chain.hash_message(txo_bytes)
|
digest = chain.hash_message(txo_bytes)
|
||||||
addr_fmt, pubkey = chains.verify_recover_pubkey(o.attestation, digest)
|
addr_fmt, pubkey = chains.verify_recover_pubkey(psbt.get(o.attestation), digest)
|
||||||
# we have extracted a valid pubkey from the sig, but is it
|
# we have extracted a valid pubkey from the sig, but is it
|
||||||
# a whitelisted pubkey or something else?
|
# a whitelisted pubkey or something else?
|
||||||
ver_addr = chain.pubkey_to_address(pubkey, addr_fmt)
|
ver_addr = chain.pubkey_to_address(pubkey, addr_fmt)
|
||||||
@ -374,11 +372,11 @@ class ApprovalRule:
|
|||||||
|
|
||||||
# check the self-transfer percentage
|
# check the self-transfer percentage
|
||||||
if self.min_pct_self_transfer:
|
if self.min_pct_self_transfer:
|
||||||
own_in_value = sum([i.amount for i in psbt.inputs if i.num_our_keys])
|
own_in_value = sum([i.amount for i in psbt.inputs if i.sp_idxs])
|
||||||
own_out_value = 0
|
own_out_value = 0
|
||||||
for idx, txo in psbt.output_iter():
|
for idx, txo in psbt.output_iter():
|
||||||
o = psbt.outputs[idx]
|
o = psbt.outputs[idx]
|
||||||
if o.num_our_keys:
|
if o.sp_idxs:
|
||||||
own_out_value += txo.nValue
|
own_out_value += txo.nValue
|
||||||
percentage = (float(own_out_value) / own_in_value) * 100.0
|
percentage = (float(own_out_value) / own_in_value) * 100.0
|
||||||
assert percentage >= self.min_pct_self_transfer, 'does not meet self transfer threshold, expected: %.2f, actual: %.2f' % (self.min_pct_self_transfer, percentage)
|
assert percentage >= self.min_pct_self_transfer, 'does not meet self transfer threshold, expected: %.2f, actual: %.2f' % (self.min_pct_self_transfer, percentage)
|
||||||
@ -389,8 +387,8 @@ class ApprovalRule:
|
|||||||
assert len(psbt.inputs) == len(psbt.outputs), 'unequal number of inputs and outputs'
|
assert len(psbt.inputs) == len(psbt.outputs), 'unequal number of inputs and outputs'
|
||||||
|
|
||||||
if "EQ_NUM_OWN_INS_OUTS" in self.patterns:
|
if "EQ_NUM_OWN_INS_OUTS" in self.patterns:
|
||||||
own_ins = sum([1 for i in psbt.inputs if i.num_our_keys])
|
own_ins = sum([1 for i in psbt.inputs if i.sp_idxs])
|
||||||
own_outs = sum([1 for o in psbt.outputs if o.num_our_keys])
|
own_outs = sum([1 for o in psbt.outputs if o.sp_idxs])
|
||||||
assert own_ins == own_outs, 'unequal number of own inputs and outputs'
|
assert own_ins == own_outs, 'unequal number of own inputs and outputs'
|
||||||
|
|
||||||
if "EQ_OUT_AMOUNTS" in self.patterns:
|
if "EQ_OUT_AMOUNTS" in self.patterns:
|
||||||
@ -488,9 +486,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', ['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)
|
||||||
@ -575,7 +573,7 @@ class HSMPolicy:
|
|||||||
fd.write('\n')
|
fd.write('\n')
|
||||||
|
|
||||||
def plist(pl):
|
def plist(pl):
|
||||||
remap = {'any': '(any path)', 'p2sh': '(any P2SH)' }
|
remap = {'any': '(any path)', 'msas': '(any miniscript)' }
|
||||||
return ' OR '.join(remap.get(i, i) for i in pl)
|
return ' OR '.join(remap.get(i, i) for i in pl)
|
||||||
|
|
||||||
fd.write('\nMessage signing:\n')
|
fd.write('\nMessage signing:\n')
|
||||||
@ -656,15 +654,6 @@ class HSMPolicy:
|
|||||||
assert not glob.hsm_active
|
assert not glob.hsm_active
|
||||||
glob.hsm_active = self
|
glob.hsm_active = self
|
||||||
|
|
||||||
# HSM is the locked-down operating mode: shut down peripherals
|
|
||||||
# that enlarge the USB-stack interaction surface.
|
|
||||||
# - VDisk: MSC bulk OUT and HID OUT share the STM32 OTG_FS RX FIFO;
|
|
||||||
# under load this can wedge the HID OUT endpoint permanently
|
|
||||||
if glob.VD is not None:
|
|
||||||
glob.VD.shutdown()
|
|
||||||
if glob.NFC is not None:
|
|
||||||
glob.NFC.shutdown()
|
|
||||||
|
|
||||||
self.start_time = utime.ticks_ms()
|
self.start_time = utime.ticks_ms()
|
||||||
|
|
||||||
if new_file:
|
if new_file:
|
||||||
@ -807,14 +796,14 @@ 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, 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 is_p2sh:
|
if miniscript:
|
||||||
return ('p2sh' in self.share_addrs)
|
return ('msas' in self.share_addrs)
|
||||||
|
|
||||||
return match_deriv_path(self.share_addrs, subpath)
|
return match_deriv_path(self.share_addrs, subpath)
|
||||||
|
|
||||||
@ -882,39 +871,17 @@ class HSMPolicy:
|
|||||||
# do this super early so always cleared even if other issues
|
# do this super early so always cleared even if other issues
|
||||||
local_ok = self.consume_local_code(psbt_sha)
|
local_ok = self.consume_local_code(psbt_sha)
|
||||||
|
|
||||||
|
if not self.rules:
|
||||||
|
raise ValueError("no txn signing allowed")
|
||||||
|
|
||||||
# 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:
|
||||||
raise ValueError("has %d warning(s)" % len(psbt.warnings))
|
raise ValueError("has %d warning(s)" % len(psbt.warnings))
|
||||||
|
|
||||||
if psbt.por322:
|
|
||||||
if not self.msg_paths:
|
|
||||||
raise ValueError("Message signing not permitted")
|
|
||||||
|
|
||||||
for inp in psbt.inputs:
|
|
||||||
if not inp.required_key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if inp.is_multisig:
|
|
||||||
paths = [
|
|
||||||
keypath_to_str(inp.subpaths[pk])
|
|
||||||
for pk in inp.required_key
|
|
||||||
if pk in inp.subpaths
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
paths = [keypath_to_str(inp.subpaths[inp.required_key])]
|
|
||||||
|
|
||||||
if not any(match_deriv_path(self.msg_paths, p) for p in paths):
|
|
||||||
raise ValueError("Message signing not enabled for that path")
|
|
||||||
|
|
||||||
self.approve(log, "BIP-322 message signing allowed")
|
|
||||||
return 'y'
|
|
||||||
|
|
||||||
if not self.rules:
|
|
||||||
raise ValueError("no txn signing allowed")
|
|
||||||
|
|
||||||
# See who has entered creditials already (all must be valid).
|
# See who has entered creditials already (all must be valid).
|
||||||
users = []
|
users = []
|
||||||
for u, (token, counter) in auth.items():
|
for u, (token, counter) in auth.items():
|
||||||
@ -1010,7 +977,7 @@ 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'] = [msc.name for msc in MiniScriptWallet.iter_wallets()]
|
||||||
|
|
||||||
rv['chain'] = settings.get('chain', 'BTC')
|
rv['chain'] = settings.get('chain', 'BTC')
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
|
|||||||
msg = '''Last chance. You are defining a new policy which \
|
msg = '''Last chance. You are defining a new policy which \
|
||||||
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
|
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
|
||||||
Policy hash:\n%s\n\n
|
Policy hash:\n%s\n\n
|
||||||
Press (%s) to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
||||||
|
|
||||||
ch = await ux_show_story(msg, title=self.title,
|
ch = await ux_show_story(msg, title=self.title,
|
||||||
escape='x'+confirm_char, strict_escape=True)
|
escape='x'+confirm_char, strict_escape=True)
|
||||||
@ -297,7 +297,7 @@ class hsmUxInteraction:
|
|||||||
|
|
||||||
|
|
||||||
# replacements for display.py:Display functions
|
# replacements for display.py:Display functions
|
||||||
def hack_fullscreen(self, msg, percent=None):
|
def hack_fullscreen(self, msg, percent=None, **kwargs):
|
||||||
self.draw_busy(msg, percent)
|
self.draw_busy(msg, percent)
|
||||||
def hack_progress_bar(self, percent):
|
def hack_progress_bar(self, percent):
|
||||||
self.draw_busy(None, percent)
|
self.draw_busy(None, percent)
|
||||||
|
|||||||
@ -134,7 +134,7 @@ class FullKeyboard(NumpadBase):
|
|||||||
if self._history[kn] == NUM_SAMPLES:
|
if self._history[kn] == NUM_SAMPLES:
|
||||||
self.is_pressed[kn] = 1
|
self.is_pressed[kn] = 1
|
||||||
new_presses.add(kn)
|
new_presses.add(kn)
|
||||||
elif self._history[kn] == 0:
|
elif self._history[i] == 0:
|
||||||
self.is_pressed[kn] = 0
|
self.is_pressed[kn] = 0
|
||||||
self._history[kn] = 0
|
self._history[kn] = 0
|
||||||
|
|
||||||
|
|||||||
@ -642,10 +642,10 @@ class Display:
|
|||||||
|
|
||||||
def _draw_addr(self, y, addr, prev_x=None):
|
def _draw_addr(self, y, addr, prev_x=None):
|
||||||
# Draw a single-line of an address
|
# Draw a single-line of an address
|
||||||
# - use prev_x=0 to start centered
|
# - use prev_x=0 to start centered
|
||||||
if prev_x is None:
|
if prev_x is None:
|
||||||
# left justify (for stories)
|
# left justify (for stories)
|
||||||
prev_x = x = 1
|
prev_x = x = 1
|
||||||
elif prev_x == 0:
|
elif prev_x == 0:
|
||||||
# center first line, following line(s) will be left-justified to match that
|
# center first line, following line(s) will be left-justified to match that
|
||||||
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
|
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
|
||||||
@ -734,9 +734,7 @@ class Display:
|
|||||||
lines = self.handle_qr_msg(msg, max_lines=True)
|
lines = self.handle_qr_msg(msg, max_lines=True)
|
||||||
self.draw_qr_lines(lines, False)
|
self.draw_qr_lines(lines, False)
|
||||||
|
|
||||||
if idx_hint:
|
self.draw_qr_idx_hint(idx_hint)
|
||||||
self.draw_qr_idx_hint(idx_hint)
|
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ freeze_as_mpy('', [
|
|||||||
'address_explorer.py',
|
'address_explorer.py',
|
||||||
'auth.py',
|
'auth.py',
|
||||||
'backups.py',
|
'backups.py',
|
||||||
'block_height.py',
|
'bsms.py',
|
||||||
'callgate.py',
|
'callgate.py',
|
||||||
'ccc.py',
|
'ccc.py',
|
||||||
'chains.py',
|
'chains.py',
|
||||||
@ -14,6 +14,9 @@ freeze_as_mpy('', [
|
|||||||
'compat7z.py',
|
'compat7z.py',
|
||||||
'countdowns.py',
|
'countdowns.py',
|
||||||
'descriptor.py',
|
'descriptor.py',
|
||||||
|
'desc_utils.py',
|
||||||
|
'dev_helper.py',
|
||||||
|
'display.py',
|
||||||
'drv_entro.py',
|
'drv_entro.py',
|
||||||
'exceptions.py',
|
'exceptions.py',
|
||||||
'export.py',
|
'export.py',
|
||||||
@ -26,6 +29,7 @@ freeze_as_mpy('', [
|
|||||||
'login.py',
|
'login.py',
|
||||||
'main.py',
|
'main.py',
|
||||||
'menu.py',
|
'menu.py',
|
||||||
|
"miniscript.py",
|
||||||
'mk4.py',
|
'mk4.py',
|
||||||
'msgsign.py',
|
'msgsign.py',
|
||||||
'multisig.py',
|
'multisig.py',
|
||||||
@ -37,6 +41,7 @@ freeze_as_mpy('', [
|
|||||||
'ownership.py',
|
'ownership.py',
|
||||||
'paper.py',
|
'paper.py',
|
||||||
'pincodes.py',
|
'pincodes.py',
|
||||||
|
'precomp_tag_hash.py',
|
||||||
'psbt.py',
|
'psbt.py',
|
||||||
'psram.py',
|
'psram.py',
|
||||||
'pwsave.py',
|
'pwsave.py',
|
||||||
@ -47,6 +52,7 @@ freeze_as_mpy('', [
|
|||||||
'selftest.py',
|
'selftest.py',
|
||||||
'serializations.py',
|
'serializations.py',
|
||||||
'sffile.py',
|
'sffile.py',
|
||||||
|
'ssd1306.py',
|
||||||
'stash.py',
|
'stash.py',
|
||||||
'tapsigner.py',
|
'tapsigner.py',
|
||||||
'trick_pins.py',
|
'trick_pins.py',
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# Mk4 only files; would not be needed on Mk3 or earlier.
|
# Mk4 only files; would not be needed on Mk3 or earlier.
|
||||||
freeze_as_mpy('', [
|
freeze_as_mpy('', [
|
||||||
'display.py',
|
|
||||||
'hsm.py',
|
'hsm.py',
|
||||||
'hsm_ux.py',
|
'hsm_ux.py',
|
||||||
'mempad.py',
|
'mempad.py',
|
||||||
|
|||||||
@ -290,7 +290,7 @@ class MenuSystem:
|
|||||||
dis.clear()
|
dis.clear()
|
||||||
|
|
||||||
cursor_y = None
|
cursor_y = None
|
||||||
for n in range(PER_M+1):
|
for n in range(self.ypos+PER_M+1):
|
||||||
real_idx = n+self.ypos
|
real_idx = n+self.ypos
|
||||||
if real_idx >= self.count: break
|
if real_idx >= self.count: break
|
||||||
|
|
||||||
|
|||||||
1171
shared/miniscript.py
Normal file
1171
shared/miniscript.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -179,16 +179,14 @@ async def msg_sign_ux_get_subpath(addr_fmt):
|
|||||||
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
||||||
chain_n = chains.current_chain().b44_cointype
|
chain_n = chains.current_chain().b44_cointype
|
||||||
|
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
|
|
||||||
ch = await ux_show_story(title="Change?",
|
ch = await ux_show_story(title="Change?",
|
||||||
msg="Press (0) to use internal/change address,"
|
msg="Press (0) to use internal/change address,"
|
||||||
" %s to use external/receive address." % OK, escape="0")
|
" %s to use external/receive address." % OK, escape="0")
|
||||||
change = 1 if ch == '0' else 0
|
change = 1 if ch == '0' else 0
|
||||||
|
|
||||||
idx = await ux_enter_bip32_index('Index Number:')
|
idx = await ux_enter_bip32_index('Index Number:') or 0
|
||||||
if idx is None: return
|
|
||||||
|
|
||||||
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
|
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
|
||||||
|
|
||||||
@ -262,13 +260,13 @@ def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_
|
|||||||
|
|
||||||
return sig_nice
|
return sig_nice
|
||||||
|
|
||||||
def validate_text_for_signing(text, allow_tab_nl=False):
|
def validate_text_for_signing(text, only_printable=True):
|
||||||
# Check for some UX/UI traps in the message itself.
|
# Check for some UX/UI traps in the message itself.
|
||||||
# - messages must be short and ascii only. Our charset is limited
|
# - messages must be short and ascii only. Our charset is limited
|
||||||
# - too many spaces, leading/trailing can be an issue
|
# - too many spaces, leading/trailing can be an issue
|
||||||
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
||||||
text = str(text, "ascii") # handle memoryview coming from USB
|
|
||||||
result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
|
result = to_ascii_printable(text, only_printable=only_printable)
|
||||||
|
|
||||||
length = len(result)
|
length = len(result)
|
||||||
assert length >= 2, "msg too short (min. 2)"
|
assert length >= 2, "msg too short (min. 2)"
|
||||||
@ -315,7 +313,6 @@ def parse_msg_sign_request(data):
|
|||||||
if text is None:
|
if text is None:
|
||||||
raise AssertionError("MSG required")
|
raise AssertionError("MSG required")
|
||||||
subpath = data_dict.get("subpath", subpath)
|
subpath = data_dict.get("subpath", subpath)
|
||||||
assert isinstance(subpath, str), "subpath"
|
|
||||||
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
||||||
is_json = True
|
is_json = True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -334,13 +331,11 @@ def parse_msg_sign_request(data):
|
|||||||
addr_fmt = addr_fmt_from_subpath(subpath)
|
addr_fmt = addr_fmt_from_subpath(subpath)
|
||||||
|
|
||||||
if not subpath:
|
if not subpath:
|
||||||
try:
|
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
||||||
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
subpath = subpath.format(
|
||||||
subpath = subpath.format(
|
coin_type=chains.current_chain().b44_cointype,
|
||||||
coin_type=chains.current_chain().b44_cointype,
|
account=0, change=0, idx=0
|
||||||
account=0, change=0, idx=0
|
)
|
||||||
)
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
return text, subpath, addr_fmt, is_json
|
return text, subpath, addr_fmt, is_json
|
||||||
|
|
||||||
@ -413,10 +408,9 @@ async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
|
|||||||
|
|
||||||
text, af = item.arg
|
text, af = item.arg
|
||||||
subpath = await msg_sign_ux_get_subpath(af)
|
subpath = await msg_sign_ux_get_subpath(af)
|
||||||
if subpath is None: return
|
|
||||||
|
|
||||||
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
||||||
kill_menu=kill_menu, allow_tab_nl=True)
|
kill_menu=kill_menu, only_printable=False)
|
||||||
|
|
||||||
# pick address format
|
# pick address format
|
||||||
rv = [
|
rv = [
|
||||||
|
|||||||
1819
shared/multisig.py
1819
shared/multisig.py
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# ndef.py -- NDEF records: making them and parsing them.
|
# ndef.py -- NDEF records: making them and parsing them.
|
||||||
#
|
#
|
||||||
# - see ../docs/nfc-coldcard.md for background.
|
# - see ../docs/nfc-on-coldcard.md for background.
|
||||||
# - cross platform file
|
# - cross platform file
|
||||||
#
|
#
|
||||||
from struct import pack, unpack
|
from struct import pack, unpack
|
||||||
|
|||||||
120
shared/nfc.py
120
shared/nfc.py
@ -226,13 +226,10 @@ class NFCHandler:
|
|||||||
self.set_rf_disable(1)
|
self.set_rf_disable(1)
|
||||||
|
|
||||||
async def share_loop(self, n, **kws):
|
async def share_loop(self, n, **kws):
|
||||||
# Keep one fully-written tag image live until the user exits. Some
|
|
||||||
# phones perform multiple probes while deciding if a tag is NDEF.
|
|
||||||
await self.big_write(n.bytes())
|
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
aborted = await self.ux_animation(exit_after_activity=False, **kws)
|
done = await self.share_start(n, **kws)
|
||||||
if aborted:
|
if done:
|
||||||
|
# do not wipe if we are not done
|
||||||
await self.wipe(kws.get("is_secret", False))
|
await self.wipe(kws.get("is_secret", False))
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -404,9 +401,8 @@ class NFCHandler:
|
|||||||
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
||||||
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
||||||
|
|
||||||
async def ux_animation(self, allow_enter=True, prompt=None, line2=None,
|
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None,
|
||||||
is_secret=False, exit_after_activity=True,
|
is_secret=False):
|
||||||
min_delay=1000):
|
|
||||||
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
||||||
# - similar when "read" and then removed from field
|
# - similar when "read" and then removed from field
|
||||||
# - return T if aborted by user
|
# - return T if aborted by user
|
||||||
@ -432,6 +428,7 @@ class NFCHandler:
|
|||||||
|
|
||||||
# (ms) How long to wait after RF field comes and goes
|
# (ms) How long to wait after RF field comes and goes
|
||||||
# - user can press OK during this period if they know they are done
|
# - user can press OK during this period if they know they are done
|
||||||
|
min_delay = (3000 if write_mode else 1000)
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
if dis.has_lcd:
|
if dis.has_lcd:
|
||||||
@ -470,7 +467,7 @@ class NFCHandler:
|
|||||||
aborted = False
|
aborted = False
|
||||||
break
|
break
|
||||||
|
|
||||||
if exit_after_activity and last_activity:
|
if last_activity:
|
||||||
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
|
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
|
||||||
if dt >= min_delay:
|
if dt >= min_delay:
|
||||||
# They acheived some RF activity and then nothing for some time, so
|
# They acheived some RF activity and then nothing for some time, so
|
||||||
@ -487,14 +484,14 @@ class NFCHandler:
|
|||||||
# - assumpting is people know what they are scanning
|
# - assumpting is people know what they are scanning
|
||||||
# - x key to abort early, but also self-clears
|
# - x key to abort early, but also self-clears
|
||||||
await self.big_write(ndef_obj.bytes())
|
await self.big_write(ndef_obj.bytes())
|
||||||
return await self.ux_animation(**kws)
|
return await self.ux_animation(False, **kws)
|
||||||
|
|
||||||
async def start_nfc_rx(self, **kws):
|
async def start_nfc_rx(self, **kws):
|
||||||
# Pretend to be a big warm empty tag ready to be stuffed with data
|
# Pretend to be a big warm empty tag ready to be stuffed with data
|
||||||
await self.big_write(ndef.CC_WR_FILE)
|
await self.big_write(ndef.CC_WR_FILE)
|
||||||
|
|
||||||
# wait until something is written
|
# wait until something is written
|
||||||
aborted = await self.ux_animation(min_delay=3000, **kws)
|
aborted = await self.ux_animation(True, **kws)
|
||||||
if aborted: return
|
if aborted: return
|
||||||
|
|
||||||
# read CCFILE area (header)
|
# read CCFILE area (header)
|
||||||
@ -522,7 +519,7 @@ class NFCHandler:
|
|||||||
await self.wipe(False)
|
await self.wipe(False)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
async def start_psbt_rx(self):
|
async def start_psbt_rx(self, miniscript_wallet=None):
|
||||||
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
|
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
|
||||||
from auth import UserAuthorizedAction, ApproveTransaction
|
from auth import UserAuthorizedAction, ApproveTransaction
|
||||||
from ux import the_ux
|
from ux import the_ux
|
||||||
@ -570,7 +567,7 @@ class NFCHandler:
|
|||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||||
psbt_len, psbt_sha=psbt_sha, input_method="nfc",
|
psbt_len, psbt_sha=psbt_sha, input_method="nfc",
|
||||||
output_encoder=output_encoder
|
output_encoder=output_encoder, miniscript_wallet=miniscript_wallet,
|
||||||
)
|
)
|
||||||
# kill any menu stack, and put our thing at the top
|
# kill any menu stack, and put our thing at the top
|
||||||
the_ux.push(UserAuthorizedAction.active_request)
|
the_ux.push(UserAuthorizedAction.active_request)
|
||||||
@ -588,7 +585,7 @@ class NFCHandler:
|
|||||||
|
|
||||||
aborted = await n.share_start(nn, allow_enter=False)
|
aborted = await n.share_start(nn, 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
|
||||||
@ -621,7 +618,7 @@ class NFCHandler:
|
|||||||
# it's a txn, and we wrote as hex
|
# it's a txn, and we wrote as hex
|
||||||
data = a2b_hex(data)
|
data = a2b_hex(data)
|
||||||
else:
|
else:
|
||||||
assert data[1:4] == bytes(3)
|
assert data[2:8] == bytes(6)
|
||||||
sha = ngu.hash.sha256s(data)
|
sha = ngu.hash.sha256s(data)
|
||||||
await self.share_signed_txn(txid, data, len(data), sha)
|
await self.share_signed_txn(txid, data, len(data), sha)
|
||||||
elif ext == 'psbt':
|
elif ext == 'psbt':
|
||||||
@ -634,29 +631,6 @@ class NFCHandler:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(ext)
|
raise ValueError(ext)
|
||||||
|
|
||||||
async def import_multisig_nfc(self, *a):
|
|
||||||
# user is pushing a file downloaded from another CC over NFC
|
|
||||||
# - would need an NFC app in between for the sneakernet step
|
|
||||||
# get some data
|
|
||||||
def f(m):
|
|
||||||
if len(m) < 70:
|
|
||||||
return
|
|
||||||
m = m.decode()
|
|
||||||
|
|
||||||
# multi( catches both multi( and sortedmulti(
|
|
||||||
if 'pub' in m or "multi(" in m:
|
|
||||||
return m
|
|
||||||
|
|
||||||
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
|
|
||||||
|
|
||||||
if winner:
|
|
||||||
from auth import maybe_enroll_xpub
|
|
||||||
try:
|
|
||||||
maybe_enroll_xpub(config=winner)
|
|
||||||
except Exception as e:
|
|
||||||
#import sys; sys.print_exception(e)
|
|
||||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
|
||||||
|
|
||||||
async def import_ephemeral_seed_words_nfc(self, *a):
|
async def import_ephemeral_seed_words_nfc(self, *a):
|
||||||
def f(m):
|
def f(m):
|
||||||
sm = m.decode().strip().split(" ")
|
sm = m.decode().strip().split(" ")
|
||||||
@ -750,11 +724,10 @@ class NFCHandler:
|
|||||||
|
|
||||||
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.
|
||||||
res = await self.read_address()
|
_, addr, args = await self.read_address()
|
||||||
if not res: return
|
if addr:
|
||||||
_, addr, args = res
|
from ownership import OWNERSHIP
|
||||||
from ownership import OWNERSHIP
|
await OWNERSHIP.search_ux(addr, args)
|
||||||
await OWNERSHIP.search_ux(addr, args)
|
|
||||||
|
|
||||||
async def read_extended_private_key(self):
|
async def read_extended_private_key(self):
|
||||||
f = lambda x: x.decode().strip() if b"prv" in x else None
|
f = lambda x: x.decode().strip() if b"prv" in x else None
|
||||||
@ -778,17 +751,15 @@ class NFCHandler:
|
|||||||
if not data: return
|
if not data: return
|
||||||
|
|
||||||
winner = None
|
winner = None
|
||||||
try:
|
for urn, msg, meta in ndef.record_parser(data):
|
||||||
for urn, msg, meta in ndef.record_parser(data):
|
msg = bytes(msg)
|
||||||
msg = bytes(msg)
|
try:
|
||||||
try:
|
r = func(msg)
|
||||||
r = func(msg)
|
if r is not None:
|
||||||
if r is not None:
|
winner = r
|
||||||
winner = r
|
break
|
||||||
break
|
except:
|
||||||
except:
|
pass
|
||||||
pass
|
|
||||||
except Exception: pass # dont crash when given garbage
|
|
||||||
|
|
||||||
if not winner:
|
if not winner:
|
||||||
await ux_show_story(fail_msg)
|
await ux_show_story(fail_msg)
|
||||||
@ -796,4 +767,43 @@ class NFCHandler:
|
|||||||
|
|
||||||
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):
|
||||||
|
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)
|
||||||
|
except Exception as e:
|
||||||
|
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
208
shared/notes.py
208
shared/notes.py
@ -10,11 +10,10 @@ from ux_q1 import QRScannerInteraction
|
|||||||
from actions import goto_top_menu
|
from actions import goto_top_menu
|
||||||
from glob import settings, dis
|
from glob import settings, dis
|
||||||
from files import CardMissingError, needs_microsd, CardSlot
|
from files import CardMissingError, needs_microsd, CardSlot
|
||||||
from public_constants import MSG_SIGNING_MAX_LENGTH
|
|
||||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||||
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
|
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
|
||||||
from lcd_display import CHARS_W
|
from lcd_display import CHARS_W
|
||||||
from utils import problem_file_line, url_unquote, wipe_if_deltamode, is_printable
|
from utils import problem_file_line, url_unquote, wipe_if_deltamode
|
||||||
|
|
||||||
# title, username and such are limited that they fit on the one line both in
|
# title, username and such are limited that they fit on the one line both in
|
||||||
# text entry (W-2) and also in menu display (W-3)
|
# text entry (W-2) and also in menu display (W-3)
|
||||||
@ -131,7 +130,9 @@ class NotesMenu(MenuSystem):
|
|||||||
else:
|
else:
|
||||||
wipe_if_deltamode()
|
wipe_if_deltamode()
|
||||||
|
|
||||||
rv = cls.construct_note_items(readonly=False)
|
rv = []
|
||||||
|
for note in NoteContent.get_all():
|
||||||
|
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
|
||||||
|
|
||||||
rv.extend(news)
|
rv.extend(news)
|
||||||
|
|
||||||
@ -149,34 +150,16 @@ class NotesMenu(MenuSystem):
|
|||||||
# When only allowed to view, no export/add new/delete.
|
# When only allowed to view, no export/add new/delete.
|
||||||
wipe_if_deltamode()
|
wipe_if_deltamode()
|
||||||
|
|
||||||
rv = cls.construct_note_items(readonly=True)
|
rv = []
|
||||||
|
for note in NoteContent.get_all():
|
||||||
|
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||||
|
menu=note.make_menu, arg=True)) # readonly=True
|
||||||
|
|
||||||
if not rv:
|
if not rv:
|
||||||
rv.append(MenuItem('(none saved yet)'))
|
rv.append(MenuItem('(none saved yet)'))
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def construct_note_items(cls, readonly=False):
|
|
||||||
rv = []
|
|
||||||
by_group = {}
|
|
||||||
|
|
||||||
for note in NoteContent.get_all():
|
|
||||||
item = MenuItem('%d: %s' % (note.idx+1, note.title),
|
|
||||||
menu=note.make_menu, arg=readonly)
|
|
||||||
group = note.group
|
|
||||||
if group:
|
|
||||||
if group not in by_group:
|
|
||||||
by_group[group] = []
|
|
||||||
by_group[group].append(item)
|
|
||||||
else:
|
|
||||||
rv.append(item)
|
|
||||||
|
|
||||||
for group in sorted(by_group):
|
|
||||||
rv.append(MenuItem('↳ ' + group, menu=NoteGroupMenu(group, readonly)))
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def export_all(cls, *a):
|
async def export_all(cls, *a):
|
||||||
await start_export(NoteContent.get_all())
|
await start_export(NoteContent.get_all())
|
||||||
@ -228,7 +211,7 @@ class NotesMenu(MenuSystem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
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 losing anything
|
# - no need for confirm, they aren't loosing anything
|
||||||
settings.remove_key('secnap')
|
settings.remove_key('secnap')
|
||||||
settings.remove_key('notes')
|
settings.remove_key('notes')
|
||||||
settings.save()
|
settings.save()
|
||||||
@ -247,28 +230,10 @@ class NotesMenu(MenuSystem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def drill_to(cls, menu, item):
|
async def drill_to(cls, menu, item):
|
||||||
# make it so looks like we drilled down into the new note
|
# make it so looks like we drilled down into the new note
|
||||||
label = '%d: %s' % (item.idx+1, item.title)
|
menu.goto_idx(item.idx)
|
||||||
group = item.group
|
|
||||||
if group:
|
|
||||||
cls.goto_exact_label(menu, '↳ ' + group)
|
|
||||||
gm = NoteGroupMenu(group)
|
|
||||||
cls.goto_exact_label(gm, label)
|
|
||||||
the_ux.push(gm)
|
|
||||||
else:
|
|
||||||
cls.goto_exact_label(menu, label)
|
|
||||||
|
|
||||||
m = await item._make_menu()
|
m = await item._make_menu()
|
||||||
the_ux.push(MenuSystem(m))
|
the_ux.push(MenuSystem(m))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def goto_exact_label(menu, label):
|
|
||||||
for i, mi in enumerate(menu.items):
|
|
||||||
if mi.label == label:
|
|
||||||
menu.goto_idx(i)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class NoteContentBase:
|
class NoteContentBase:
|
||||||
def __init__(self, json={}, idx=-1):
|
def __init__(self, json={}, idx=-1):
|
||||||
@ -284,15 +249,9 @@ class NoteContentBase:
|
|||||||
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
|
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
res = {}
|
return {fld:getattr(self, fld, '') for fld in self.flds}
|
||||||
for fld in self.flds:
|
|
||||||
val = getattr(self, fld, '')
|
|
||||||
# user field is necessary for proper password identification in constructor
|
|
||||||
if not val and (fld != "user"):
|
|
||||||
continue
|
|
||||||
res[fld] = val
|
|
||||||
|
|
||||||
return res
|
to_json = serialize
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls):
|
def get_all(cls):
|
||||||
@ -318,15 +277,6 @@ class NoteContentBase:
|
|||||||
settings.put('notes', [n.serialize() for n in notes])
|
settings.put('notes', [n.serialize() for n in notes])
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_groups(cls):
|
|
||||||
groups = set()
|
|
||||||
for note in cls.get_all():
|
|
||||||
if note.group:
|
|
||||||
groups.add(note.group)
|
|
||||||
|
|
||||||
return sorted(groups)
|
|
||||||
|
|
||||||
async def delete(self, *a):
|
async def delete(self, *a):
|
||||||
# Remove note
|
# Remove note
|
||||||
ok = await ux_confirm("Everything about this note/password will be lost.")
|
ok = await ux_confirm("Everything about this note/password will be lost.")
|
||||||
@ -347,11 +297,6 @@ class NoteContentBase:
|
|||||||
the_ux.pop()
|
the_ux.pop()
|
||||||
m = the_ux.top_of_stack()
|
m = the_ux.top_of_stack()
|
||||||
m.update_contents()
|
m.update_contents()
|
||||||
parent = the_ux.parent_of(m)
|
|
||||||
if parent:
|
|
||||||
parent.update_contents()
|
|
||||||
if isinstance(m, NoteGroupMenu) and not m.has_notes():
|
|
||||||
the_ux.pop()
|
|
||||||
|
|
||||||
await ux_dramatic_pause('Deleted.', 3)
|
await ux_dramatic_pause('Deleted.', 3)
|
||||||
|
|
||||||
@ -389,11 +334,6 @@ class NoteContentBase:
|
|||||||
# update parent
|
# update parent
|
||||||
parent = the_ux.parent_of(menu)
|
parent = the_ux.parent_of(menu)
|
||||||
parent.update_contents()
|
parent.update_contents()
|
||||||
grandparent = the_ux.parent_of(parent)
|
|
||||||
if grandparent:
|
|
||||||
grandparent.update_contents()
|
|
||||||
if isinstance(parent, NoteGroupMenu) and not parent.has_notes():
|
|
||||||
the_ux.stack.remove(parent)
|
|
||||||
else:
|
else:
|
||||||
menu.update_contents()
|
menu.update_contents()
|
||||||
|
|
||||||
@ -423,94 +363,12 @@ class NoteContentBase:
|
|||||||
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
||||||
|
|
||||||
def sign_misc_menu_item(self):
|
def sign_misc_menu_item(self):
|
||||||
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc,
|
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc)
|
||||||
predicate=2 <= len(self.misc) <= MSG_SIGNING_MAX_LENGTH)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_b39pass_applicable(data, read_only):
|
|
||||||
from seed import MAX_PASS_LEN
|
|
||||||
from ccc import sssp_spending_policy
|
|
||||||
if read_only and not sssp_spending_policy('okeys'):
|
|
||||||
return False
|
|
||||||
return (len(data) <= MAX_PASS_LEN) and is_printable(data) and settings.get("words", True)
|
|
||||||
|
|
||||||
async def apply_as_b39_pass(self, a, b, item):
|
|
||||||
data, readonly = item.arg
|
|
||||||
# rstrip just trailing whitespaces/tabs/newlines
|
|
||||||
data = data.rstrip()
|
|
||||||
# do not allow any more tabs/newlines
|
|
||||||
assert self.is_b39pass_applicable(data, readonly)
|
|
||||||
from seed import apply_pass_value
|
|
||||||
await apply_pass_value(data)
|
|
||||||
|
|
||||||
|
|
||||||
class NoteGroupMenu(MenuSystem):
|
|
||||||
def __init__(self, group, readonly=False):
|
|
||||||
self.group = group
|
|
||||||
self.readonly = readonly
|
|
||||||
super().__init__(self.construct())
|
|
||||||
|
|
||||||
def construct(self):
|
|
||||||
items = []
|
|
||||||
for note in NoteContent.get_all():
|
|
||||||
if note.group == self.group:
|
|
||||||
items.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
|
||||||
menu=note.make_menu, arg=self.readonly))
|
|
||||||
|
|
||||||
return items or [MenuItem('(none)')]
|
|
||||||
|
|
||||||
def has_notes(self):
|
|
||||||
return any(note.group == self.group for note in NoteContent.get_all())
|
|
||||||
|
|
||||||
def update_contents(self):
|
|
||||||
self.replace_items(self.construct())
|
|
||||||
|
|
||||||
|
|
||||||
class GroupPickerMenu(MenuSystem):
|
|
||||||
def __init__(self, current=''):
|
|
||||||
self.result = None
|
|
||||||
self.current = current
|
|
||||||
|
|
||||||
groups = NoteContentBase.get_groups()
|
|
||||||
chosen = 0
|
|
||||||
items = [MenuItem('(none)', f=self.picked, arg='')]
|
|
||||||
|
|
||||||
for group in groups:
|
|
||||||
if group == self.current:
|
|
||||||
chosen = len(items)
|
|
||||||
items.append(MenuItem(group, f=self.picked, arg=group))
|
|
||||||
|
|
||||||
items.append(MenuItem('New Group', f=self.new_group))
|
|
||||||
|
|
||||||
super().__init__(items, chosen=chosen)
|
|
||||||
|
|
||||||
async def picked(self, menu, idx, mi):
|
|
||||||
assert menu == self
|
|
||||||
self.result = mi.arg
|
|
||||||
the_ux.pop()
|
|
||||||
|
|
||||||
async def new_group(self, menu, idx, mi):
|
|
||||||
group = await ux_input_text('', max_len=ONE_LINE, confirm_exit=False,
|
|
||||||
prompt='Group', placeholder='(optional)')
|
|
||||||
if group is None:
|
|
||||||
self.result = None
|
|
||||||
else:
|
|
||||||
self.result = group
|
|
||||||
|
|
||||||
the_ux.pop()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def pick(cls, current=''):
|
|
||||||
m = cls(current)
|
|
||||||
the_ux.push(m)
|
|
||||||
await m.interact()
|
|
||||||
|
|
||||||
return current if m.result is None else m.result
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordContent(NoteContentBase):
|
class PasswordContent(NoteContentBase):
|
||||||
# "Passwords" have a few more fields and are more structured
|
# "Passwords" have a few more fields and are more structured
|
||||||
flds = ['title', 'user', 'password', 'site', 'misc', 'group']
|
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||||
type_label = 'password'
|
type_label = 'password'
|
||||||
|
|
||||||
async def _make_menu(self, readonly=False):
|
async def _make_menu(self, readonly=False):
|
||||||
@ -522,7 +380,7 @@ class PasswordContent(NoteContentBase):
|
|||||||
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||||
rv += [
|
rv += [
|
||||||
MenuItem('View Password', f=self.view_pw),
|
MenuItem('View Password', f=self.view_pw),
|
||||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
|
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
|
||||||
]
|
]
|
||||||
if not readonly:
|
if not readonly:
|
||||||
rv += [
|
rv += [
|
||||||
@ -537,12 +395,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
||||||
]
|
]
|
||||||
|
|
||||||
# if password is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
|
||||||
# and current seed (master or tmp) is word based - offer to apply pwd text as BIP-39 passphrase
|
|
||||||
if self.is_b39pass_applicable(self.password, readonly):
|
|
||||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
|
||||||
f=self.apply_as_b39_pass, arg=(self.password, readonly))]
|
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
async def make_menu(self, a, b, item):
|
async def make_menu(self, a, b, item):
|
||||||
@ -616,8 +468,7 @@ class PasswordContent(NoteContentBase):
|
|||||||
|
|
||||||
if self.idx == -1:
|
if self.idx == -1:
|
||||||
# prompt for password only on new records.
|
# prompt for password only on new records.
|
||||||
# can be None if CANCEL is pressed - handle, Send Password requires string
|
self.password = await get_a_password(self.password)
|
||||||
self.password = await get_a_password(self.password) or ""
|
|
||||||
|
|
||||||
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
|
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
|
||||||
prompt='Website', placeholder='(optional)')
|
prompt='Website', placeholder='(optional)')
|
||||||
@ -629,8 +480,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
if misc is None:
|
if misc is None:
|
||||||
misc = self.misc
|
misc = self.misc
|
||||||
|
|
||||||
group = await GroupPickerMenu.pick(self.group)
|
|
||||||
|
|
||||||
if self.idx != -1:
|
if self.idx != -1:
|
||||||
# confirm changes, don't for new records
|
# confirm changes, don't for new records
|
||||||
chgs = []
|
chgs = []
|
||||||
@ -642,8 +491,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
chgs.append('Username')
|
chgs.append('Username')
|
||||||
if self.misc != misc:
|
if self.misc != misc:
|
||||||
chgs.append('Other Notes')
|
chgs.append('Other Notes')
|
||||||
if self.group != group:
|
|
||||||
chgs.append('Group')
|
|
||||||
|
|
||||||
if not chgs:
|
if not chgs:
|
||||||
await ux_dramatic_pause('No changes.', 3)
|
await ux_dramatic_pause('No changes.', 3)
|
||||||
@ -657,7 +504,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
self.user = user
|
self.user = user
|
||||||
self.site = site
|
self.site = site
|
||||||
self.misc = misc
|
self.misc = misc
|
||||||
self.group = group
|
|
||||||
|
|
||||||
await self._save_ux(menu)
|
await self._save_ux(menu)
|
||||||
return self
|
return self
|
||||||
@ -665,12 +511,11 @@ class PasswordContent(NoteContentBase):
|
|||||||
|
|
||||||
class NoteContent(NoteContentBase):
|
class NoteContent(NoteContentBase):
|
||||||
# Pure "notes" have just a title and free-form text
|
# Pure "notes" have just a title and free-form text
|
||||||
flds = ['title', 'misc', 'group']
|
flds = ['title', 'misc']
|
||||||
type_label = 'note'
|
type_label = 'note'
|
||||||
|
|
||||||
async def _make_menu(self, readonly=False):
|
async def _make_menu(self, readonly=False):
|
||||||
# Details and actions for this Note
|
# Details and actions for this Note
|
||||||
|
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem('"%s"' % self.title, f=self.view),
|
MenuItem('"%s"' % self.title, f=self.view),
|
||||||
MenuItem('View Note', f=self.view),
|
MenuItem('View Note', f=self.view),
|
||||||
@ -681,19 +526,11 @@ class NoteContent(NoteContentBase):
|
|||||||
MenuItem('Delete', f=self.delete),
|
MenuItem('Delete', f=self.delete),
|
||||||
MenuItem('Export', f=self.export),
|
MenuItem('Export', f=self.export),
|
||||||
]
|
]
|
||||||
|
|
||||||
rv += [
|
rv += [
|
||||||
self.sign_misc_menu_item(),
|
self.sign_misc_menu_item(),
|
||||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
||||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# if misc is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
|
||||||
# and current seed (master or tmp) is word based - offer to apply note text as BIP-39 passphrase
|
|
||||||
if self.is_b39pass_applicable(self.misc, readonly):
|
|
||||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
|
||||||
f=self.apply_as_b39_pass, arg=(self.misc, readonly))]
|
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
async def make_menu(self, a, b, item):
|
async def make_menu(self, a, b, item):
|
||||||
@ -720,8 +557,6 @@ class NoteContent(NoteContentBase):
|
|||||||
if misc is None:
|
if misc is None:
|
||||||
misc = self.misc
|
misc = self.misc
|
||||||
|
|
||||||
group = await GroupPickerMenu.pick(self.group)
|
|
||||||
|
|
||||||
if self.idx != -1:
|
if self.idx != -1:
|
||||||
# confirm changes, don't for new records
|
# confirm changes, don't for new records
|
||||||
chgs = []
|
chgs = []
|
||||||
@ -729,8 +564,6 @@ class NoteContent(NoteContentBase):
|
|||||||
chgs.append('Title')
|
chgs.append('Title')
|
||||||
if self.misc != misc:
|
if self.misc != misc:
|
||||||
chgs.append('Note Text')
|
chgs.append('Note Text')
|
||||||
if self.group != group:
|
|
||||||
chgs.append('Group')
|
|
||||||
|
|
||||||
if not chgs:
|
if not chgs:
|
||||||
await ux_dramatic_pause('No changes.', 3)
|
await ux_dramatic_pause('No changes.', 3)
|
||||||
@ -743,7 +576,6 @@ class NoteContent(NoteContentBase):
|
|||||||
|
|
||||||
self.title = title
|
self.title = title
|
||||||
self.misc = misc
|
self.misc = misc
|
||||||
self.group = group
|
|
||||||
|
|
||||||
await self._save_ux(menu)
|
await self._save_ux(menu)
|
||||||
|
|
||||||
@ -832,12 +664,11 @@ async def import_from_other(menu, *a):
|
|||||||
records = json.load(open(fn, 'rt'))
|
records = json.load(open(fn, 'rt'))
|
||||||
|
|
||||||
# We have some JSON, parsed now.
|
# We have some JSON, parsed now.
|
||||||
ok = await import_from_json(records)
|
await import_from_json(records)
|
||||||
if not ok: return
|
|
||||||
|
|
||||||
await ux_dramatic_pause('Saved.', 3)
|
await ux_dramatic_pause('Saved.', 3)
|
||||||
menu.update_contents()
|
menu.update_contents()
|
||||||
|
|
||||||
async def import_from_json(records):
|
async def import_from_json(records):
|
||||||
# should dedup, but we aren't
|
# should dedup, but we aren't
|
||||||
try:
|
try:
|
||||||
@ -852,7 +683,6 @@ async def import_from_json(records):
|
|||||||
settings.set('notes', was)
|
settings.set('notes', was)
|
||||||
settings.set('secnap', True)
|
settings.set('secnap', True)
|
||||||
settings.save()
|
settings.save()
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
|
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
|
||||||
|
|||||||
@ -32,7 +32,8 @@ from utils import call_later_ms
|
|||||||
# batt_to = (when on battery only) idle timeout period
|
# batt_to = (when on battery only) idle timeout period
|
||||||
# _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) [before removal of MultisigWallet]
|
||||||
|
# miniscript = list of defined miniscript wallets, including multisig (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
|
||||||
@ -63,10 +64,9 @@ from utils import call_later_ms
|
|||||||
# b85max = (bool) allow max BIP-32 int value in BIP-85 derivations
|
# b85max = (bool) allow max BIP-32 int value in BIP-85 derivations
|
||||||
# ptxurl = (str) URL for PushTx feature, clear to disable feature
|
# ptxurl = (str) URL for PushTx feature, clear to disable feature
|
||||||
# hmx = (bool) Force display of current XFP in home menu, even w/o tmp seed active
|
# hmx = (bool) Force display of current XFP in home menu, even w/o tmp seed active
|
||||||
# unsort_ms = (bool) Allow unsorted multisig with BIP-67 disabled
|
|
||||||
# msas = multisig address show (do not censor multisig addresses)
|
|
||||||
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
||||||
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
||||||
|
# aemscsv = (bool) opt-in enable more verbose CSV output for miniscript wallets with Derivations and Scripts
|
||||||
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
|
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
|
||||||
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
|
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
|
||||||
# wifs = (list) List of tuples (public/private key)
|
# wifs = (list) List of tuples (public/private key)
|
||||||
@ -83,7 +83,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 = ["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
|
||||||
@ -91,7 +91,7 @@ from utils import call_later_ms
|
|||||||
# keep these settings only if unspecified on the other end
|
# keep these settings only if unspecified on the other end
|
||||||
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
|
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
|
||||||
"axskip", "del", "pms", "idle_to", "batt_to",
|
"axskip", "del", "pms", "idle_to", "batt_to",
|
||||||
"bright", "msas"]
|
"bright"]
|
||||||
|
|
||||||
# key value pairs saved directly to master seed settings
|
# key value pairs saved directly to master seed settings
|
||||||
# held in RAM for tmp seed sessions
|
# held in RAM for tmp seed sessions
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -6,9 +6,8 @@ import os, chains, ngu, struct, version
|
|||||||
from glob import settings
|
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 ubinascii import unhexlify as a2b_hex
|
|
||||||
from exceptions import UnknownAddressExplained
|
from exceptions import UnknownAddressExplained
|
||||||
from utils import problem_file_line, show_single_address, validate_own_address
|
from utils import problem_file_line, show_single_address
|
||||||
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
|
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
|
||||||
|
|
||||||
# Track many addresses, but in compressed form
|
# Track many addresses, but in compressed form
|
||||||
@ -51,7 +50,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 +157,7 @@ class AddressCacheFile:
|
|||||||
self.setup(self.change_idx, start_idx)
|
self.setup(self.change_idx, start_idx)
|
||||||
|
|
||||||
bonus = None
|
bonus = None
|
||||||
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
|
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count, self.change_idx):
|
||||||
change_idx=self.change_idx):
|
|
||||||
self.append(here)
|
self.append(here)
|
||||||
self.count += 1
|
self.count += 1
|
||||||
|
|
||||||
@ -217,7 +215,8 @@ class OwnershipCache:
|
|||||||
def filter(cls, addr_fmt, args):
|
def filter(cls, addr_fmt, args):
|
||||||
# Filter possible candidates!
|
# Filter possible candidates!
|
||||||
# - if you start w/ testnet, we'll follow that
|
# - if you start w/ testnet, we'll follow that
|
||||||
from multisig import MultisigWallet
|
from wallet import MiniScriptWallet
|
||||||
|
from glob import dis
|
||||||
|
|
||||||
args = args or {}
|
args = args or {}
|
||||||
|
|
||||||
@ -225,7 +224,7 @@ class OwnershipCache:
|
|||||||
named_wal = args.get("wallet", None)
|
named_wal = args.get("wallet", None)
|
||||||
if named_wal:
|
if named_wal:
|
||||||
# quick search without deserialization
|
# quick search without deserialization
|
||||||
res = list(MultisigWallet.iter_wallets(name=named_wal))
|
res = list(MiniScriptWallet.iter_wallets(name=named_wal))
|
||||||
if not res:
|
if not res:
|
||||||
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
|
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
|
||||||
|
|
||||||
@ -233,6 +232,8 @@ class OwnershipCache:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
possibles = []
|
possibles = []
|
||||||
|
if addr_fmt == AF_P2TR:
|
||||||
|
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
|
||||||
afs = [addr_fmt]
|
afs = [addr_fmt]
|
||||||
@ -246,7 +247,7 @@ class OwnershipCache:
|
|||||||
# 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
|
||||||
|
|
||||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
|
possibles.extend(MiniScriptWallet.iter_wallets(addr_fmts=afs))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Construct possible single-signer wallets, always at least account=0 case
|
# Construct possible single-signer wallets, always at least account=0 case
|
||||||
@ -266,7 +267,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.")
|
||||||
|
|
||||||
# ordering here
|
# ordering here
|
||||||
return possibles
|
return possibles
|
||||||
@ -304,10 +305,11 @@ class OwnershipCache:
|
|||||||
|
|
||||||
dis.fullscreen("Wait...")
|
dis.fullscreen("Wait...")
|
||||||
|
|
||||||
try:
|
ch = chains.current_chain()
|
||||||
addr, addr_fmt = validate_own_address(addr)
|
addr_fmt = ch.possible_address_fmt(addr)
|
||||||
except Exception as e:
|
if not addr_fmt:
|
||||||
raise UnknownAddressExplained('That address is not valid on ' + e.args[0])
|
# might be valid address over on testnet vs mainnet
|
||||||
|
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
|
||||||
|
|
||||||
matches = OWNERSHIP.filter(addr_fmt, args)
|
matches = OWNERSHIP.filter(addr_fmt, args)
|
||||||
|
|
||||||
@ -338,11 +340,11 @@ class OwnershipCache:
|
|||||||
|
|
||||||
# nothing found among singlesig & registered multisig wallets
|
# nothing found among singlesig & registered multisig wallets
|
||||||
# check WIF store (single sig only)
|
# check WIF store (single sig only)
|
||||||
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
|
if addr_fmt not in [AF_P2WSH]:
|
||||||
dis.fullscreen("WIF Store...")
|
dis.fullscreen("WIF Store...")
|
||||||
from wif import iter_wif_store_addresses
|
from wif import iter_wif_store_addresses
|
||||||
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
|
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
|
||||||
for i, store_addr in iter_wif_store_addresses(target_af):
|
for i, store_addr in iter_wif_store_addresses(ch, target_af):
|
||||||
if store_addr == addr:
|
if store_addr == addr:
|
||||||
return False, ("wif", target_af), i+1
|
return False, ("wif", target_af), i+1
|
||||||
|
|
||||||
@ -354,23 +356,30 @@ class OwnershipCache:
|
|||||||
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
||||||
from ux import ux_show_story, show_qr_code
|
from ux import ux_show_story, show_qr_code
|
||||||
from charcodes import KEY_QR
|
from charcodes import KEY_QR
|
||||||
from multisig import MultisigWallet
|
from wallet import MiniScriptWallet
|
||||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, wallet, subpath = cls.search(addr, args)
|
_, wallet, subpath = cls.search(addr, args)
|
||||||
is_ms = isinstance(wallet, MultisigWallet)
|
is_complex = isinstance(wallet, MiniScriptWallet)
|
||||||
|
|
||||||
msg = show_single_address(addr)
|
msg = show_single_address(addr)
|
||||||
esc = ""
|
esc = ""
|
||||||
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
|
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
|
||||||
msg += '\n\nFound in WIF store at index %d' % subpath
|
msg += '\n\nFound in WIF store at index %d' % subpath
|
||||||
addr_fmt = wallet[1]
|
addr_fmt = wallet[1]
|
||||||
else:
|
else:
|
||||||
sp = wallet.render_path(*subpath)
|
msg += '\n\nFound in wallet:\n' + wallet.name
|
||||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
msg += '\n\nDerivation path:\n'
|
||||||
msg += '\nDerivation path:\n ' + sp
|
|
||||||
addr_fmt = wallet.addr_fmt
|
addr_fmt = wallet.addr_fmt
|
||||||
if not is_ms:
|
if hasattr(wallet, "render_path"):
|
||||||
|
sp = wallet.render_path(*subpath)
|
||||||
|
msg += sp
|
||||||
|
else:
|
||||||
|
sp = None
|
||||||
|
msg += ".../%d/%d" % subpath
|
||||||
|
|
||||||
|
if not is_complex:
|
||||||
esc = "0"
|
esc = "0"
|
||||||
msg += "\n\nPress (0) to sign message with this key."
|
msg += "\n\nPress (0) to sign message with this key."
|
||||||
|
|
||||||
@ -386,9 +395,12 @@ class OwnershipCache:
|
|||||||
while 1:
|
while 1:
|
||||||
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
|
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
|
||||||
if ch in ("1"+KEY_QR):
|
if ch in ("1"+KEY_QR):
|
||||||
await show_qr_code(addr, msg=addr, is_addrs=True,
|
await show_qr_code(
|
||||||
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
|
addr,
|
||||||
elif not is_ms and (ch == "0"): # only singlesig
|
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
||||||
|
msg=addr, is_addrs=True
|
||||||
|
)
|
||||||
|
elif not is_complex and (ch == "0"): # only singlesig
|
||||||
from msgsign import sign_with_own_address
|
from msgsign import sign_with_own_address
|
||||||
await sign_with_own_address(sp, addr_fmt)
|
await sign_with_own_address(sp, addr_fmt)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
#
|
#
|
||||||
# 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, problem_file_line
|
||||||
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
|
||||||
@ -29,10 +29,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 +47,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 +64,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)
|
||||||
@ -104,12 +106,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 +170,10 @@ 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:
|
||||||
|
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
|
||||||
@ -178,14 +186,14 @@ class PaperWalletMaker:
|
|||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from utils import problem_file_line
|
|
||||||
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
|
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
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 +222,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')
|
||||||
|
|||||||
@ -410,9 +410,13 @@ class PinAttempt:
|
|||||||
# Main secret has changed: reset the settings+their key,
|
# Main secret has changed: reset the settings+their key,
|
||||||
# and capture xfp/xpub
|
# and capture xfp/xpub
|
||||||
# if None is provided as raw_secret -> restore to main seed
|
# if None is provided as raw_secret -> restore to main seed
|
||||||
|
import glob
|
||||||
from glob import settings, dis
|
from glob import settings, dis
|
||||||
stash.SensitiveValues.clear_cache()
|
stash.SensitiveValues.clear_cache()
|
||||||
|
|
||||||
|
# invalidate descriptor cache - upon new secret load
|
||||||
|
glob.DESC_CACHE.clear()
|
||||||
|
|
||||||
bypass_tmp = False
|
bypass_tmp = False
|
||||||
stash.bip39_passphrase = bool(bip39pw)
|
stash.bip39_passphrase = bool(bip39pw)
|
||||||
|
|
||||||
|
|||||||
12
shared/precomp_tag_hash.py
Normal file
12
shared/precomp_tag_hash.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||||
|
#
|
||||||
|
# taproot precomputed tag hashes
|
||||||
|
#
|
||||||
|
# SHA256(TapLeaf)
|
||||||
|
TAP_LEAF_H = b'\xae\xea\x8f\xdcB\x08\x981\x05sKX\x08\x1d\x1e&8\xd3_\x1c\xb5@\x08\xd4\xd3W\xca\x03\xbex\xe9\xee'
|
||||||
|
# SHA256(TapBranch)
|
||||||
|
TAP_BRANCH_H = b'\x19A\xa1\xf2\xe5n\xb9_\xa2\xa9\xf1\x94\xbe\\\x01\xf7!o3\xed\x82\xb0\x91F4\x90\xd0[\xf5\x16\xa0\x15'
|
||||||
|
# SHA256(TapTweak)
|
||||||
|
TAP_TWEAK_H = b'\xe8\x0f\xe1c\x9c\x9c\xa0P\xe3\xaf\x1b9\xc1C\xc6>B\x9c\xbc\xeb\x15\xd9@\xfb\xb5\xc5\xa1\xf4\xafW\xc5\xe9'
|
||||||
|
# SHA256(TapSighash)
|
||||||
|
TAP_SIGHASH_H = b'\xf4\nH\xdfK*p\xc8\xb4\x92K\xf2eFa\xed=\x95\xfdf\xa3\x13\xeb\x87#u\x97\xc6(\xe4\xa01'
|
||||||
3192
shared/psbt.py
3192
shared/psbt.py
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,10 @@ class PSRAMWrapper:
|
|||||||
|
|
||||||
return memoryview(self._wr)[offset:offset+ln]
|
return memoryview(self._wr)[offset:offset+ln]
|
||||||
|
|
||||||
|
def is_at(self, ptr, offset):
|
||||||
|
# is bytes() object really one we created at read_at
|
||||||
|
return uctypes.addressof(ptr) == self.base+offset
|
||||||
|
|
||||||
# Be compatible with SPIFlash class...
|
# Be compatible with SPIFlash class...
|
||||||
|
|
||||||
def read(self, address, buf, cmd=None):
|
def read(self, address, buf, cmd=None):
|
||||||
|
|||||||
@ -57,8 +57,9 @@ SLOW_BAUD = const(9600)
|
|||||||
FAST_BAUD = const(57600)
|
FAST_BAUD = const(57600)
|
||||||
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
|
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
|
||||||
|
|
||||||
# TODO: constructor should avoid full setup until after login; after setup,
|
# TODO: constructor should leave it in reset for simple lower-power usage; then after
|
||||||
# command sleep is the known low-power state.
|
# login we can do full setup (2+ seconds) and then sleep again until needed.
|
||||||
|
|
||||||
class QRScanner:
|
class QRScanner:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -67,8 +68,6 @@ class QRScanner:
|
|||||||
self.scan_light = False # is light on during scanning?
|
self.scan_light = False # is light on during scanning?
|
||||||
self.version = None
|
self.version = None
|
||||||
self.setup_done = False
|
self.setup_done = False
|
||||||
self.needs_reinit = False
|
|
||||||
self.sleep_seq = 0
|
|
||||||
|
|
||||||
# hodl this lock when communicating w/ QR scanner
|
# hodl this lock when communicating w/ QR scanner
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
@ -85,21 +84,16 @@ class QRScanner:
|
|||||||
# setup hardware, reset scanner and return time to delay until ready
|
# setup hardware, reset scanner and return time to delay until ready
|
||||||
from machine import UART, Pin
|
from machine import UART, Pin
|
||||||
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
|
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
|
||||||
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=1)
|
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=0)
|
||||||
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
|
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
|
||||||
|
|
||||||
self.pulse_reset()
|
# NOTE: reset is active low (open drain)
|
||||||
|
|
||||||
# needs full 2 seconds of recovery time after reset
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def pulse_reset(self):
|
|
||||||
# RESET is active low (open drain). Keep it as a pulse; module docs
|
|
||||||
# describe low on this pin as wake-up, so don't use it as parking state.
|
|
||||||
self.reset(0)
|
self.reset(0)
|
||||||
utime.sleep_ms(10)
|
utime.sleep_ms(10)
|
||||||
self.reset(1)
|
self.reset(1)
|
||||||
self.needs_reinit = False
|
|
||||||
|
# needs full 2 seconds of recovery time
|
||||||
|
return 2
|
||||||
|
|
||||||
def set_baud(self, br):
|
def set_baud(self, br):
|
||||||
# change serial port baud rate
|
# change serial port baud rate
|
||||||
@ -124,104 +118,56 @@ class QRScanner:
|
|||||||
|
|
||||||
async def setup_task(self, start_delay):
|
async def setup_task(self, start_delay):
|
||||||
# Task to setup device, and then die.
|
# Task to setup device, and then die.
|
||||||
async with self.lock:
|
await asyncio.sleep(start_delay)
|
||||||
for attempt in range(3):
|
|
||||||
await asyncio.sleep(start_delay)
|
|
||||||
|
|
||||||
try:
|
async with self.lock:
|
||||||
await self._configure()
|
|
||||||
except Exception:
|
|
||||||
# a step failed or timed out (would have left scanner dead
|
|
||||||
# until next boot); reset module and start over
|
|
||||||
await self.blind_shutdown()
|
|
||||||
if attempt == 2:
|
|
||||||
break
|
|
||||||
start_delay = self.reset_stream()
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.setup_done = True
|
# might need to repeat a few time to get into right state
|
||||||
await self.goto_sleep()
|
|
||||||
return
|
|
||||||
self.mark_needs_reinit()
|
|
||||||
|
|
||||||
def reset_stream(self):
|
|
||||||
self.sleep_seq += 1
|
|
||||||
start_delay = self.hardware_setup()
|
|
||||||
self.stream = asyncio.StreamReader(self.serial, {})
|
|
||||||
return start_delay
|
|
||||||
|
|
||||||
def mark_needs_reinit(self):
|
|
||||||
self.setup_done = False
|
|
||||||
self.version = None
|
|
||||||
self.needs_reinit = True
|
|
||||||
if hasattr(self, 'reset'):
|
|
||||||
self.reset(1)
|
|
||||||
|
|
||||||
async def blind_shutdown(self):
|
|
||||||
for baud in (SLOW_BAUD, FAST_BAUD):
|
|
||||||
self.set_baud(baud)
|
|
||||||
await self.tx('S_CMD_020D') # return to "Command mode"
|
|
||||||
await asyncio.sleep_ms(20)
|
|
||||||
await self.tx('S_CMD_03L0') # turn off bright light
|
|
||||||
await asyncio.sleep_ms(20)
|
|
||||||
await self.tx('SRDF0050') # sleep scanner
|
|
||||||
await asyncio.sleep_ms(150)
|
|
||||||
await self.tx('SRDF0050')
|
|
||||||
await asyncio.sleep_ms(20)
|
|
||||||
|
|
||||||
async def _configure(self):
|
|
||||||
# full config sequence; any step may raise on timeout/framing error
|
|
||||||
|
|
||||||
# might need to repeat a few time to get into right state
|
|
||||||
for retry in range(5):
|
|
||||||
baud = await self.probe_baud()
|
|
||||||
if baud: break
|
|
||||||
else:
|
|
||||||
#print("QR Scanner: missing")
|
|
||||||
raise RuntimeError('no contact')
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
|
||||||
except RuntimeError:
|
|
||||||
await asyncio.sleep_ms(1000)
|
|
||||||
for retry in range(5):
|
for retry in range(5):
|
||||||
baud = await self.probe_baud()
|
baud = await self.probe_baud()
|
||||||
if baud: break
|
if baud: break
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('no contact after S_CMD_FFFF')
|
#print("QR Scanner: missing")
|
||||||
|
return
|
||||||
|
|
||||||
# go to high speed!
|
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
||||||
if baud != FAST_BAUD:
|
|
||||||
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
|
||||||
self.set_baud(FAST_BAUD)
|
|
||||||
|
|
||||||
# configure it like we want it
|
# go to high speed!
|
||||||
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
if baud != FAST_BAUD:
|
||||||
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
||||||
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
self.set_baud(FAST_BAUD)
|
||||||
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
|
|
||||||
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
|
|
||||||
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
|
|
||||||
await self.txrx('S_CMD_03L0') # light off all the time by default
|
|
||||||
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
|
|
||||||
|
|
||||||
# settings under continuous scan mode
|
# configure it like we want it
|
||||||
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
||||||
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
||||||
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
||||||
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
|
||||||
|
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
|
||||||
|
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
|
||||||
|
await self.txrx('S_CMD_03L0') # light off all the time by default
|
||||||
|
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
|
||||||
|
|
||||||
# these aren't useful (yet?) and just make things harder to decode.
|
# settings under continuous scan mode
|
||||||
#await self.txrx('S_CMD_05F1') # add all information on
|
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
||||||
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
||||||
#await self.txrx('S_CMD_05S1') # STX start char
|
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
||||||
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
||||||
#await self.txrx('S_CMD_0501') # prefix on
|
|
||||||
#await self.txrx('S_CMD_0506') # suffix
|
|
||||||
#await self.txrx('S_CMD_05D0') # tx total data
|
|
||||||
|
|
||||||
# prevent scanning magic QR to affect settings
|
# these aren't useful (yet?) and just make things harder to decode.
|
||||||
await self.txrx('S_CMD_0000') # close setting codes
|
#await self.txrx('S_CMD_05F1') # add all information on
|
||||||
|
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
||||||
|
#await self.txrx('S_CMD_05S1') # STX start char
|
||||||
|
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
||||||
|
#await self.txrx('S_CMD_0501') # prefix on
|
||||||
|
#await self.txrx('S_CMD_0506') # suffix
|
||||||
|
#await self.txrx('S_CMD_05D0') # tx total data
|
||||||
|
|
||||||
|
# prevent scanning magic QR to affect settings
|
||||||
|
await self.txrx('S_CMD_0000') # close setting codes
|
||||||
|
|
||||||
|
self.setup_done = True
|
||||||
|
|
||||||
|
await self.goto_sleep()
|
||||||
|
|
||||||
async def scan_once(self):
|
async def scan_once(self):
|
||||||
# Blocks until something is scanned. Returns it as string
|
# Blocks until something is scanned. Returns it as string
|
||||||
@ -230,16 +176,6 @@ class QRScanner:
|
|||||||
# - returns a BBQr object at that point
|
# - returns a BBQr object at that point
|
||||||
self.scan_light = False
|
self.scan_light = False
|
||||||
|
|
||||||
if self.needs_reinit:
|
|
||||||
try:
|
|
||||||
await self.setup_task(self.reset_stream())
|
|
||||||
if self.setup_done:
|
|
||||||
await asyncio.sleep_ms(200)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
await self.blind_shutdown()
|
|
||||||
self.mark_needs_reinit()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# wait for reset process to complete (can be an issue right after boot)
|
# wait for reset process to complete (can be an issue right after boot)
|
||||||
# - few seconds of boot time needed
|
# - few seconds of boot time needed
|
||||||
for retry in range(10):
|
for retry in range(10):
|
||||||
@ -275,22 +211,19 @@ class QRScanner:
|
|||||||
finally:
|
finally:
|
||||||
# Problem: another valid scan can come in just as we are trying
|
# Problem: another valid scan can come in just as we are trying
|
||||||
# to get out of scanner mode
|
# to get out of scanner mode
|
||||||
for retry in range(3):
|
for retry in range(10):
|
||||||
try:
|
try:
|
||||||
await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
|
await self.txrx('S_CMD_020D') # return to "Command mode"
|
||||||
await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
|
await self.txrx('S_CMD_03L0') # turn off bright light
|
||||||
#print('rest after %d retries' % retry)
|
#print('rest after %d retries' % retry)
|
||||||
break
|
break
|
||||||
except Exception:
|
except: pass
|
||||||
pass
|
await asyncio.sleep_ms(25)
|
||||||
await asyncio.sleep_ms(50)
|
|
||||||
else:
|
else:
|
||||||
|
pass
|
||||||
#print('reset failed')
|
#print('reset failed')
|
||||||
await self.blind_shutdown()
|
|
||||||
self.mark_needs_reinit()
|
|
||||||
|
|
||||||
if self.setup_done:
|
await self.goto_sleep()
|
||||||
await self.goto_sleep()
|
|
||||||
self.busy_scanning = False
|
self.busy_scanning = False
|
||||||
|
|
||||||
# return BBQr object or string if simple QR
|
# return BBQr object or string if simple QR
|
||||||
@ -321,14 +254,13 @@ class QRScanner:
|
|||||||
# send specific command until it responds
|
# send specific command until it responds
|
||||||
# - it will wake on any command, but not instant
|
# - it will wake on any command, but not instant
|
||||||
# - first one seems to fail 100%
|
# - first one seems to fail 100%
|
||||||
self.sleep_seq += 1
|
|
||||||
await self.tx('SRDF0051') # blindly at first
|
await self.tx('SRDF0051') # blindly at first
|
||||||
|
|
||||||
for retry in range(5):
|
for retry in range(5):
|
||||||
try:
|
try:
|
||||||
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
|
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
|
||||||
return
|
return
|
||||||
except Exception:
|
except:
|
||||||
# first try usually fails, that's okay... its asleep and groggy
|
# first try usually fails, that's okay... its asleep and groggy
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -338,13 +270,9 @@ class QRScanner:
|
|||||||
# - need blind retries here
|
# - need blind retries here
|
||||||
# - might be two layers of sleep, and we need this second command after the first
|
# - might be two layers of sleep, and we need this second command after the first
|
||||||
# - helps to turn off the yellow LED, and save power as well
|
# - helps to turn off the yellow LED, and save power as well
|
||||||
self.sleep_seq += 1
|
|
||||||
sleep_seq = self.sleep_seq
|
|
||||||
await self.tx('SRDF0050')
|
await self.tx('SRDF0050')
|
||||||
async def later():
|
async def later():
|
||||||
await asyncio.sleep_ms(150)
|
await asyncio.sleep_ms(150)
|
||||||
if sleep_seq != self.sleep_seq or self.busy_scanning:
|
|
||||||
return
|
|
||||||
await self.tx('SRDF0050')
|
await self.tx('SRDF0050')
|
||||||
asyncio.create_task(later())
|
asyncio.create_task(later())
|
||||||
|
|
||||||
@ -362,22 +290,6 @@ class QRScanner:
|
|||||||
#print('tx >> ' + msg)
|
#print('tx >> ' + msg)
|
||||||
self.serial.write(msg)
|
self.serial.write(msg)
|
||||||
|
|
||||||
async def readexactly_timeout(self, num, timeout, msg=None):
|
|
||||||
# Avoid asyncio.wait_for_ms here: it can leave the scanner setup task
|
|
||||||
# stuck after a CancelledError. Convert scanner silence into a normal
|
|
||||||
# retryable command failure instead.
|
|
||||||
if timeout is None:
|
|
||||||
return await self.stream.readexactly(num)
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while self.stream.s.any() < num:
|
|
||||||
if utime.ticks_diff(utime.ticks_ms(), start) >= timeout:
|
|
||||||
#print("no rx after %s" % msg)
|
|
||||||
raise RuntimeError
|
|
||||||
await asyncio.sleep_ms(5)
|
|
||||||
|
|
||||||
return await self.stream.readexactly(num)
|
|
||||||
|
|
||||||
async def txrx(self, msg, timeout=250):
|
async def txrx(self, msg, timeout=250):
|
||||||
# Send a command, get the corresponding response.
|
# Send a command, get the corresponding response.
|
||||||
# - has a long timeout, collects rx based on framing
|
# - has a long timeout, collects rx based on framing
|
||||||
@ -398,8 +310,13 @@ class QRScanner:
|
|||||||
expect = LEN_OKAY
|
expect = LEN_OKAY
|
||||||
rx = b''
|
rx = b''
|
||||||
while 1:
|
while 1:
|
||||||
rx += await self.readexactly_timeout(expect, timeout, msg)
|
try:
|
||||||
|
rx += await asyncio.wait_for_ms(self.stream.readexactly(expect), timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
if timeout is None:
|
||||||
|
continue
|
||||||
|
#print("no rx after %s" % msg)
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
#print('txrx << ' + B2A(rx))
|
#print('txrx << ' + B2A(rx))
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
# - 'abandon' * 17 + 'agent'
|
# - 'abandon' * 17 + 'agent'
|
||||||
# - 'abandon' * 11 + 'about'
|
# - 'abandon' * 11 + 'about'
|
||||||
#
|
#
|
||||||
import ngu, uctypes, bip39, random, version
|
import ngu, uctypes, bip39, random, version, ure, chains
|
||||||
from ucollections import OrderedDict
|
from ucollections import OrderedDict
|
||||||
from menu import MenuItem, MenuSystem
|
from menu import MenuItem, MenuSystem
|
||||||
from utils import xfp2str, parse_extended_key, swab32
|
from utils import xfp2str, swab32
|
||||||
from utils import deserialize_secret, problem_file_line, wipe_if_deltamode
|
from utils import deserialize_secret, problem_file_line, wipe_if_deltamode
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, OK, X
|
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, OK, X
|
||||||
@ -33,9 +33,6 @@ from ucollections import namedtuple
|
|||||||
# seed words lengths we support: 24=>256 bits, and recommended
|
# seed words lengths we support: 24=>256 bits, and recommended
|
||||||
VALID_LENGTHS = (24, 18, 12)
|
VALID_LENGTHS = (24, 18, 12)
|
||||||
|
|
||||||
# maximum length for BIP-39 passphrase
|
|
||||||
MAX_PASS_LEN = 100
|
|
||||||
|
|
||||||
# bit flag that means "also include bare prefix as a valid word"
|
# bit flag that means "also include bare prefix as a valid word"
|
||||||
_PREFIX_MARKER = const(1<<26)
|
_PREFIX_MARKER = const(1<<26)
|
||||||
|
|
||||||
@ -52,7 +49,7 @@ def seed_vault_iter():
|
|||||||
# raw vault entries are list type when json.loaded from flash
|
# raw vault entries are list type when json.loaded from flash
|
||||||
for lst in settings.master_get("seeds", []):
|
for lst in settings.master_get("seeds", []):
|
||||||
yield VaultEntry(*lst)
|
yield VaultEntry(*lst)
|
||||||
|
|
||||||
def letter_choices(sofar='', depth=0, thres=5):
|
def letter_choices(sofar='', depth=0, thres=5):
|
||||||
# make a list of word completions based on indicated prefix
|
# make a list of word completions based on indicated prefix
|
||||||
if not sofar:
|
if not sofar:
|
||||||
@ -713,9 +710,17 @@ def seed_words_to_encoded_secret(words):
|
|||||||
return nv
|
return nv
|
||||||
|
|
||||||
def xprv_to_encoded_secret(xprv):
|
def xprv_to_encoded_secret(xprv):
|
||||||
node, chain, _ = parse_extended_key(xprv, private=True)
|
# read an xprv/tprv/etc and return BIP-32 node and what chain it's on.
|
||||||
if node is None:
|
# - can handle any garbage line
|
||||||
raise ValueError("Failed to parse extended private key.")
|
# - returns (node, chain)
|
||||||
|
# - people are using SLIP132 so we need this
|
||||||
|
ln = xprv.strip()
|
||||||
|
pat = ure.compile('.prv[A-Za-z0-9]+')
|
||||||
|
found = pat.search(ln)
|
||||||
|
assert found, "not extended privkey"
|
||||||
|
# serialize, and note version code
|
||||||
|
node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
|
||||||
|
assert node, "wrong extended privkey"
|
||||||
nv = SecretStash.encode(xprv=node)
|
nv = SecretStash.encode(xprv=node)
|
||||||
node.blank()
|
node.blank()
|
||||||
return nv, chain # need to know chain
|
return nv, chain # need to know chain
|
||||||
@ -1169,7 +1174,7 @@ class EphemeralSeedMenu(MenuSystem):
|
|||||||
from actions import nfc_recv_ephemeral, import_xprv
|
from actions import nfc_recv_ephemeral, import_xprv
|
||||||
from actions import restore_backup, scan_any_qr
|
from actions import restore_backup, scan_any_qr
|
||||||
from tapsigner import import_tapsigner_backup_file
|
from tapsigner import import_tapsigner_backup_file
|
||||||
from xor_seed import xor_restore_temporary
|
from xor_seed import xor_restore_start
|
||||||
from charcodes import KEY_QR
|
from charcodes import KEY_QR
|
||||||
|
|
||||||
import_ephemeral_menu = [
|
import_ephemeral_menu = [
|
||||||
@ -1193,7 +1198,7 @@ class EphemeralSeedMenu(MenuSystem):
|
|||||||
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
|
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
|
||||||
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
|
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
|
||||||
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
|
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
|
||||||
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
|
MenuItem("Restore Seed XOR", f=xor_restore_start),
|
||||||
]
|
]
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
@ -1238,10 +1243,10 @@ the passphrase as well, it's okay to put them together.) There is no way for \
|
|||||||
the Coldcard to know if your entry is correct, and if you have it wrong, \
|
the Coldcard to know if your entry is correct, and if you have it wrong, \
|
||||||
you will be looking at an empty wallet.
|
you will be looking at an empty wallet.
|
||||||
|
|
||||||
Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
||||||
|
|
||||||
%s to continue or press (2) to hide this message forever.
|
%s to continue or press (2) to hide this message forever.
|
||||||
''' % (howto if not version.has_qwerty else '', MAX_PASS_LEN, OK)
|
''' % (howto if not version.has_qwerty else '', OK)
|
||||||
|
|
||||||
ch = await ux_show_story(msg, escape='2')
|
ch = await ux_show_story(msg, escape='2')
|
||||||
if ch == '2':
|
if ch == '2':
|
||||||
@ -1251,8 +1256,8 @@ Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
|||||||
|
|
||||||
if version.has_qwerty and not PassphraseSaver.has_file():
|
if version.has_qwerty and not PassphraseSaver.has_file():
|
||||||
# no need for any menus if Q and no card present
|
# no need for any menus if Q and no card present
|
||||||
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
|
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase",
|
||||||
scan_ok=True, max_len=MAX_PASS_LEN)
|
b39_complete=True, scan_ok=True, max_len=100)
|
||||||
if not pp: return
|
if not pp: return
|
||||||
|
|
||||||
await apply_pass_value(pp)
|
await apply_pass_value(pp)
|
||||||
@ -1262,7 +1267,7 @@ Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
|||||||
|
|
||||||
|
|
||||||
class PassphraseMenu(MenuSystem):
|
class PassphraseMenu(MenuSystem):
|
||||||
# Collect up to MAX_PASS_LEN chars as a BIP-39 passphrase
|
# Collect up to 100 chars as a BIP-39 passphrase
|
||||||
|
|
||||||
# singleton (cls level) vars
|
# singleton (cls level) vars
|
||||||
done_cb = None
|
done_cb = None
|
||||||
@ -1351,7 +1356,7 @@ class PassphraseMenu(MenuSystem):
|
|||||||
async def view_edit_phrase(cls, *a):
|
async def view_edit_phrase(cls, *a):
|
||||||
# let them control each character
|
# let them control each character
|
||||||
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
|
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
|
||||||
b39_complete=True, scan_ok=True, max_len=MAX_PASS_LEN)
|
b39_complete=True, scan_ok=True, max_len=100)
|
||||||
if pw is not None:
|
if pw is not None:
|
||||||
cls.pp_sofar = pw
|
cls.pp_sofar = pw
|
||||||
cls.check_length()
|
cls.check_length()
|
||||||
@ -1362,8 +1367,8 @@ class PassphraseMenu(MenuSystem):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_length(cls):
|
def check_length(cls):
|
||||||
# enforce a limit of MAX_PASS_LEN chars
|
# enforce a limit of 100 chars
|
||||||
cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
|
cls.pp_sofar = cls.pp_sofar[0:100]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def add_text(cls, _1, _2, item):
|
async def add_text(cls, _1, _2, item):
|
||||||
|
|||||||
@ -19,7 +19,7 @@ from ubinascii import hexlify as b2a_hex
|
|||||||
import ustruct as struct
|
import ustruct as struct
|
||||||
import ngu
|
import ngu
|
||||||
from opcodes import *
|
from opcodes import *
|
||||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_P2TR, AF_BARE_PK
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_BARE_PK, AF_P2TR
|
||||||
|
|
||||||
# single-shot hash functions
|
# single-shot hash functions
|
||||||
sha256 = ngu.hash.sha256s
|
sha256 = ngu.hash.sha256s
|
||||||
@ -27,9 +27,7 @@ ripemd160 = ngu.hash.ripemd160
|
|||||||
hash256 = ngu.hash.sha256d
|
hash256 = ngu.hash.sha256d
|
||||||
hash160 = ngu.hash.hash160
|
hash160 = ngu.hash.hash160
|
||||||
|
|
||||||
#def bytes_to_hex_str(s):
|
SIGHASH_DEFAULT = const(0) # in taproot meaning same as SIGHASH_ALL (over whole TX)
|
||||||
# return str(b2a_hex(s), 'ascii')
|
|
||||||
|
|
||||||
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 +35,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,17 +55,23 @@ 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]
|
||||||
assert nit >= 253
|
assert nit >= 253
|
||||||
|
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]
|
||||||
assert nit >= 0x1_0000
|
assert nit >= 0x1_0000
|
||||||
|
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]
|
||||||
assert nit >= 0x1_0000_0000
|
assert nit >= 0x1_0000_0000
|
||||||
|
num_bytes += 8
|
||||||
|
if ret_num_bytes:
|
||||||
|
return nit, num_bytes
|
||||||
return nit
|
return nit
|
||||||
|
|
||||||
def deser_string(f):
|
def deser_string(f):
|
||||||
@ -195,49 +200,47 @@ def disassemble(script):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
offset = 0
|
offset = 0
|
||||||
slen = len(script)
|
|
||||||
while 1:
|
while 1:
|
||||||
if offset >= slen:
|
if offset >= len(script):
|
||||||
#print('dis %d done' % offset)
|
#print('dis %d done' % offset)
|
||||||
return
|
return
|
||||||
c = script[offset]
|
c = script[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
|
|
||||||
if 1 <= c <= 75:
|
if 1 <= c <= 75:
|
||||||
cnt = c
|
#print('dis %d: bytes=%s' % (offset, b2a_hex(script[offset:offset+c])))
|
||||||
|
yield (script[offset:offset+c], None)
|
||||||
|
offset += c
|
||||||
elif OP_1 <= c <= OP_16:
|
elif OP_1 <= c <= OP_16:
|
||||||
# OP_1 thru OP_16
|
# OP_1 thru OP_16
|
||||||
|
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
|
||||||
yield (c - OP_1 + 1, None)
|
yield (c - OP_1 + 1, None)
|
||||||
continue
|
|
||||||
elif c == OP_PUSHDATA1:
|
elif c == OP_PUSHDATA1:
|
||||||
cnt = script[offset]
|
cnt = script[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
|
yield (script[offset:offset+cnt], None)
|
||||||
|
offset += cnt
|
||||||
elif c == OP_PUSHDATA2:
|
elif c == OP_PUSHDATA2:
|
||||||
# up to 65535 bytes
|
# up to 65535 bytes
|
||||||
cnt, = struct.unpack_from("H", script, offset)
|
cnt, = struct.unpack_from("H", script, offset)
|
||||||
offset += 2
|
offset += 2
|
||||||
|
yield (script[offset:offset+cnt], None)
|
||||||
|
offset += cnt
|
||||||
elif c == OP_PUSHDATA4:
|
elif c == OP_PUSHDATA4:
|
||||||
# no where to put so much data
|
# no where to put so much data
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
elif c == OP_1NEGATE:
|
elif c == OP_1NEGATE:
|
||||||
yield (-1, None)
|
yield (-1, None)
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
# OP_0 included here
|
# OP_0 included here
|
||||||
|
#print('dis %d: opcode=%d' % (offset, c))
|
||||||
yield (None, c)
|
yield (None, c)
|
||||||
continue
|
|
||||||
|
|
||||||
# a data push of `cnt` bytes - reject if it runs off the end
|
|
||||||
if offset + cnt > slen:
|
|
||||||
raise ValueError
|
|
||||||
yield (script[offset:offset+cnt], None)
|
|
||||||
offset += cnt
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# import sys;sys.print_exception(e)
|
# import sys;sys.print_exception(e)
|
||||||
raise ValueError("bad script")
|
raise ValueError("bad script")
|
||||||
|
|
||||||
|
|
||||||
def ser_sig_der(r, s, sighash_type=1):
|
def ser_sig_der(r, s, sighash_type=SIGHASH_ALL):
|
||||||
# Take R and S values from a signature and encode into DER format.
|
# Take R and S values from a signature and encode into DER format.
|
||||||
sig = b"\x30"
|
sig = b"\x30"
|
||||||
|
|
||||||
@ -353,38 +356,36 @@ class CTxOut(object):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
def get_address(self):
|
def get_address(self):
|
||||||
# Detect type of output from scriptPubKey, and return 3-tuple:
|
# Detect type of output from scriptPubKey, and return 2-tuple:
|
||||||
# (addr_type_code, addr, is_segwit)
|
# (addr_type_code, pubkey/pubkeyhash/scripthash)
|
||||||
# 'addr' is byte string, either 20 or 32 long
|
# 'addr' is byte string, either 20 or 32 long
|
||||||
if self.is_p2tr():
|
if self.is_p2tr():
|
||||||
return AF_P2TR, self.scriptPubKey[2:2+32], True
|
return AF_P2TR, self.scriptPubKey[2:2+32]
|
||||||
|
|
||||||
if self.is_p2wpkh():
|
if self.is_p2wpkh():
|
||||||
return AF_P2WPKH, self.scriptPubKey[2:2+20], True
|
return AF_P2WPKH, self.scriptPubKey[2:2+20]
|
||||||
|
|
||||||
if self.is_p2wsh():
|
if self.is_p2wsh():
|
||||||
return AF_P2WSH, self.scriptPubKey[2:2+32], True
|
return AF_P2WSH, self.scriptPubKey[2:2+32]
|
||||||
|
|
||||||
if self.is_p2pkh():
|
if self.is_p2pkh():
|
||||||
return AF_CLASSIC, self.scriptPubKey[3:3+20], False
|
return AF_CLASSIC, self.scriptPubKey[3:3+20]
|
||||||
|
|
||||||
if self.is_p2sh():
|
if self.is_p2sh():
|
||||||
# can be:
|
# can be:
|
||||||
# * bare P2SH
|
# * bare P2SH
|
||||||
# * P2SH-P2WPKH
|
# * P2SH-P2WPKH
|
||||||
# * P2SH-P2WSH
|
# * P2SH-P2WSH
|
||||||
return AF_P2SH, self.scriptPubKey[2:2+20], False
|
return AF_P2SH, self.scriptPubKey[2:2+20]
|
||||||
|
|
||||||
if self.is_p2pk():
|
if self.is_p2pk():
|
||||||
# rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
|
# rare, pay to full pubkey
|
||||||
# push_op is 0x21 (33) for compressed, 0x41 (65) for uncompressed
|
return AF_BARE_PK, self.scriptPubKey[2:2+33]
|
||||||
pk_len = self.scriptPubKey[0]
|
|
||||||
return AF_BARE_PK, self.scriptPubKey[1:1+pk_len], False
|
|
||||||
|
|
||||||
if self.is_op_return():
|
if self.scriptPubKey[0] == OP_RETURN:
|
||||||
return OP_RETURN, self.scriptPubKey, False
|
return OP_RETURN, self.scriptPubKey
|
||||||
|
|
||||||
return None, self.scriptPubKey, None
|
return None, self.scriptPubKey
|
||||||
|
|
||||||
def is_p2tr(self):
|
def is_p2tr(self):
|
||||||
return len(self.scriptPubKey) == 34 and \
|
return len(self.scriptPubKey) == 34 and \
|
||||||
@ -409,11 +410,8 @@ class CTxOut(object):
|
|||||||
|
|
||||||
def is_p2pk(self):
|
def is_p2pk(self):
|
||||||
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
|
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
|
||||||
and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
|
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \
|
||||||
and self.scriptPubKey[-1] == OP_CHECKSIG
|
and self.scriptPubKey[-1] == 0xac
|
||||||
|
|
||||||
def is_op_return(self):
|
|
||||||
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
|
|
||||||
|
|
||||||
#def __repr__(self):
|
#def __repr__(self):
|
||||||
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
|
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
|
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
|
||||||
#
|
#
|
||||||
# - implements stream IO protocol
|
# - implements stream IO protoccol
|
||||||
# - random read, sequential write
|
# - random read, sequential write
|
||||||
# - only a few of these are possible
|
# - only a few of these are possible
|
||||||
# - the offset is the file name
|
# - the offset is the file name
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from bbqr import b32encode, b32decode
|
|||||||
from menu import MenuItem, MenuSystem
|
from menu import MenuItem, MenuSystem
|
||||||
from notes import NoteContentBase
|
from notes import NoteContentBase
|
||||||
from sffile import SFFile
|
from sffile import SFFile
|
||||||
from multisig import MultisigWallet
|
from wallet import MiniScriptWallet
|
||||||
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
|
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
|
||||||
|
|
||||||
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
|
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
|
||||||
@ -233,6 +233,7 @@ def pick_noid_key():
|
|||||||
|
|
||||||
async def kt_decode_rx(is_psbt, payload):
|
async def kt_decode_rx(is_psbt, payload):
|
||||||
# we are getting data back from a sender, decode it.
|
# we are getting data back from a sender, decode it.
|
||||||
|
dis.fullscreen("Wait...")
|
||||||
|
|
||||||
prompt = 'Teleport Password (text)'
|
prompt = 'Teleport Password (text)'
|
||||||
|
|
||||||
@ -251,11 +252,11 @@ async def kt_decode_rx(is_psbt, payload):
|
|||||||
ses_key, body = decode_step1(pair, his_pubkey, body)
|
ses_key, body = decode_step1(pair, his_pubkey, body)
|
||||||
else:
|
else:
|
||||||
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
|
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
|
||||||
if not MultisigWallet.exists():
|
if not MiniScriptWallet.exists():
|
||||||
await ux_show_story("Incoming PSBT requires multisig wallet(s) to be already setup, but you have none.")
|
await ux_show_story("Incoming PSBT requires miniscript wallet(s) to be already setup, but you have none.")
|
||||||
return
|
return
|
||||||
|
|
||||||
ses_key, body, sender_xfp = MultisigWallet.kt_search_rxkey(payload)
|
ses_key, body, sender_xfp = MiniScriptWallet.kt_search_rxkey(payload)
|
||||||
|
|
||||||
if sender_xfp is not None:
|
if sender_xfp is not None:
|
||||||
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)
|
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)
|
||||||
@ -343,20 +344,17 @@ async def kt_accept_values(dtype, raw):
|
|||||||
|
|
||||||
# This will take over UX w/ the signing process
|
# This will take over UX w/ the signing process
|
||||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||||
sign_transaction(psbt_len, flags=None, input_method="kt")
|
sign_transaction(psbt_len, flags=None)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif dtype == 'b':
|
elif dtype == 'b':
|
||||||
# full system backup, including master: text lines
|
# full system backup, including master: text lines
|
||||||
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
|
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
|
||||||
|
|
||||||
try:
|
vals = text_bk_parser(raw)
|
||||||
vals = text_bk_parser(raw)
|
assert vals # empty?
|
||||||
assert vals # empty?
|
|
||||||
raw_sec, _ = extract_raw_secret(vals)
|
raw_sec, _ = extract_raw_secret(vals)
|
||||||
except Exception as e:
|
|
||||||
await ux_show_story("Invalid backup\n\n" + str(e), title='FAILED')
|
|
||||||
return
|
|
||||||
|
|
||||||
from flow import has_secrets
|
from flow import has_secrets
|
||||||
|
|
||||||
@ -591,10 +589,10 @@ class SecretPickerMenu(MenuSystem):
|
|||||||
async def share_full_backup(self, *a):
|
async def share_full_backup(self, *a):
|
||||||
# context, and warn them
|
# context, and warn them
|
||||||
ch = await ux_show_story("Sending complete backup, including master secret, "
|
ch = await ux_show_story("Sending complete backup, including master secret, "
|
||||||
"seed vault (if any), multisig wallets, notes/passwords, and all settings! "
|
"seed vault (if any), miniscript wallets, notes/passwords, and all settings! "
|
||||||
"The receiving "
|
"The receiving "
|
||||||
"COLDCARD must already have the master seed wiped to be able to install "
|
"COLDCARD must already have the master seed wiped to be able to install "
|
||||||
"everything, otherwise only master secret and multisig are saved into a tmp seed. "
|
"everything, otherwise only master secret and miniscripts are saved into a tmp seed. "
|
||||||
"OK to proceed?")
|
"OK to proceed?")
|
||||||
if ch != 'y': return
|
if ch != 'y': return
|
||||||
|
|
||||||
@ -646,18 +644,24 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
|||||||
# User wants to send to one or more other senders for them to complete signing.
|
# User wants to send to one or more other senders for them to complete signing.
|
||||||
|
|
||||||
# who remains to sign? look at inputs
|
# who remains to sign? look at inputs
|
||||||
ms = psbt.active_multisig
|
# all_xfps is set, no need to list one master xfp more than once - assuming CC can sign it all
|
||||||
all_xfps = [x for x,*p in ms.get_xfp_paths()]
|
assert psbt.active_miniscript
|
||||||
need = [x for x in psbt.multisig_xfps_needed() if x in all_xfps]
|
ms = psbt.active_miniscript
|
||||||
|
all_xfps = {x for x,*p in ms.to_descriptor().xfp_paths(skip_unspend_ik=True)}
|
||||||
|
|
||||||
|
# ignore -> keys to ignore, currently only musig aggregate keys
|
||||||
|
need, ignore = psbt.miniscript_xfps_needed()
|
||||||
|
need = [x for x in need if x in all_xfps]
|
||||||
# maybe it's not really a PSBT where we know the other signers? might be
|
# maybe it's not really a PSBT where we know the other signers? might be
|
||||||
# a weird coinjoin we don't fully understand
|
# a weird coinjoin we don't fully understand
|
||||||
if not need:
|
if not need:
|
||||||
await ux_show_story("No more signers?")
|
await ux_show_story("No more signers?")
|
||||||
return
|
return
|
||||||
|
|
||||||
# (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
|
# move out of PSRAM
|
||||||
with SFFile(psbt_offset, psbt_len) as fd:
|
from auth import TXN_OUTPUT_OFFSET
|
||||||
|
|
||||||
|
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
|
||||||
bin_psbt = fd.read(psbt_len)
|
bin_psbt = fd.read(psbt_len)
|
||||||
|
|
||||||
my_xfp = settings.get('xfp')
|
my_xfp = settings.get('xfp')
|
||||||
@ -677,7 +681,7 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
|||||||
|
|
||||||
ci = []
|
ci = []
|
||||||
next_signer = None
|
next_signer = None
|
||||||
for idx, x in enumerate(all_xfps):
|
for idx, x in enumerate(all_xfps - ignore): # set diff
|
||||||
txt = '[%s] Co-signer #%d' % (xfp2str(x), idx+1)
|
txt = '[%s] Co-signer #%d' % (xfp2str(x), idx+1)
|
||||||
f = done_cb
|
f = done_cb
|
||||||
if x == my_xfp:
|
if x == my_xfp:
|
||||||
@ -690,7 +694,7 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
|||||||
async def sign_now(*a):
|
async def sign_now(*a):
|
||||||
# this will reset the UX stack:
|
# this will reset the UX stack:
|
||||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||||
sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
|
sign_transaction(psbt_len, flags=None, offset=psbt_offset)
|
||||||
|
|
||||||
f = sign_now
|
f = sign_now
|
||||||
|
|
||||||
@ -713,14 +717,16 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
|||||||
m.goto_idx(next_signer) # position cursor on next candidate
|
m.goto_idx(next_signer) # position cursor on next candidate
|
||||||
the_ux.push(m)
|
the_ux.push(m)
|
||||||
await m.interact()
|
await m.interact()
|
||||||
|
|
||||||
if m.next_xfp:
|
if m.next_xfp:
|
||||||
assert m.next_xfp != my_xfp
|
assert m.next_xfp != my_xfp
|
||||||
ri, rx_pubkey, kp = ms.kt_make_rxkey(m.next_xfp)
|
ri, rx_pubkey, kp = ms.kt_make_rxkey(m.next_xfp)
|
||||||
await kt_do_send(rx_pubkey, 'p', raw=bin_psbt, prefix=ri, kp=kp,
|
await kt_do_send(rx_pubkey, 'p', raw=bin_psbt, prefix=ri, kp=kp,
|
||||||
rx_label='[%s] co-signer' % xfp2str(m.next_xfp))
|
rx_label='[%s] co-signer' % xfp2str(m.next_xfp))
|
||||||
|
|
||||||
return True, ms.M - (ms.N - len(need))
|
return True
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def kt_send_file_psbt(*a):
|
async def kt_send_file_psbt(*a):
|
||||||
# Menu item: choose a PSBT file from SD card, and send to co-signers.
|
# Menu item: choose a PSBT file from SD card, and send to co-signers.
|
||||||
@ -769,8 +775,6 @@ async def kt_send_file_psbt(*a):
|
|||||||
psbt.consider_inputs()
|
psbt.consider_inputs()
|
||||||
dis.progress_sofar(3, 4)
|
dis.progress_sofar(3, 4)
|
||||||
|
|
||||||
psbt.consider_keys()
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# not going to do full reporting here, use our other code for that!
|
# not going to do full reporting here, use our other code for that!
|
||||||
await ux_show_story("Cannot validate PSBT?\n\n"+str(exc), "PSBT Load Failed")
|
await ux_show_story("Cannot validate PSBT?\n\n"+str(exc), "PSBT Load Failed")
|
||||||
@ -778,8 +782,8 @@ async def kt_send_file_psbt(*a):
|
|||||||
finally:
|
finally:
|
||||||
dis.progress_bar_show(1)
|
dis.progress_bar_show(1)
|
||||||
|
|
||||||
if not psbt.active_multisig:
|
if not psbt.active_miniscript:
|
||||||
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
|
await ux_show_story("We are not part of this wallet.", "Cannot Teleport PSBT")
|
||||||
return
|
return
|
||||||
|
|
||||||
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
|
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
|
||||||
|
|||||||
@ -12,6 +12,8 @@ from menu import MenuSystem, MenuItem
|
|||||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
|
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
|
||||||
from stash import SecretStash
|
from stash import SecretStash
|
||||||
from drv_entro import bip85_derive
|
from drv_entro import bip85_derive
|
||||||
|
from glob import settings
|
||||||
|
|
||||||
from utils import node_from_privkey
|
from utils import node_from_privkey
|
||||||
|
|
||||||
# see from mk4-bootloader/se2.h
|
# see from mk4-bootloader/se2.h
|
||||||
@ -99,22 +101,6 @@ class TrickPinMgmt:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128
|
assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128
|
||||||
self.reload()
|
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
# we track known PINS as a dictionary:
|
|
||||||
# pin (in ascii) => (slot_num, tc_flags, arg)
|
|
||||||
from glob import settings
|
|
||||||
self.tp = settings.get('tp', {})
|
|
||||||
|
|
||||||
def save_record(self):
|
|
||||||
# commit changes back to settings
|
|
||||||
from glob import settings
|
|
||||||
if self.tp:
|
|
||||||
settings.set('tp', self.tp)
|
|
||||||
else:
|
|
||||||
settings.remove_key('tp')
|
|
||||||
settings.save()
|
|
||||||
|
|
||||||
def roundtrip(self, method_num, slot_buf=None):
|
def roundtrip(self, method_num, slot_buf=None):
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
@ -134,26 +120,36 @@ class TrickPinMgmt:
|
|||||||
|
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
return settings.get("tp", {})
|
||||||
|
|
||||||
|
def commit(self, trick_pins):
|
||||||
|
settings.set("tp", trick_pins)
|
||||||
|
settings.save()
|
||||||
|
|
||||||
def clear_all(self):
|
def clear_all(self):
|
||||||
# get rid of them all
|
# get rid of them all
|
||||||
self.roundtrip(0)
|
self.roundtrip(0)
|
||||||
self.tp = {}
|
settings.remove_key('tp')
|
||||||
self.save_record()
|
settings.save()
|
||||||
|
|
||||||
def forget_pin(self, pin):
|
def forget_pin(self, pin):
|
||||||
# forget about settings for a PIN
|
# forget about settings for a PIN
|
||||||
self.tp.pop(pin, None)
|
t_pins = self.get_all()
|
||||||
self.save_record()
|
t_pins.pop(pin, None)
|
||||||
|
self.commit(t_pins)
|
||||||
|
|
||||||
def restore_pin(self, new_pin):
|
def restore_pin(self, new_pin):
|
||||||
# remember/restore PIN that we "forgot", return T if worked
|
# remember/restore PIN that we "forgot", return T if worked
|
||||||
b, slot = tp.get_by_pin(new_pin)
|
b, slot = self.get_by_pin(new_pin)
|
||||||
if slot is None: return False
|
if slot is None: return False
|
||||||
|
|
||||||
record = (slot.slot_num, slot.tc_flags,
|
record = (slot.slot_num, slot.tc_flags,
|
||||||
0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
|
0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
|
||||||
self.tp[new_pin] = record
|
|
||||||
self.save_record()
|
t_pins = self.get_all()
|
||||||
|
t_pins[new_pin] = record
|
||||||
|
self.commit(t_pins)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -211,14 +207,14 @@ class TrickPinMgmt:
|
|||||||
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
|
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
|
||||||
# create or update a trick pin
|
# create or update a trick pin
|
||||||
# - doesn't support wallet to no-wallet transitions
|
# - doesn't support wallet to no-wallet transitions
|
||||||
#
|
'''
|
||||||
# from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
|
>>> from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
|
||||||
#
|
'''
|
||||||
assert isinstance(pin, bytes)
|
assert isinstance(pin, bytes)
|
||||||
|
|
||||||
b, slot = self.get_by_pin(pin)
|
b, slot = self.get_by_pin(pin)
|
||||||
if not slot:
|
if not slot:
|
||||||
assert new, "wrong pin"
|
if not new: raise KeyError("wrong pin")
|
||||||
|
|
||||||
# Making a new entry
|
# Making a new entry
|
||||||
b, slot = make_slot()
|
b, slot = make_slot()
|
||||||
@ -232,11 +228,12 @@ class TrickPinMgmt:
|
|||||||
|
|
||||||
slot.slot_num = sn
|
slot.slot_num = sn
|
||||||
|
|
||||||
|
t_pins = self.get_all()
|
||||||
if new_pin is not None:
|
if new_pin is not None:
|
||||||
slot.pin_len = len(new_pin)
|
slot.pin_len = len(new_pin)
|
||||||
slot.pin[0:slot.pin_len] = new_pin
|
slot.pin[0:slot.pin_len] = new_pin
|
||||||
if new_pin != pin:
|
if new_pin != pin:
|
||||||
self.tp.pop(pin.decode(), None)
|
t_pins.pop(pin.decode(), None)
|
||||||
pin = new_pin
|
pin = new_pin
|
||||||
|
|
||||||
if tc_flags is not None:
|
if tc_flags is not None:
|
||||||
@ -270,14 +267,14 @@ class TrickPinMgmt:
|
|||||||
assert rc == 0
|
assert rc == 0
|
||||||
|
|
||||||
# record key details.
|
# record key details.
|
||||||
self.tp[pin.decode()] = record
|
t_pins[pin.decode()] = record
|
||||||
self.save_record()
|
self.commit(t_pins)
|
||||||
|
|
||||||
return b, slot
|
return b, slot
|
||||||
|
|
||||||
def all_tricks(self):
|
def all_tricks(self):
|
||||||
# put them in order, with "wrong" last
|
# put them in order, with "wrong" last
|
||||||
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
return sorted(self.get_all().keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
||||||
|
|
||||||
def define_unlock_pin(self, new_pin):
|
def define_unlock_pin(self, new_pin):
|
||||||
# user is setting the bypass PIN for first time.
|
# user is setting the bypass PIN for first time.
|
||||||
@ -304,16 +301,14 @@ class TrickPinMgmt:
|
|||||||
# if spending policy defined, this PIN allows adjustment
|
# if spending policy defined, this PIN allows adjustment
|
||||||
# - not TRICK bypass choices, like ones that wipe
|
# - not TRICK bypass choices, like ones that wipe
|
||||||
# - could be multiple, but only first returned.
|
# - could be multiple, but only first returned.
|
||||||
self.reload()
|
for k, (sn,flags,arg) in self.get_all().items():
|
||||||
for k, (sn,flags,arg) in self.tp.items():
|
|
||||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||||
return k
|
return k
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_sp_unlock_pins(self):
|
def delete_sp_unlock_pins(self):
|
||||||
# remove all bypass pins, they are done w/ feature
|
# remove all bypass pins, they are done w/ feature
|
||||||
self.reload()
|
for k, (sn,flags,arg) in self.get_all().items():
|
||||||
for k, (sn,flags,arg) in self.tp.items():
|
|
||||||
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||||
self.clear_slots([sn])
|
self.clear_slots([sn])
|
||||||
self.forget_pin(k)
|
self.forget_pin(k)
|
||||||
@ -321,13 +316,13 @@ class TrickPinMgmt:
|
|||||||
|
|
||||||
def get_deltamode_pins(self):
|
def get_deltamode_pins(self):
|
||||||
# iterate over all delta-mode PIN's defined.
|
# iterate over all delta-mode PIN's defined.
|
||||||
for k, (sn,flags,args) in self.tp.items():
|
for k, (sn,flags,args) in self.get_all().items():
|
||||||
if flags & TC_DELTA_MODE:
|
if flags & TC_DELTA_MODE:
|
||||||
yield k
|
yield k
|
||||||
|
|
||||||
def get_duress_pins(self):
|
def get_duress_pins(self):
|
||||||
# iterate over all duress wallets
|
# iterate over all duress wallets
|
||||||
for k, (sn,flags,args) in self.tp.items():
|
for k, (sn,flags,args) in self.get_all().items():
|
||||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||||
yield k
|
yield k
|
||||||
|
|
||||||
@ -338,7 +333,7 @@ class TrickPinMgmt:
|
|||||||
# as checking only self.tp is not sufficient for hidden TPs or after fast wipe
|
# as checking only self.tp is not sufficient for hidden TPs or after fast wipe
|
||||||
# - return error msg or None
|
# - return error msg or None
|
||||||
assert isinstance(pin, str)
|
assert isinstance(pin, str)
|
||||||
b, slot = tp.get_by_pin(pin)
|
b, slot = self.get_by_pin(pin)
|
||||||
if slot is not None:
|
if slot is not None:
|
||||||
return 'That PIN is already in use as a Trick PIN.'
|
return 'That PIN is already in use as a Trick PIN.'
|
||||||
|
|
||||||
@ -357,8 +352,9 @@ class TrickPinMgmt:
|
|||||||
def backup_duress_wallets(self, sv):
|
def backup_duress_wallets(self, sv):
|
||||||
# for backup file, yield (label, path, pairs-of-data)
|
# for backup file, yield (label, path, pairs-of-data)
|
||||||
done = set()
|
done = set()
|
||||||
|
t_pins = self.get_all()
|
||||||
for pin in self.get_duress_pins():
|
for pin in self.get_duress_pins():
|
||||||
sn, flags, arg = self.tp[pin]
|
sn, flags, arg = t_pins[pin]
|
||||||
|
|
||||||
if (flags, arg) in done:
|
if (flags, arg) in done:
|
||||||
continue
|
continue
|
||||||
@ -368,7 +364,7 @@ class TrickPinMgmt:
|
|||||||
label = "Duress: BIP-85 Derived wallet"
|
label = "Duress: BIP-85 Derived wallet"
|
||||||
nwords = 12 if ((arg // 1000) == 2) else 24
|
nwords = 12 if ((arg // 1000) == 2) else 24
|
||||||
path = "BIP85(words=%d, index=%d)" % (nwords, arg)
|
path = "BIP85(words=%d, index=%d)" % (nwords, arg)
|
||||||
b, slot = tp.get_by_pin(pin)
|
b, slot = self.get_by_pin(pin)
|
||||||
words = bip39.b2a_words(slot.xdata[0:(32 if nwords==24 else 16)])
|
words = bip39.b2a_words(slot.xdata[0:(32 if nwords==24 else 16)])
|
||||||
|
|
||||||
d = [ ('duress_%d_words' % arg, words) ]
|
d = [ ('duress_%d_words' % arg, words) ]
|
||||||
@ -398,17 +394,17 @@ class TrickPinMgmt:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if flags & TC_DELTA_MODE:
|
if flags & TC_DELTA_MODE:
|
||||||
prob, arg = validate_delta_pin(true_pin, pin)
|
prob = validate_delta_pin(true_pin, pin)
|
||||||
if prob:
|
if prob:
|
||||||
# just forget it, no UI here to report issue
|
# just forget it, no UI here to report issue
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# might need to construct a BIP-85 or XPRV secret to match
|
# might need to construct a BIP-85 or XPRV secret to match
|
||||||
path, new_secret = construct_duress_secret(flags, arg)
|
path, new_secret = construct_duress_secret(flags, arg)
|
||||||
|
|
||||||
tp.update_slot(pin.encode(), new=True, secret=new_secret,
|
self.update_slot(pin.encode(), new=True, tc_flags=flags,
|
||||||
tc_flags=flags, tc_arg=arg)
|
tc_arg=arg, secret=new_secret)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -444,7 +440,6 @@ class TrickPinMenu(MenuSystem):
|
|||||||
if bool(pa.tmp_value):
|
if bool(pa.tmp_value):
|
||||||
return [MenuItem('Not Available')]
|
return [MenuItem('Not Available')]
|
||||||
|
|
||||||
tp.reload()
|
|
||||||
tricks = tp.all_tricks()
|
tricks = tp.all_tricks()
|
||||||
|
|
||||||
if self.current_pin in tricks:
|
if self.current_pin in tricks:
|
||||||
@ -841,7 +836,7 @@ so you may perform transactions with it.''')
|
|||||||
|
|
||||||
# "arg" can be out-of-date, if they edited timer value after parent was
|
# "arg" can be out-of-date, if they edited timer value after parent was
|
||||||
# rendered, where arg was captured into item.arg ... so don't use it.
|
# rendered, where arg was captured into item.arg ... so don't use it.
|
||||||
cd_val = tp.tp[pin][2]
|
cd_val = tp.get_all()[pin][2]
|
||||||
|
|
||||||
msg = 'Shows login countdown (%s)' % lgto_map.get(cd_val, '???').strip()
|
msg = 'Shows login countdown (%s)' % lgto_map.get(cd_val, '???').strip()
|
||||||
if flags & TC_WIPE:
|
if flags & TC_WIPE:
|
||||||
@ -857,14 +852,13 @@ so you may perform transactions with it.''')
|
|||||||
|
|
||||||
def adjust_countdown_chooser():
|
def adjust_countdown_chooser():
|
||||||
# 'disabled' choice not appropriate for this case
|
# 'disabled' choice not appropriate for this case
|
||||||
ch = lgto_ch[1:]
|
|
||||||
va = lgto_va[1:]
|
va = lgto_va[1:]
|
||||||
|
|
||||||
def set_it(idx, text):
|
def set_it(idx, text):
|
||||||
new_val = va[idx]
|
new_val = va[idx]
|
||||||
# save it
|
# save it
|
||||||
try:
|
try:
|
||||||
b, slot = tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
|
tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
return va.index(cd_val), lgto_ch[1:], set_it
|
return va.index(cd_val), lgto_ch[1:], set_it
|
||||||
@ -915,7 +909,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
|||||||
# drill down into a sub-menu per existing PIN
|
# drill down into a sub-menu per existing PIN
|
||||||
# - data display only, no editing; just clear and redo
|
# - data display only, no editing; just clear and redo
|
||||||
pin = item.arg
|
pin = item.arg
|
||||||
slot_num, flags, arg = tp.tp[pin] if (pin in tp.tp) else (-1, 0, 0)
|
t_pins = tp.get_all()
|
||||||
|
slot_num, flags, arg = t_pins[pin] if (pin in t_pins) else (-1, 0, 0)
|
||||||
|
|
||||||
rv = []
|
rv = []
|
||||||
|
|
||||||
|
|||||||
173
shared/usb.py
173
shared/usb.py
@ -2,10 +2,10 @@
|
|||||||
#
|
#
|
||||||
# usb.py - USB related things
|
# usb.py - USB related things
|
||||||
#
|
#
|
||||||
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr
|
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr, ujson
|
||||||
from uasyncio import sleep_ms, core
|
from uasyncio import sleep_ms, core
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN, AFC_SCRIPT
|
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN
|
||||||
from public_constants import STXN_FLAGS_MASK
|
from public_constants import STXN_FLAGS_MASK
|
||||||
from ustruct import pack, unpack_from
|
from ustruct import pack, unpack_from
|
||||||
from ckcc import watchpoint, is_simulator
|
from ckcc import watchpoint, is_simulator
|
||||||
@ -53,8 +53,8 @@ HSM_WHITELIST = frozenset({
|
|||||||
'smsg', # limited by policy
|
'smsg', # limited by policy
|
||||||
'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', # quick status checks
|
||||||
'p2sh', 'show', # limited by HSM policy
|
'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
|
||||||
})
|
})
|
||||||
@ -75,7 +75,14 @@ HOBBLED_CMDS = frozenset({
|
|||||||
'enrl', # no new multisigs during policy enforcement
|
'enrl', # no new multisigs during policy enforcement
|
||||||
'back', # no backups
|
'back', # no backups
|
||||||
'bagi', 'dfu_', # just in case
|
'bagi', 'dfu_', # just in case
|
||||||
}) | HSM_DISABLE_CMDS
|
|
||||||
|
"user", # same as HSM_DISABLE_CMDS
|
||||||
|
"rmur",
|
||||||
|
"nwur",
|
||||||
|
"gslr",
|
||||||
|
"hsts",
|
||||||
|
"hsms",
|
||||||
|
})
|
||||||
|
|
||||||
# singleton instance of USBHandler()
|
# singleton instance of USBHandler()
|
||||||
handler = None
|
handler = None
|
||||||
@ -126,6 +133,16 @@ def is_vcp_active():
|
|||||||
|
|
||||||
return cur and ('VCP' in cur) and en
|
return cur and ('VCP' in cur) and en
|
||||||
|
|
||||||
|
|
||||||
|
def get_miniscript_by_name(name_bytes):
|
||||||
|
from wallet import MiniScriptWallet
|
||||||
|
|
||||||
|
for w in MiniScriptWallet.iter_wallets():
|
||||||
|
if w.name == str(name_bytes, 'ascii'):
|
||||||
|
return True, w
|
||||||
|
else:
|
||||||
|
return False, b'err_Miniscript wallet not found'
|
||||||
|
|
||||||
class USBHandler:
|
class USBHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.dev = pyb.USB_HID()
|
self.dev = pyb.USB_HID()
|
||||||
@ -416,12 +433,10 @@ class USBHandler:
|
|||||||
|
|
||||||
if cmd == 'dwld':
|
if cmd == 'dwld':
|
||||||
offset, length, fileno = unpack_from('<III', args)
|
offset, length, fileno = unpack_from('<III', args)
|
||||||
assert len(args) == 12, 'badlen'
|
|
||||||
return await self.handle_download(offset, length, fileno)
|
return await self.handle_download(offset, length, fileno)
|
||||||
|
|
||||||
if cmd == 'ncry':
|
if cmd == 'ncry':
|
||||||
version, his_pubkey = unpack_from('<I64s', args)
|
version, his_pubkey = unpack_from('<I64s', args)
|
||||||
assert len(args) == 68, 'badlen'
|
|
||||||
|
|
||||||
return self.handle_crypto_setup(version, his_pubkey)
|
return self.handle_crypto_setup(version, his_pubkey)
|
||||||
|
|
||||||
@ -451,54 +466,19 @@ class USBHandler:
|
|||||||
if cmd == 'smsg':
|
if cmd == 'smsg':
|
||||||
# sign message
|
# sign message
|
||||||
addr_fmt, len_subpath, len_msg = unpack_from('<III', args)
|
addr_fmt, len_subpath, len_msg = unpack_from('<III', args)
|
||||||
assert len(args) == (12 + len_subpath + len_msg), 'badlen'
|
|
||||||
subpath = args[12:12+len_subpath]
|
subpath = args[12:12+len_subpath]
|
||||||
msg = args[12+len_subpath:]
|
msg = args[12+len_subpath:]
|
||||||
|
assert len(msg) == len_msg, "badlen"
|
||||||
|
|
||||||
from auth import sign_msg
|
from auth import sign_msg
|
||||||
sign_msg(msg, subpath, addr_fmt)
|
sign_msg(msg, subpath, addr_fmt)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if cmd == 'p2sh':
|
|
||||||
# show P2SH (probably multisig) address on screen (also provides it back)
|
|
||||||
# - must provide redeem script, and list of [xfp+path]
|
|
||||||
from auth import start_show_p2sh_address
|
|
||||||
|
|
||||||
if hsm_active and not hsm_active.approve_address_share(is_p2sh=True):
|
|
||||||
raise HSMDenied
|
|
||||||
|
|
||||||
# new multsig goodness, needs mapping from xfp->path and M values
|
|
||||||
addr_fmt, M, N, script_len = unpack_from('<IBBH', args)
|
|
||||||
|
|
||||||
assert addr_fmt & AFC_SCRIPT
|
|
||||||
assert 1 <= M <= N <= 20
|
|
||||||
assert 30 <= script_len <= 520
|
|
||||||
|
|
||||||
offset = 8
|
|
||||||
witdeem_script = args[offset:offset+script_len]
|
|
||||||
offset += script_len
|
|
||||||
|
|
||||||
assert len(witdeem_script) == script_len
|
|
||||||
|
|
||||||
xfp_paths = []
|
|
||||||
for i in range(N):
|
|
||||||
assert offset < len(args), 'badlen'
|
|
||||||
ln = args[offset]
|
|
||||||
assert 1 <= ln <= 16, 'badlen'
|
|
||||||
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
|
|
||||||
offset += (ln*4) + 1
|
|
||||||
|
|
||||||
assert offset == len(args)
|
|
||||||
|
|
||||||
return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths,
|
|
||||||
witdeem_script)
|
|
||||||
|
|
||||||
if cmd == 'show':
|
if cmd == 'show':
|
||||||
# simple cases, older code: text subpath
|
# simple cases, older code: text subpath
|
||||||
from auth import usb_show_address
|
from auth import usb_show_address
|
||||||
|
|
||||||
addr_fmt, = unpack_from('<I', args)
|
addr_fmt, = unpack_from('<I', args)
|
||||||
assert len(args) >= 4, 'badlen'
|
|
||||||
# regression patch of AFC_BECH32M flag
|
# regression patch of AFC_BECH32M flag
|
||||||
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
|
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
|
||||||
if addr_fmt == 0x17: # old P2TR
|
if addr_fmt == 0x17: # old P2TR
|
||||||
@ -510,10 +490,9 @@ class USBHandler:
|
|||||||
# - text config file must already be uploaded
|
# - text config file must already be uploaded
|
||||||
|
|
||||||
file_len, file_sha = unpack_from('<I32s', args)
|
file_len, file_sha = unpack_from('<I32s', args)
|
||||||
assert len(args) == 36, 'badlen'
|
|
||||||
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
|
||||||
@ -521,24 +500,96 @@ class USBHandler:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if cmd == 'msck':
|
if cmd == 'mins':
|
||||||
# Quick check to test if we have a wallet already installed.
|
# Enroll new xpubkey to be involved in miniscript.
|
||||||
from multisig import MultisigWallet
|
# - descriptor text config file must already be uploaded
|
||||||
M, N, xfp_xor = unpack_from('<3I', args)
|
|
||||||
assert len(args) == 12, 'badlen'
|
file_len, file_sha = unpack_from('<I32s', args)
|
||||||
return int(MultisigWallet.quick_check(M, N, xfp_xor))
|
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)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
if cmd.startswith("ms"):
|
||||||
|
# miniscript related commands
|
||||||
|
assert self.encrypted_req, 'must encrypt'
|
||||||
|
|
||||||
|
if cmd == "msls":
|
||||||
|
# list all registered miniscript wallet names
|
||||||
|
from wallet import MiniScriptWallet
|
||||||
|
wallets = [w.name for w in MiniScriptWallet.iter_wallets()]
|
||||||
|
return b'asci' + ujson.dumps(wallets)
|
||||||
|
|
||||||
|
if cmd == "msas":
|
||||||
|
# get miniscript address based on int/ext index
|
||||||
|
if hsm_active and not hsm_active.approve_address_share(miniscript=True):
|
||||||
|
raise HSMDenied
|
||||||
|
|
||||||
|
change, idx, = unpack_from('<II', args)
|
||||||
|
assert change in (0, 1), "change not bool"
|
||||||
|
assert 0 <= idx < (2 ** 31), "child idx"
|
||||||
|
|
||||||
|
name = args[8:]
|
||||||
|
assert len(name) <= 32, "name len"
|
||||||
|
|
||||||
|
ok, w = get_miniscript_by_name(name)
|
||||||
|
if not ok:
|
||||||
|
return w
|
||||||
|
|
||||||
|
from auth import start_show_miniscript_address
|
||||||
|
return b'asci' + start_show_miniscript_address(w, change, idx)
|
||||||
|
|
||||||
|
|
||||||
|
assert len(args) <= 32, "name len"
|
||||||
|
ok, w = get_miniscript_by_name(args)
|
||||||
|
if not ok:
|
||||||
|
return w
|
||||||
|
|
||||||
|
if cmd == "msdl":
|
||||||
|
# delete miniscript wallet by its name (unique id)
|
||||||
|
from auth import maybe_delete_miniscript
|
||||||
|
maybe_delete_miniscript(w)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if cmd == "msgt":
|
||||||
|
# takes name and returns descriptor + name json
|
||||||
|
# MiniscriptWallet.to_string only fills policy
|
||||||
|
return b'asci' + ujson.dumps({"name": w.name, "desc": w.to_string()})
|
||||||
|
|
||||||
|
if cmd == "mspl":
|
||||||
|
# takes name and returns BIP-388 Wallet Policy
|
||||||
|
return b'asci' + ujson.dumps({"name": w.name, "desc_template": w.desc_tmplt,
|
||||||
|
"keys_info": w.keys_info})
|
||||||
|
|
||||||
if cmd == 'stxn':
|
if cmd == 'stxn':
|
||||||
# sign transaction
|
# sign transaction
|
||||||
txn_len, flags, txn_sha = unpack_from('<II32s', args)
|
txn_len, flags, txn_sha = unpack_from('<II32s', args)
|
||||||
assert len(args) == 40, 'badlen'
|
|
||||||
if txn_sha != self.file_checksum.digest():
|
if txn_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
return b'err_Checksum'
|
||||||
|
|
||||||
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
|
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
|
||||||
|
|
||||||
|
# optional miniscript wallet name
|
||||||
|
try:
|
||||||
|
name_len = unpack_from('B', args[40:])[0]
|
||||||
|
name = str(args[41:41 + name_len], "ascii")
|
||||||
|
assert 1 <= len(name) <= 32, "name len"
|
||||||
|
except:
|
||||||
|
name = None
|
||||||
|
|
||||||
|
w = None
|
||||||
|
if name:
|
||||||
|
ok, w = get_miniscript_by_name(name)
|
||||||
|
if not ok:
|
||||||
|
return w
|
||||||
|
|
||||||
from auth import sign_transaction
|
from auth import sign_transaction
|
||||||
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, input_method="usb")
|
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, miniscript_wallet=w)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
|
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
|
||||||
@ -601,8 +652,6 @@ class USBHandler:
|
|||||||
if cmd == 'rest':
|
if cmd == 'rest':
|
||||||
# restore backup from what is already uploaded in PSRAM
|
# restore backup from what is already uploaded in PSRAM
|
||||||
file_len, file_sha, bf = unpack_from('<I32sB', args)
|
file_len, file_sha, bf = unpack_from('<I32sB', args)
|
||||||
assert len(args) == 37, 'badlen'
|
|
||||||
assert 0 < file_len <= MAX_TXN_LEN, "badlen"
|
|
||||||
if file_sha != self.file_checksum.digest():
|
if file_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
return b'err_Checksum'
|
||||||
|
|
||||||
@ -625,7 +674,6 @@ class USBHandler:
|
|||||||
# HSM mode "start" -- requires user approval
|
# HSM mode "start" -- requires user approval
|
||||||
if args:
|
if args:
|
||||||
file_len, file_sha = unpack_from('<I32s', args)
|
file_len, file_sha = unpack_from('<I32s', args)
|
||||||
assert len(args) == 36, 'badlen'
|
|
||||||
if file_sha != self.file_checksum.digest():
|
if file_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
return b'err_Checksum'
|
||||||
assert 2 <= file_len <= (200*1000), "badlen"
|
assert 2 <= file_len <= (200*1000), "badlen"
|
||||||
@ -641,7 +689,6 @@ class USBHandler:
|
|||||||
if cmd == 'hsts':
|
if cmd == 'hsts':
|
||||||
# can always query HSM mode
|
# can always query HSM mode
|
||||||
from hsm import hsm_status_report
|
from hsm import hsm_status_report
|
||||||
import ujson
|
|
||||||
return b'asci' + ujson.dumps(hsm_status_report())
|
return b'asci' + ujson.dumps(hsm_status_report())
|
||||||
|
|
||||||
if cmd == 'gslr':
|
if cmd == 'gslr':
|
||||||
@ -653,8 +700,6 @@ class USBHandler:
|
|||||||
if cmd == 'nwur': # new user
|
if cmd == 'nwur': # new user
|
||||||
from users import Users
|
from users import Users
|
||||||
auth_mode, ul, sl = unpack_from('<BBB', args)
|
auth_mode, ul, sl = unpack_from('<BBB', args)
|
||||||
assert len(args) == (3 + ul + sl), 'badlen'
|
|
||||||
assert ul and sl, "badlen"
|
|
||||||
username = bytes(args[3:3+ul]).decode('ascii')
|
username = bytes(args[3:3+ul]).decode('ascii')
|
||||||
secret = bytes(args[3+ul:3+ul+sl])
|
secret = bytes(args[3+ul:3+ul+sl])
|
||||||
|
|
||||||
@ -663,8 +708,6 @@ class USBHandler:
|
|||||||
if cmd == 'rmur': # delete user
|
if cmd == 'rmur': # delete user
|
||||||
from users import Users
|
from users import Users
|
||||||
ul, = unpack_from('<B', args)
|
ul, = unpack_from('<B', args)
|
||||||
assert len(args) == (1 + ul), 'badlen'
|
|
||||||
assert ul, "badlen"
|
|
||||||
username = bytes(args[1:1+ul]).decode('ascii')
|
username = bytes(args[1:1+ul]).decode('ascii')
|
||||||
|
|
||||||
return Users.delete(username)
|
return Users.delete(username)
|
||||||
@ -672,8 +715,6 @@ class USBHandler:
|
|||||||
if cmd == 'user': # auth user (HSM mode)
|
if cmd == 'user': # auth user (HSM mode)
|
||||||
from users import Users
|
from users import Users
|
||||||
totp_time, ul, tl = unpack_from('<IBB', args)
|
totp_time, ul, tl = unpack_from('<IBB', args)
|
||||||
assert len(args) == (6 + ul + tl), 'badlen'
|
|
||||||
assert ul and tl, "badlen"
|
|
||||||
username = bytes(args[6:6+ul]).decode('ascii')
|
username = bytes(args[6:6+ul]).decode('ascii')
|
||||||
token = bytes(args[6+ul:6+ul+tl])
|
token = bytes(args[6+ul:6+ul+tl])
|
||||||
|
|
||||||
@ -762,8 +803,7 @@ class USBHandler:
|
|||||||
length = min(length, MAX_BLK_LEN)
|
length = min(length, MAX_BLK_LEN)
|
||||||
|
|
||||||
assert 0 <= file_number < 2, 'bad fnum'
|
assert 0 <= file_number < 2, 'bad fnum'
|
||||||
assert 0 <= offset < MAX_TXN_LEN, "bad offset"
|
assert 0 <= offset <= MAX_TXN_LEN, "bad offset"
|
||||||
assert offset + length <= MAX_TXN_LEN, "bad offset"
|
|
||||||
assert 1 <= length, 'len'
|
assert 1 <= length, 'len'
|
||||||
|
|
||||||
# maintain a running SHA256 over what's sent
|
# maintain a running SHA256 over what's sent
|
||||||
@ -798,8 +838,7 @@ class USBHandler:
|
|||||||
dis.progress_sofar(offset, total_size)
|
dis.progress_sofar(offset, total_size)
|
||||||
|
|
||||||
assert offset % 256 == 0, 'alignment'
|
assert offset % 256 == 0, 'alignment'
|
||||||
assert 1 <= total_size <= MAX_UPLOAD_LEN, 'long'
|
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
|
||||||
assert offset + len(data) <= total_size, 'long'
|
|
||||||
|
|
||||||
if hsm_active or pa.hobbled_mode:
|
if hsm_active or pa.hobbled_mode:
|
||||||
# additional restriction in HSM mode or hobbled: must be PSBT
|
# additional restriction in HSM mode or hobbled: must be PSBT
|
||||||
|
|||||||
@ -78,7 +78,7 @@ KEY = 'usr'
|
|||||||
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
|
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
|
||||||
|
|
||||||
class Users:
|
class Users:
|
||||||
'''Track users and their TOTP secrets or hashed passwords'''
|
'''Track users and thier TOTP secrets or hashed passwords'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls):
|
def get(cls):
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from ubinascii import hexlify as b2a_hex
|
|||||||
from ubinascii import a2b_base64, b2a_base64
|
from ubinascii import a2b_base64, b2a_base64
|
||||||
from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP
|
from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2TR
|
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC
|
||||||
|
|
||||||
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
||||||
|
|
||||||
@ -193,31 +193,34 @@ def str2xfp(txt):
|
|||||||
# Inverse of xfp2str
|
# Inverse of xfp2str
|
||||||
return ustruct.unpack('<I', a2b_hex(txt))[0]
|
return ustruct.unpack('<I', a2b_hex(txt))[0]
|
||||||
|
|
||||||
|
def is_ascii(s):
|
||||||
|
if len(s) == len(s.encode()):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def is_printable(s):
|
def is_printable(s):
|
||||||
|
PRINTABLE = range(32, 127)
|
||||||
for ch in s:
|
for ch in s:
|
||||||
o = ord(ch)
|
if ord(ch) not in PRINTABLE:
|
||||||
if o < 32 or o > 126:
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def to_ascii_printable(s, allow_tab_nl=False):
|
def to_ascii_printable(s, strip=False, only_printable=True):
|
||||||
try:
|
try:
|
||||||
# s must be a string!
|
s = str(s, 'ascii')
|
||||||
assert len(s) == len(s.encode())
|
if strip:
|
||||||
if not allow_tab_nl:
|
s = s.strip()
|
||||||
|
assert is_ascii(s)
|
||||||
|
if only_printable:
|
||||||
assert is_printable(s)
|
assert is_printable(s)
|
||||||
else:
|
|
||||||
for ch in s:
|
|
||||||
o = ord(ch)
|
|
||||||
assert 32 <= o <= 126 or o == 9 or o == 10
|
|
||||||
return s
|
return s
|
||||||
except:
|
except:
|
||||||
err = "must be ascii printable" + (", tab, or newline" if allow_tab_nl else "")
|
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
|
||||||
raise AssertionError(err)
|
|
||||||
|
|
||||||
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 occurred. Best used on AssertionError.
|
# an exception occured. Best used on AssertionError.
|
||||||
|
|
||||||
tmp = uio.StringIO()
|
tmp = uio.StringIO()
|
||||||
sys.print_exception(exc, tmp)
|
sys.print_exception(exc, tmp)
|
||||||
@ -249,7 +252,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
|
|||||||
# - 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)
|
||||||
|
|
||||||
s = to_ascii_printable(str(bin_path, "ascii").strip()).lower()
|
s = to_ascii_printable(bin_path, strip=True).lower()
|
||||||
|
|
||||||
# empty string is valid
|
# empty string is valid
|
||||||
if s == '': return 'm'
|
if s == '': return 'm'
|
||||||
@ -340,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()
|
||||||
@ -453,8 +463,6 @@ def clean_shutdown(style=0):
|
|||||||
callgate.show_logout(style)
|
callgate.show_logout(style)
|
||||||
|
|
||||||
def call_later_ms(delay, cb, *args, **kws):
|
def call_later_ms(delay, cb, *args, **kws):
|
||||||
import uasyncio
|
|
||||||
|
|
||||||
async def doit():
|
async def doit():
|
||||||
await uasyncio.sleep_ms(delay)
|
await uasyncio.sleep_ms(delay)
|
||||||
await cb(*args, **kws)
|
await cb(*args, **kws)
|
||||||
@ -525,32 +533,6 @@ def word_wrap(ln, w):
|
|||||||
ln = ln[nsp:]
|
ln = ln[nsp:]
|
||||||
if not ln: return
|
if not ln: return
|
||||||
|
|
||||||
|
|
||||||
def parse_extended_key(ln, private=False):
|
|
||||||
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
|
|
||||||
# - can handle any garbage line
|
|
||||||
# - returns (node, chain, addr_fmt)
|
|
||||||
# - people are using SLIP132 so we need this
|
|
||||||
node, chain, addr_fmt = None, None, None
|
|
||||||
if ln is None:
|
|
||||||
return node, chain, addr_fmt
|
|
||||||
|
|
||||||
ln = ln.strip()
|
|
||||||
if private:
|
|
||||||
rgx = r'.prv[A-Za-z0-9]+'
|
|
||||||
else:
|
|
||||||
rgx = r'.pub[A-Za-z0-9]+'
|
|
||||||
|
|
||||||
pat = ure.compile(rgx)
|
|
||||||
found = pat.search(ln)
|
|
||||||
# serialize, and note version code
|
|
||||||
try:
|
|
||||||
node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return node, chain, addr_fmt
|
|
||||||
|
|
||||||
def deserialize_secret(text_sec_str):
|
def deserialize_secret(text_sec_str):
|
||||||
# Chip can hold 72-bytes as a secret
|
# Chip can hold 72-bytes as a secret
|
||||||
# - has 0th byte as marker, secret and zero padding to AE_SECRET_LEN
|
# - has 0th byte as marker, secret and zero padding to AE_SECRET_LEN
|
||||||
@ -600,6 +582,7 @@ 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 txid_from_fname(fname):
|
def txid_from_fname(fname):
|
||||||
if len(fname) >= 64:
|
if len(fname) >= 64:
|
||||||
txid = fname[:64]
|
txid = fname[:64]
|
||||||
@ -688,35 +671,6 @@ def decode_bip21_text(got):
|
|||||||
|
|
||||||
raise ValueError('not bip-21')
|
raise ValueError('not bip-21')
|
||||||
|
|
||||||
def validate_own_address(addr):
|
|
||||||
ch = chains.current_chain()
|
|
||||||
addr_l = addr.lower()
|
|
||||||
|
|
||||||
if addr_l[:3] in ("bc1", "tb1") or addr_l[:5] == 'bcrt1':
|
|
||||||
try:
|
|
||||||
hrp, witver, data = ngu.codecs.segwit_decode(addr)
|
|
||||||
|
|
||||||
assert hrp == ch.bech32_hrp
|
|
||||||
assert witver == 0
|
|
||||||
if len(data) == 20:
|
|
||||||
return addr_l, AF_P2WPKH
|
|
||||||
if len(data) == 32:
|
|
||||||
return addr_l, AF_P2WSH
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Bitcoin main/test/reg base58 address prefixes.
|
|
||||||
elif addr and addr[0] in '123mn':
|
|
||||||
try:
|
|
||||||
raw = ngu.codecs.b58_decode(addr)
|
|
||||||
assert len(raw) == 21
|
|
||||||
if raw[0] == ch.b58_addr[0]:
|
|
||||||
return addr, AF_CLASSIC
|
|
||||||
if raw[0] == ch.b58_script[0]:
|
|
||||||
return addr, AF_P2SH
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
assert False, ch.name
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@ -349,7 +349,7 @@ async def show_qr_code(data, is_alnum=False, msg=None, **kw):
|
|||||||
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
|
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
|
||||||
await o.interact_bare()
|
await o.interact_bare()
|
||||||
|
|
||||||
async def ux_enter_bip32_index(prompt, can_cancel=True, unlimited=False):
|
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
|
||||||
if unlimited:
|
if unlimited:
|
||||||
max_value = (2 ** 31) - 1 # we handle hardened
|
max_value = (2 ** 31) - 1 # we handle hardened
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class PressRelease:
|
|||||||
return ch
|
return ch
|
||||||
|
|
||||||
|
|
||||||
async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
|
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
|
||||||
# return the decimal number which the user has entered
|
# return the decimal number which the user has entered
|
||||||
# - default/blank value assumed to be zero
|
# - default/blank value assumed to be zero
|
||||||
# - clamps large values to the max
|
# - clamps large values to the max
|
||||||
@ -283,7 +283,7 @@ async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100, min_
|
|||||||
ch = await press.wait()
|
ch = await press.wait()
|
||||||
if ch == 'y':
|
if ch == 'y':
|
||||||
if len(pw) < min_len:
|
if len(pw) < min_len:
|
||||||
ch = await ux_show_story('Need %d characters at least. Press OK '
|
ch = await ux_show_story('Need %d character(s) at least. Press OK '
|
||||||
'to continue X to exit.' % min_len, escape="xy",
|
'to continue X to exit.' % min_len, escape="xy",
|
||||||
strict_escape=True)
|
strict_escape=True)
|
||||||
if ch == "x": return
|
if ch == "x": return
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class PressRelease:
|
|||||||
self.last_key = ch
|
self.last_key = ch
|
||||||
return ch
|
return ch
|
||||||
|
|
||||||
async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
|
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
|
||||||
# return the decimal number which the user has entered
|
# return the decimal number which the user has entered
|
||||||
# - default/blank value assumed to be zero
|
# - default/blank value assumed to be zero
|
||||||
# - clamps large values to the max
|
# - clamps large values to the max
|
||||||
@ -121,7 +121,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
|
|||||||
dis.text(0, 4, ' '*CHARS_W)
|
dis.text(0, 4, ' '*CHARS_W)
|
||||||
elif ch == KEY_CANCEL:
|
elif ch == KEY_CANCEL:
|
||||||
if can_cancel:
|
if can_cancel:
|
||||||
# quit if they press CANCEL on any screen
|
# quit if they press X on empty screen
|
||||||
return None
|
return None
|
||||||
elif '0' <= ch <= '9':
|
elif '0' <= ch <= '9':
|
||||||
if len(value) == max_w:
|
if len(value) == max_w:
|
||||||
@ -578,7 +578,7 @@ def ux_draw_words(y, num_words, words):
|
|||||||
if num_words == 12:
|
if num_words == 12:
|
||||||
# luxious space after colon
|
# luxious space after colon
|
||||||
msg = ('%2d: ' % n) + word
|
msg = ('%2d: ' % n) + word
|
||||||
x_off = 4
|
x_off = 3
|
||||||
else:
|
else:
|
||||||
if n <= n_per_c:
|
if n <= n_per_c:
|
||||||
# no space in front of 1: thru N: in leftmost column of 3
|
# no space in front of 1: thru N: in leftmost column of 3
|
||||||
@ -667,7 +667,7 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None, li
|
|||||||
what, vals = decode_qr_result(got, expect_secret=True)
|
what, vals = decode_qr_result(got, expect_secret=True)
|
||||||
except QRDecodeExplained as e:
|
except QRDecodeExplained as e:
|
||||||
err_msg = str(e)
|
err_msg = str(e)
|
||||||
redraw_words(words)
|
redraw_words()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if what != "words":
|
if what != "words":
|
||||||
@ -825,6 +825,7 @@ class QRScannerInteraction:
|
|||||||
while 1:
|
while 1:
|
||||||
if task.done():
|
if task.done():
|
||||||
data = await task
|
data = await task
|
||||||
|
#print("Scanned: %r" % data)
|
||||||
break
|
break
|
||||||
|
|
||||||
dis.image(None, 40, 'scan_%d' % frames[ph])
|
dis.image(None, 40, 'scan_%d' % frames[ph])
|
||||||
@ -837,12 +838,7 @@ class QRScannerInteraction:
|
|||||||
data = None
|
data = None
|
||||||
break
|
break
|
||||||
|
|
||||||
if not task.done():
|
task.cancel()
|
||||||
task.cancel()
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# clear screen right away so user knows we got it
|
# clear screen right away so user knows we got it
|
||||||
dis.clear()
|
dis.clear()
|
||||||
@ -885,7 +881,7 @@ class QRScannerInteraction:
|
|||||||
file_type, _, data = decode_qr_result(got, expect_bbqr=True)
|
file_type, _, data = decode_qr_result(got, expect_bbqr=True)
|
||||||
if file_type == 'U':
|
if file_type == 'U':
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
if data[:1] == b'{' and data[-1:] == b'}':
|
if data[0] == '{' and data[-1] == '}':
|
||||||
file_type = 'J'
|
file_type = 'J'
|
||||||
if file_type != 'J':
|
if file_type != 'J':
|
||||||
raise QRDecodeExplained('Expected JSON data')
|
raise QRDecodeExplained('Expected JSON data')
|
||||||
@ -923,7 +919,7 @@ class QRScannerInteraction:
|
|||||||
return await self.scan_general(prompt, addr_taster, line2=line2, enter_quits=True)
|
return await self.scan_general(prompt, addr_taster, line2=line2, enter_quits=True)
|
||||||
|
|
||||||
|
|
||||||
async def scan_anything(self, expect_secret=False, tmp=False):
|
async def scan_anything(self, expect_secret=False, tmp=False, miniscript_wallet=None):
|
||||||
# start a QR scan, and act on what we find, whatever it may be.
|
# start a QR scan, and act on what we find, whatever it may be.
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
@ -982,7 +978,7 @@ class QRScannerInteraction:
|
|||||||
|
|
||||||
if what == 'psbt':
|
if what == 'psbt':
|
||||||
decoder, psbt_len, got = vals
|
decoder, psbt_len, got = vals
|
||||||
await qr_psbt_sign(decoder, psbt_len, got)
|
await qr_psbt_sign(decoder, psbt_len, got, miniscript_wallet)
|
||||||
|
|
||||||
elif what == 'txn':
|
elif what == 'txn':
|
||||||
bin_txn, = vals
|
bin_txn, = vals
|
||||||
@ -992,7 +988,7 @@ class QRScannerInteraction:
|
|||||||
proto, addr, args = vals
|
proto, addr, args = vals
|
||||||
await ux_visualize_bip21(proto, addr, args)
|
await ux_visualize_bip21(proto, addr, args)
|
||||||
|
|
||||||
elif what == "multi":
|
elif what == "minisc":
|
||||||
from auth import maybe_enroll_xpub
|
from auth import maybe_enroll_xpub
|
||||||
ms_config, = vals
|
ms_config, = vals
|
||||||
try:
|
try:
|
||||||
@ -1000,6 +996,7 @@ class QRScannerInteraction:
|
|||||||
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)))
|
||||||
|
return
|
||||||
|
|
||||||
elif what == "wif":
|
elif what == "wif":
|
||||||
data, = vals
|
data, = vals
|
||||||
@ -1033,7 +1030,7 @@ class QRScannerInteraction:
|
|||||||
await ux_show_story(what, title='Unhandled')
|
await ux_show_story(what, title='Unhandled')
|
||||||
|
|
||||||
|
|
||||||
async def qr_psbt_sign(decoder, psbt_len, raw):
|
async def qr_psbt_sign(decoder, psbt_len, raw, miniscript_wallet=None):
|
||||||
# Got a PSBT coming in from QR scanner. Sign it.
|
# Got a PSBT coming in from QR scanner. Sign it.
|
||||||
# - similar to auth.sign_psbt_file()
|
# - similar to auth.sign_psbt_file()
|
||||||
from auth import UserAuthorizedAction, ApproveTransaction
|
from auth import UserAuthorizedAction, ApproveTransaction
|
||||||
@ -1061,14 +1058,14 @@ async def qr_psbt_sign(decoder, psbt_len, raw):
|
|||||||
psbt_len = total
|
psbt_len = total
|
||||||
|
|
||||||
else:
|
else:
|
||||||
with SFFile(TXN_INPUT_OFFSET, length=psbt_len) as out:
|
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
|
||||||
taste = out.read(10)
|
taste = out.read(10)
|
||||||
_, output_encoder, _ = psbt_encoding_taster(taste, psbt_len)
|
_, output_encoder, _ = psbt_encoding_taster(taste, psbt_len)
|
||||||
|
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||||
psbt_len, input_method="qr",
|
psbt_len, input_method="qr", output_encoder=output_encoder,
|
||||||
output_encoder=output_encoder
|
miniscript_wallet=miniscript_wallet,
|
||||||
)
|
)
|
||||||
the_ux.push(UserAuthorizedAction.active_request)
|
the_ux.push(UserAuthorizedAction.active_request)
|
||||||
|
|
||||||
@ -1113,22 +1110,20 @@ async def ux_visualize_bip21(proto, addr, args):
|
|||||||
# - imho, a bare address is a valid BIP-21 URL so we come here too
|
# - imho, a bare address is a valid BIP-21 URL so we come here too
|
||||||
# - validate address ownership on request
|
# - validate address ownership on request
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
from chains import current_chain
|
|
||||||
|
|
||||||
msg = show_single_address(addr) + '\n\n'
|
msg = show_single_address(addr) + '\n\n'
|
||||||
args = args or {}
|
args = args or {}
|
||||||
|
|
||||||
if 'amount' in args:
|
if 'amount' in args:
|
||||||
|
msg += 'Amount: '
|
||||||
try:
|
try:
|
||||||
amt = args.pop('amount')
|
amt = args.pop('amount')
|
||||||
whole, _, frac = amt.partition('.')
|
whole, frac = amt.split('.', 1)
|
||||||
assert whole.isdigit()
|
frac = int(frac) if frac else 0
|
||||||
assert len(whole) <= 8
|
whole = int(whole) if whole else 0
|
||||||
assert len(frac) <= 8
|
msg += '%d.%08d BTC\n' % (whole, frac)
|
||||||
sats = int((whole or '0') + (frac + '00000000')[:8])
|
|
||||||
msg += 'Amount: %s %s\n' % current_chain().render_value(sats)
|
|
||||||
except:
|
except:
|
||||||
msg += 'Amount: (corrupt)\n'
|
msg += '(corrupt)\n'
|
||||||
|
|
||||||
for fn in ['label', 'message', 'lightning']:
|
for fn in ['label', 'message', 'lightning']:
|
||||||
if fn in args:
|
if fn in args:
|
||||||
@ -1205,6 +1200,10 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
|||||||
from ux import ux_wait_keydown
|
from ux import ux_wait_keydown
|
||||||
import uqr
|
import uqr
|
||||||
|
|
||||||
|
# put QR shenanigans at offset 1MB after TXN_OUTPUT_OFFSET
|
||||||
|
TMP_OFFSET = const(3 * 1024 * 1024)
|
||||||
|
|
||||||
|
assert not PSRAM.is_at(data, TMP_OFFSET) # output data would be overwritten with our work
|
||||||
assert type_code in TYPE_LABELS
|
assert type_code in TYPE_LABELS
|
||||||
|
|
||||||
dis.fullscreen('Generating BBQr...', .1)
|
dis.fullscreen('Generating BBQr...', .1)
|
||||||
@ -1215,11 +1214,6 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
|||||||
else:
|
else:
|
||||||
# default to Base32, because always best option
|
# default to Base32, because always best option
|
||||||
encoding = '2'
|
encoding = '2'
|
||||||
if isinstance(data, str):
|
|
||||||
# 'U'/'J' payloads are UTF-8 text; b32encode consumes the UTF-8
|
|
||||||
# bytes, so convert now to keep length/slicing consistent (else
|
|
||||||
# multi-byte chars overflow target_vers -> assert below trips)
|
|
||||||
data = data.encode()
|
|
||||||
data_len = len(data)
|
data_len = len(data)
|
||||||
|
|
||||||
# try a few select resolutions (sizes) in order such that we use either single QR
|
# try a few select resolutions (sizes) in order such that we use either single QR
|
||||||
@ -1266,7 +1260,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
|||||||
else:
|
else:
|
||||||
_, _, raw = qr_data.packed()
|
_, _, raw = qr_data.packed()
|
||||||
|
|
||||||
PSRAM.write_at(qr_size * pkt, qr_size)[0:raw_qr_size] = raw
|
PSRAM.write_at(TMP_OFFSET + (qr_size * pkt), qr_size)[0:raw_qr_size] = raw
|
||||||
|
|
||||||
del qr_data
|
del qr_data
|
||||||
|
|
||||||
@ -1282,7 +1276,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
|||||||
ch = None
|
ch = None
|
||||||
while not ch:
|
while not ch:
|
||||||
for pkt in range(num_parts):
|
for pkt in range(num_parts):
|
||||||
buf = PSRAM.read_at(qr_size * pkt, raw_qr_size)
|
buf = PSRAM.read_at(TMP_OFFSET + (qr_size * pkt), raw_qr_size)
|
||||||
dis.draw_qr_display( (scan_w, w, buf), msg, True, None, None, False,
|
dis.draw_qr_display( (scan_w, w, buf), msg, True, None, None, False,
|
||||||
partial_bar=((pkt, num_parts) if num_parts else None))
|
partial_bar=((pkt, num_parts) if num_parts else None))
|
||||||
|
|
||||||
|
|||||||
@ -131,6 +131,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')
|
||||||
|
|
||||||
probe_system()
|
probe_system()
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
1352
shared/wallet.py
1352
shared/wallet.py
File diff suppressed because it is too large
Load Diff
212
shared/wif.py
212
shared/wif.py
@ -9,10 +9,9 @@ from utils import problem_file_line, show_single_address, node_from_pubkey
|
|||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||||
from public_constants import AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2SH
|
from public_constants import AF_P2WPKH
|
||||||
from msgsign import msg_signing_done
|
from msgsign import msg_signing_done
|
||||||
|
|
||||||
MAX_ITEMS = 30
|
|
||||||
|
|
||||||
def decode_wif(wif):
|
def decode_wif(wif):
|
||||||
# Decode base58 encoded WIF string, return keypair and metadata
|
# Decode base58 encoded WIF string, return keypair and metadata
|
||||||
@ -34,7 +33,7 @@ def decode_wif(wif):
|
|||||||
return kp, testnet, compressed
|
return kp, testnet, compressed
|
||||||
|
|
||||||
|
|
||||||
def iter_wif_store_addresses(addr_fmt):
|
def iter_wif_store_addresses(chain, addr_fmt):
|
||||||
# nothing found among singlesig & registered multisig wallets
|
# nothing found among singlesig & registered multisig wallets
|
||||||
# check WIF store
|
# check WIF store
|
||||||
wifs = settings.get("wifs", [])
|
wifs = settings.get("wifs", [])
|
||||||
@ -42,37 +41,7 @@ def iter_wif_store_addresses(addr_fmt):
|
|||||||
|
|
||||||
for i, (pk, sk) in enumerate(wifs):
|
for i, (pk, sk) in enumerate(wifs):
|
||||||
node = node_from_pubkey(a2b_hex(pk))
|
node = node_from_pubkey(a2b_hex(pk))
|
||||||
yield i, chains.current_chain().address(node, addr_fmt)
|
yield i, chain.address(node, addr_fmt)
|
||||||
|
|
||||||
|
|
||||||
def save_wif_store_items(new_wifs):
|
|
||||||
saved = settings.get("wifs", [])
|
|
||||||
len_saved = len(saved)
|
|
||||||
unique = []
|
|
||||||
dups = 0
|
|
||||||
|
|
||||||
for item in new_wifs:
|
|
||||||
if item in unique:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if item not in saved:
|
|
||||||
unique.append(item)
|
|
||||||
else:
|
|
||||||
dups += 1
|
|
||||||
|
|
||||||
err = ("No valid WIF key found." + (" Contains duplicate WIF(s)" if dups else ""))
|
|
||||||
assert unique, err
|
|
||||||
|
|
||||||
err = ("Max %d items allowed in WIF Store.\n\nAttempted to import %d keys,"
|
|
||||||
" while remaining WIF store capacity is only %d. Please, make room"
|
|
||||||
" first." % (MAX_ITEMS, len(unique), MAX_ITEMS - len_saved))
|
|
||||||
assert (len_saved + len(unique)) <= MAX_ITEMS, err
|
|
||||||
|
|
||||||
saved.extend(unique)
|
|
||||||
settings.set('wifs', saved)
|
|
||||||
settings.save()
|
|
||||||
|
|
||||||
return len(unique)
|
|
||||||
|
|
||||||
|
|
||||||
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
||||||
@ -89,24 +58,27 @@ async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
|||||||
|
|
||||||
ch = await ux_show_story(msg, title="WIF Key", escape=esc)
|
ch = await ux_show_story(msg, title="WIF Key", escape=esc)
|
||||||
if ch == "1":
|
if ch == "1":
|
||||||
title = "Success"
|
saved = settings.get("wifs", [])
|
||||||
try:
|
if (pk, sk) in saved:
|
||||||
save_wif_store_items([(pk, sk)])
|
await ux_show_story("Already saved in WIF Store.", title="Failure")
|
||||||
msg = "Saved to WIF Store."
|
return
|
||||||
except Exception as e:
|
|
||||||
title = "Failure"
|
|
||||||
msg = str(e)
|
|
||||||
|
|
||||||
await ux_show_story(msg, title=title)
|
saved.append((pk, sk))
|
||||||
|
settings.set('wifs', saved)
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
await ux_show_story("Saved to WIF Store.", title="Success")
|
||||||
|
|
||||||
|
|
||||||
class WIFStoreMenu(MenuSystem):
|
class WIFStore(MenuSystem):
|
||||||
|
MAX_ITEMS = 30
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
items = self.construct()
|
items = self.construct()
|
||||||
super().__init__(items)
|
super().__init__(items)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def make(cls, *a):
|
async def make_menu(cls, *a):
|
||||||
if not settings.get("wifs", None):
|
if not settings.get("wifs", None):
|
||||||
intro = ("Individual private keys, encoded as WIF (Wallet Import Format) keys"
|
intro = ("Individual private keys, encoded as WIF (Wallet Import Format) keys"
|
||||||
" can be imported and used for signing. Any PSBT that uses a WIF stored here"
|
" can be imported and used for signing. Any PSBT that uses a WIF stored here"
|
||||||
@ -132,7 +104,7 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
if len(wifs) < MAX_ITEMS:
|
if len(wifs) < self.MAX_ITEMS:
|
||||||
items.append(MenuItem('Import WIF', f=self.import_wif, predicate=not_hobbled_mode))
|
items.append(MenuItem('Import WIF', f=self.import_wif, predicate=not_hobbled_mode))
|
||||||
|
|
||||||
a_items = []
|
a_items = []
|
||||||
@ -143,7 +115,6 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
|
|
||||||
submenu = [
|
submenu = [
|
||||||
MenuItem("Detail", f=self.detail, arg=(wif,pk,sk)),
|
MenuItem("Detail", f=self.detail, arg=(wif,pk,sk)),
|
||||||
MenuItem("Descriptors", f=self.show_desc_step1, arg=pk),
|
|
||||||
MenuItem("Addresses", f=self.show_addr_step1, arg=pk),
|
MenuItem("Addresses", f=self.show_addr_step1, arg=pk),
|
||||||
MenuItem("Sign MSG", f=self.sign_msg_step1, arg=sk),
|
MenuItem("Sign MSG", f=self.sign_msg_step1, arg=sk),
|
||||||
MenuItem('Delete', f=self.delete, arg=(i, pk), predicate=not_hobbled_mode),
|
MenuItem('Delete', f=self.delete, arg=(i, pk), predicate=not_hobbled_mode),
|
||||||
@ -173,51 +144,40 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
await export_contents(title, wif, "wif.txt", None, None,
|
await export_contents(title, wif, "wif.txt", None, None,
|
||||||
force_prompt=True, intro=msg, ux_title=title)
|
force_prompt=True, intro=msg, ux_title=title)
|
||||||
|
|
||||||
async def show_desc_step1(self, a, b, item):
|
|
||||||
rv = [
|
|
||||||
MenuItem(chains.addr_fmt_label(af), f=self.show_desc_step2, arg=(item.arg, af))
|
|
||||||
for af in chains.SINGLESIG_AF
|
|
||||||
]
|
|
||||||
the_ux.push(MenuSystem(rv))
|
|
||||||
|
|
||||||
async def show_desc_step2(self, a, b, item):
|
|
||||||
# allow to export pubkey, instead of main detail where WIF is exported
|
|
||||||
pk, af = item.arg
|
|
||||||
title = "Descriptor"
|
|
||||||
|
|
||||||
if af == AF_P2WPKH:
|
|
||||||
desc = "wpkh(%s)"
|
|
||||||
elif af == AF_CLASSIC:
|
|
||||||
desc = "pkh(%s)"
|
|
||||||
else:
|
|
||||||
assert af == AF_P2WPKH_P2SH
|
|
||||||
desc = "sh(wpkh(%s))"
|
|
||||||
|
|
||||||
from descriptor import append_checksum
|
|
||||||
desc = append_checksum(desc % pk)
|
|
||||||
|
|
||||||
from export import export_contents
|
|
||||||
await export_contents(title, desc, "wif_desc_%d.txt" % af, None, None,
|
|
||||||
force_prompt=True, intro=desc, ux_title=title)
|
|
||||||
|
|
||||||
async def show_addr_step1(self, a, b, item):
|
async def show_addr_step1(self, a, b, item):
|
||||||
|
pubkey = a2b_hex(item.arg)
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem(chains.addr_fmt_label(af), f=self.show_addr_step2, arg=(item.arg, af))
|
MenuItem(chains.addr_fmt_label(af), f=self.show_addr_step2, arg=(pubkey, af))
|
||||||
for af in chains.SINGLESIG_AF
|
for af in chains.SINGLESIG_AF
|
||||||
]
|
]
|
||||||
the_ux.push(MenuSystem(rv))
|
the_ux.push(MenuSystem(rv))
|
||||||
|
|
||||||
async def show_addr_step2(self, a, b, item):
|
async def show_addr_step2(self, a, b, item):
|
||||||
|
from glob import NFC
|
||||||
pubkey, af = item.arg
|
pubkey, af = item.arg
|
||||||
node = node_from_pubkey(a2b_hex(pubkey))
|
node = node_from_pubkey(pubkey)
|
||||||
addr = chains.current_chain().address(node, af)
|
addr = chains.current_chain().address(node, af)
|
||||||
msg = show_single_address(addr)
|
msg = show_single_address(addr) + "\n\n"
|
||||||
|
|
||||||
ux_title = chains.addr_fmt_label(af) if version.has_qwerty else None
|
escape = ""
|
||||||
|
# Q only hint keys
|
||||||
|
if not version.has_qwerty:
|
||||||
|
msg += "Press (1) to show address QR code."
|
||||||
|
escape += "1"
|
||||||
|
if NFC:
|
||||||
|
msg += "(3) to share via NFC."
|
||||||
|
escape += "3"
|
||||||
|
|
||||||
from export import export_contents
|
title = chains.addr_fmt_label(af) if version.has_qwerty else None
|
||||||
await export_contents("Address", addr, "wif_addr.txt", None, None,
|
while True:
|
||||||
force_prompt=True, intro=msg, ux_title=ux_title)
|
ch = await ux_show_story(msg, title=title, escape=escape,
|
||||||
|
hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
|
||||||
|
if ch == "x": return
|
||||||
|
if ch in "1"+KEY_QR:
|
||||||
|
await show_qr_code(addr, is_alnum=af == AF_P2WPKH)
|
||||||
|
|
||||||
|
elif NFC and (ch in "3"+KEY_NFC):
|
||||||
|
await NFC.share_text(addr)
|
||||||
|
|
||||||
async def sign_msg_step1(self, a, b, item):
|
async def sign_msg_step1(self, a, b, item):
|
||||||
privkey = a2b_hex(item.arg)
|
privkey = a2b_hex(item.arg)
|
||||||
@ -264,17 +224,16 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
return
|
return
|
||||||
|
|
||||||
idx, pubkey = item.arg
|
idx, pubkey = item.arg
|
||||||
wifs = settings.get('wifs', [])
|
wifs = settings.get('wifs', {})
|
||||||
if not wifs: return
|
if not wifs: return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entry = wifs[idx]
|
item = wifs[idx]
|
||||||
assert entry[0] == pubkey
|
assert item[0] == pubkey
|
||||||
del wifs[idx]
|
del wifs[idx]
|
||||||
settings.set('wifs', wifs)
|
settings.set('wifs', wifs)
|
||||||
settings.save()
|
settings.save()
|
||||||
except IndexError:
|
except IndexError: pass
|
||||||
return
|
|
||||||
|
|
||||||
the_ux.pop() # pop submenu
|
the_ux.pop() # pop submenu
|
||||||
self.update_contents()
|
self.update_contents()
|
||||||
@ -339,8 +298,12 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
# allow commas, spaces, and newlines as separators
|
# allow commas, spaces, and newlines as separators
|
||||||
got = got.replace(',', ' ').split()
|
got = got.replace(',', ' ').split()
|
||||||
|
|
||||||
|
saved = settings.get("wifs", [])
|
||||||
|
len_saved = len(saved)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_wifs = []
|
new_wifs = []
|
||||||
|
dups = 0
|
||||||
|
|
||||||
for here in got:
|
for here in got:
|
||||||
here = here.strip()
|
here = here.strip()
|
||||||
@ -359,10 +322,28 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
sk = b2a_hex(kp.privkey()).decode()
|
sk = b2a_hex(kp.privkey()).decode()
|
||||||
pk = b2a_hex(kp.pubkey().to_bytes()).decode()
|
pk = b2a_hex(kp.pubkey().to_bytes()).decode()
|
||||||
|
|
||||||
new_wifs.append((pk, sk))
|
item = (pk, sk)
|
||||||
|
if item in new_wifs:
|
||||||
|
# duplicate in import content
|
||||||
|
continue
|
||||||
|
|
||||||
save_wif_store_items(new_wifs)
|
if item in saved: # ignore dups
|
||||||
|
dups += 1
|
||||||
|
else:
|
||||||
|
new_wifs.append(item)
|
||||||
|
|
||||||
|
assert new_wifs, 'no valid WIF found' if not dups else 'duplicate WIF(s)'
|
||||||
|
|
||||||
|
if (len_saved + len(new_wifs)) > self.MAX_ITEMS:
|
||||||
|
await ux_show_story("Max %d items allowed in WIF Store.\n\nAttempted to import %d keys,"
|
||||||
|
" while remaining WIF store capacity is only %d. Please, make room"
|
||||||
|
" first." % (self.MAX_ITEMS, len(new_wifs), self.MAX_ITEMS - len_saved),
|
||||||
|
title="Failure")
|
||||||
|
return
|
||||||
|
|
||||||
|
saved.extend(new_wifs)
|
||||||
|
settings.set('wifs', saved)
|
||||||
|
settings.save()
|
||||||
self.update_contents()
|
self.update_contents()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -370,54 +351,13 @@ class WIFStoreMenu(MenuSystem):
|
|||||||
title="Failure")
|
title="Failure")
|
||||||
|
|
||||||
|
|
||||||
|
def init_wif_store():
|
||||||
class WIFStore:
|
# stored as hex strings, need load to bytes
|
||||||
def __init__(self):
|
wifs = settings.get('wifs', [])
|
||||||
wifs = settings.get('wifs', [])
|
if not wifs: return {}
|
||||||
self.wifs = [] # max 30 items, each (pubkey, privkey)
|
res = {}
|
||||||
for pk, sk in wifs:
|
for pk, sk in wifs:
|
||||||
self.wifs.append((a2b_hex(pk), a2b_hex(sk)))
|
res[a2b_hex(pk)] = a2b_hex(sk)
|
||||||
|
return res
|
||||||
# built lazily, on first match_address_hash() call
|
|
||||||
self._pkh = [] # hash160(pubkey) — P2PKH / P2WPKH
|
|
||||||
self._sh = [] # hash160(0014 || _pkh) — P2SH-P2WPKH
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return len(self.wifs) > 0
|
|
||||||
|
|
||||||
def __contains__(self, pubkey):
|
|
||||||
return self._privkey_for(pubkey) is not None
|
|
||||||
|
|
||||||
def __getitem__(self, pubkey):
|
|
||||||
sk = self._privkey_for(pubkey)
|
|
||||||
if sk is None: raise KeyError
|
|
||||||
return sk
|
|
||||||
|
|
||||||
def _privkey_for(self, pubkey):
|
|
||||||
for pk, sk in self.wifs:
|
|
||||||
if pk == pubkey:
|
|
||||||
return sk
|
|
||||||
|
|
||||||
def match_address_hash(self, addr_fmt, hash20):
|
|
||||||
if not self.wifs:
|
|
||||||
return None
|
|
||||||
if not self._pkh:
|
|
||||||
self._pkh = [ngu.hash.hash160(pk) for pk, _ in self.wifs]
|
|
||||||
|
|
||||||
if addr_fmt in (AF_P2WPKH, AF_CLASSIC):
|
|
||||||
table = self._pkh
|
|
||||||
elif addr_fmt == AF_P2SH:
|
|
||||||
if not self._sh:
|
|
||||||
self._sh = [ngu.hash.hash160(b'\x00\x14' + h) for h in self._pkh]
|
|
||||||
table = self._sh
|
|
||||||
else:
|
|
||||||
return None # AF_P2WSH / AF_P2TR / AF_BARE_PK / unknown — not us
|
|
||||||
|
|
||||||
try:
|
|
||||||
idx = table.index(hash20)
|
|
||||||
return idx, self.wifs[idx][0]
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -124,7 +124,7 @@ You have confirmed the details of the new split.''')
|
|||||||
# - stores encoded secret bytes (not word lists)
|
# - stores encoded secret bytes (not word lists)
|
||||||
import_xor_parts = []
|
import_xor_parts = []
|
||||||
|
|
||||||
async def xor_all_done(data, force_tmp, done_cb):
|
async def xor_all_done(data):
|
||||||
# So we have another part, might be done or not.
|
# So we have another part, might be done or not.
|
||||||
global import_xor_parts
|
global import_xor_parts
|
||||||
|
|
||||||
@ -178,9 +178,9 @@ async def xor_all_done(data, force_tmp, done_cb):
|
|||||||
if version.has_qwerty:
|
if version.has_qwerty:
|
||||||
from ux_q1 import seed_word_entry
|
from ux_q1 import seed_word_entry
|
||||||
await seed_word_entry("Part %s Words" % chr(65+len(import_xor_parts)),
|
await seed_word_entry("Part %s Words" % chr(65+len(import_xor_parts)),
|
||||||
target_words, done_cb=done_cb)
|
target_words, done_cb=xor_all_done)
|
||||||
else:
|
else:
|
||||||
nxt = XORWordNestMenu(num_words=target_words, done_cb=done_cb)
|
nxt = XORWordNestMenu(num_words=target_words, done_cb=xor_all_done)
|
||||||
the_ux.push(nxt)
|
the_ux.push(nxt)
|
||||||
|
|
||||||
elif ch == '2':
|
elif ch == '2':
|
||||||
@ -190,7 +190,7 @@ async def xor_all_done(data, force_tmp, done_cb):
|
|||||||
|
|
||||||
enc = SecretStash.encode(seed_phrase=seed)
|
enc = SecretStash.encode(seed_phrase=seed)
|
||||||
|
|
||||||
if pa.is_secret_blank() and not force_tmp:
|
if pa.is_secret_blank():
|
||||||
# save it since they have no other secret
|
# save it since they have no other secret
|
||||||
set_seed_value(encoded=enc)
|
set_seed_value(encoded=enc)
|
||||||
# update menu contents now that wallet defined
|
# update menu contents now that wallet defined
|
||||||
@ -239,7 +239,7 @@ async def show_n_parts(parts, chk_word):
|
|||||||
return await ux_show_story(msg, title="Record these:", sensitive=True, escape="4",
|
return await ux_show_story(msg, title="Record these:", sensitive=True, escape="4",
|
||||||
hint_icons=KEY_QR)
|
hint_icons=KEY_QR)
|
||||||
|
|
||||||
async def xor_restore_start(*a, force_tmp=False):
|
async def xor_restore_start(*a):
|
||||||
# shown on import menu when no seed of any kind yet
|
# shown on import menu when no seed of any kind yet
|
||||||
# - or operational system
|
# - or operational system
|
||||||
ch = await ux_show_story('''\
|
ch = await ux_show_story('''\
|
||||||
@ -261,9 +261,6 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
|
|||||||
global import_xor_parts
|
global import_xor_parts
|
||||||
import_xor_parts.clear()
|
import_xor_parts.clear()
|
||||||
|
|
||||||
async def done_cb(data):
|
|
||||||
return await xor_all_done(data, force_tmp=force_tmp, done_cb=done_cb)
|
|
||||||
|
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
@ -320,17 +317,14 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
|
|||||||
if selected:
|
if selected:
|
||||||
import_xor_parts += [opt[i][-1] for i in range(len(opt)) if i in selected]
|
import_xor_parts += [opt[i][-1] for i in range(len(opt)) if i in selected]
|
||||||
|
|
||||||
return await done_cb(None)
|
return await xor_all_done(None)
|
||||||
|
|
||||||
if version.has_qwerty:
|
if version.has_qwerty:
|
||||||
from ux_q1 import seed_word_entry
|
from ux_q1 import seed_word_entry
|
||||||
# if current loaded seed is added to xor - it is always A
|
# if current loaded seed is added to xor - it is always A
|
||||||
await seed_word_entry("Part %s Words" % (chr(65+len(import_xor_parts))),
|
await seed_word_entry("Part %s Words" % (chr(65+len(import_xor_parts))),
|
||||||
desired_num_words, done_cb=done_cb)
|
desired_num_words, done_cb=xor_all_done)
|
||||||
else:
|
else:
|
||||||
return XORWordNestMenu(num_words=desired_num_words, done_cb=done_cb)
|
return XORWordNestMenu(num_words=desired_num_words, done_cb=xor_all_done)
|
||||||
|
|
||||||
async def xor_restore_temporary(*a):
|
|
||||||
return await xor_restore_start(*a, force_tmp=True)
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
//
|
//
|
||||||
// AUTO-generated.
|
// AUTO-generated.
|
||||||
//
|
//
|
||||||
// built: 2026-03-05
|
// built: 2026-03-23
|
||||||
// version: 5.5.0
|
// version: 6.5.0X
|
||||||
//
|
//
|
||||||
#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 0x5c6528a0UL;
|
return 0x5c7730a0UL;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
//
|
//
|
||||||
// AUTO-generated.
|
// AUTO-generated.
|
||||||
//
|
//
|
||||||
// built: 2026-03-05
|
// built: 2026-03-23
|
||||||
// version: 1.4.0Q
|
// version: 6.5.0QX
|
||||||
//
|
//
|
||||||
#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 0x5c650880UL;
|
return 0x5c7730a0UL;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ LATEST_RELEASE = $(shell ls -t1 ../releases/*-mk-*.dfu ../releases/*-mk4-*.dfu |
|
|||||||
|
|
||||||
# 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.5.0
|
VERSION_STRING = 6.5.0X
|
||||||
|
|
||||||
# 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.4.0Q
|
VERSION_STRING = 6.5.0QX
|
||||||
|
|
||||||
# Remove this closer to shipping.
|
# Remove this closer to shipping.
|
||||||
#$(warning "Forcing debug build")
|
#$(warning "Forcing debug build")
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
||||||
#
|
|
||||||
# Capture current (mainnet) block height for SSSP/CCC features
|
|
||||||
#
|
|
||||||
import sys, time, datetime
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
|
|
||||||
FILE_NAME = "../shared/block_height.py"
|
|
||||||
|
|
||||||
|
|
||||||
def _get_block_height(url):
|
|
||||||
with urllib.request.urlopen(url) as response:
|
|
||||||
height_data = response.read().decode().strip()
|
|
||||||
return int(height_data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_block_height(url):
|
|
||||||
try:
|
|
||||||
return _get_block_height(url)
|
|
||||||
except:
|
|
||||||
time.sleep(2)
|
|
||||||
return _get_block_height(url)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_block_height_file():
|
|
||||||
with open(FILE_NAME, "r") as f:
|
|
||||||
for l in f.readlines():
|
|
||||||
if l.startswith("BLOCK_HEIGHT ="):
|
|
||||||
return int(l.split("=")[-1].strip())
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def write_block_height_file(block_height):
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
with open(FILE_NAME, "wt") as f:
|
|
||||||
f.write('''\
|
|
||||||
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
||||||
#
|
|
||||||
# AUTO-generated.
|
|
||||||
#
|
|
||||||
|
|
||||||
# As of %s UTC
|
|
||||||
BLOCK_HEIGHT = %d
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
''' % (now.strftime("%Y-%m-%d %H:%M:%S"), block_height))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
current_height = None
|
|
||||||
for _ in range(2):
|
|
||||||
bh_a = get_block_height("https://mempool.space/api/blocks/tip/height")
|
|
||||||
bh_b = get_block_height("https://blockstream.info/api/blocks/tip/height")
|
|
||||||
if bh_a == bh_b:
|
|
||||||
current_height = bh_a
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
if current_height is None:
|
|
||||||
raise RuntimeError("Could not get current block height")
|
|
||||||
|
|
||||||
file_block_height = parse_block_height_file()
|
|
||||||
if file_block_height is None:
|
|
||||||
raise RuntimeError("Could not parse block height from file")
|
|
||||||
|
|
||||||
if current_height > file_block_height:
|
|
||||||
write_block_height_file(current_height)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
# EOF
|
|
||||||
@ -5,14 +5,14 @@
|
|||||||
# Capture build time and version number into a number used as the timestamp on
|
# Capture build time and version number into a number used as the timestamp on
|
||||||
# all created files for that Coldcard version.
|
# all created files for that Coldcard version.
|
||||||
#
|
#
|
||||||
import os, sys, datetime
|
import os, sys, time, datetime
|
||||||
|
|
||||||
out_fname, version = sys.argv[1:]
|
out_fname, version = sys.argv[1:]
|
||||||
|
|
||||||
assert out_fname.endswith('.c'), out_fname
|
assert out_fname.endswith('.c'), out_fname
|
||||||
|
|
||||||
if os.path.exists(out_fname):
|
if os.path.exists(out_fname):
|
||||||
# to help deterministic builds, don't replace the file from git if version # is right
|
# to help deterministic builds, don't replace the file from git if verison # is right
|
||||||
with open(out_fname, 'rt') as fd:
|
with open(out_fname, 'rt') as fd:
|
||||||
if ('// version: %s\n' % version) in fd.read():
|
if ('// version: %s\n' % version) in fd.read():
|
||||||
print("==> %s already version %s; not changing it" % (out_fname, version))
|
print("==> %s already version %s; not changing it" % (out_fname, version))
|
||||||
@ -22,7 +22,7 @@ if os.path.exists(out_fname):
|
|||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
value = ((today.year - 1980) << 25) | (today.month << 21) | (today.day << 16)
|
value = ((today.year - 1980) << 25) | (today.month << 21) | (today.day << 16)
|
||||||
|
|
||||||
# only 2second resolution for times, so can only support minor version up to x.x.5 and hard to see
|
# only 2second resolution for times, so can only support minor verion up to x.x.5 and hard to see
|
||||||
# anyway, let's omit ... worst case, use the date instead
|
# anyway, let's omit ... worst case, use the date instead
|
||||||
ver = ''.join(v for v in version if v in '0123456789.') # strip letter codes from end
|
ver = ''.join(v for v in version if v in '0123456789.') # strip letter codes from end
|
||||||
h, m, _ = [int(x) for x in ver.split('b')[0].split('.')]
|
h, m, _ = [int(x) for x in ver.split('b')[0].split('.')]
|
||||||
|
|||||||
@ -82,18 +82,6 @@ $(BOARD)/file_time.c: make_filetime.py *-Makefile shared.mk
|
|||||||
./make_filetime.py $(BOARD)/file_time.c $(VERSION_STRING)
|
./make_filetime.py $(BOARD)/file_time.c $(VERSION_STRING)
|
||||||
cp $(BOARD)/file_time.c .
|
cp $(BOARD)/file_time.c .
|
||||||
|
|
||||||
|
|
||||||
.PHONY: block_height
|
|
||||||
|
|
||||||
block_height:
|
|
||||||
@python3 make_block_height.py; \
|
|
||||||
if [ $$? -eq 0 ]; then \
|
|
||||||
echo "Block Height file already up-to-date."; \
|
|
||||||
else \
|
|
||||||
echo "Block Height file updated."; \
|
|
||||||
git commit -m "update block height" ../shared/block_height.py; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make a factory release: using key #1
|
# Make a factory release: using key #1
|
||||||
# - when executed in a repro w/o the required key, it defaults to key zero
|
# - when executed in a repro w/o the required key, it defaults to key zero
|
||||||
# - and that's what happens inside the Docker build
|
# - and that's what happens inside the Docker build
|
||||||
@ -103,7 +91,7 @@ production.bin: firmware-signed.bin Makefile
|
|||||||
SUBMAKE = $(MAKE) -f $(PARENT_MKFILE)
|
SUBMAKE = $(MAKE) -f $(PARENT_MKFILE)
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: submods-match code-committed block_height
|
release: submods-match code-committed
|
||||||
$(SUBMAKE) clean
|
$(SUBMAKE) clean
|
||||||
$(SUBMAKE) repro
|
$(SUBMAKE) repro
|
||||||
test -f built/production.bin
|
test -f built/production.bin
|
||||||
@ -130,7 +118,7 @@ rc1:
|
|||||||
rc2: RC2_TIMESTAMP = $(shell date "+%F_%H%M")
|
rc2: RC2_TIMESTAMP = $(shell date "+%F_%H%M")
|
||||||
rc2: RC2_FNAME = ./RC2-$(RC2_TIMESTAMP)-$(HW_MODEL)-coldcard.dfu
|
rc2: RC2_FNAME = ./RC2-$(RC2_TIMESTAMP)-$(HW_MODEL)-coldcard.dfu
|
||||||
rc2: RC2_FNAME_FACT = ./RC2-$(RC2_TIMESTAMP)-$(HW_MODEL)-factory.dfu
|
rc2: RC2_FNAME_FACT = ./RC2-$(RC2_TIMESTAMP)-$(HW_MODEL)-factory.dfu
|
||||||
rc2: submods-match code-committed block_height
|
rc2: submods-match code-committed
|
||||||
$(SUBMAKE) clean
|
$(SUBMAKE) clean
|
||||||
$(SUBMAKE) repro
|
$(SUBMAKE) repro
|
||||||
test -f built/production.bin
|
test -f built/production.bin
|
||||||
@ -151,9 +139,11 @@ release-products: built/production.bin
|
|||||||
-git commit $(BOARD)/file_time.c -m "For $(NEW_VERSION)"
|
-git commit $(BOARD)/file_time.c -m "For $(NEW_VERSION)"
|
||||||
$(SIGNIT) sign -m $(HW_MODEL) $(VERSION_STRING) -r built/production.bin $(PROD_KEYNUM) -o built/production.bin
|
$(SIGNIT) sign -m $(HW_MODEL) $(VERSION_STRING) -r built/production.bin $(PROD_KEYNUM) -o built/production.bin
|
||||||
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin $(RELEASE_FNAME)
|
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin $(RELEASE_FNAME)
|
||||||
|
ifeq ($(findstring X,$(NEW_VERSION)),)
|
||||||
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin \
|
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin \
|
||||||
-b $(BOOTLOADER_BASE):$(BOOTLOADER_DIR)/releases/$(BOOTLOADER_VERSION)/bootloader.bin \
|
-b $(BOOTLOADER_BASE):$(BOOTLOADER_DIR)/releases/$(BOOTLOADER_VERSION)/bootloader.bin \
|
||||||
$(RELEASE_FNAME:%.dfu=%-factory.dfu)
|
$(RELEASE_FNAME:%.dfu=%-factory.dfu)
|
||||||
|
endif
|
||||||
@echo
|
@echo
|
||||||
@echo 'Made release: ' $(RELEASE_FNAME)
|
@echo 'Made release: ' $(RELEASE_FNAME)
|
||||||
@echo
|
@echo
|
||||||
|
|||||||
@ -11,6 +11,10 @@ from ckcc.protocol import CCProtocolPacker
|
|||||||
def find_bitcoind():
|
def find_bitcoind():
|
||||||
# search for the binary we need
|
# search for the binary we need
|
||||||
# - should be in the path really
|
# - should be in the path really
|
||||||
|
env_path = os.environ.get("CC_TEST_BITCOIND", None)
|
||||||
|
if env_path:
|
||||||
|
return env_path
|
||||||
|
|
||||||
easy = shutil.which('bitcoind')
|
easy = shutil.which('bitcoind')
|
||||||
if easy:
|
if easy:
|
||||||
return easy
|
return easy
|
||||||
@ -60,10 +64,10 @@ class Bitcoind:
|
|||||||
"-noprinttoconsole",
|
"-noprinttoconsole",
|
||||||
"-fallbackfee=0.0002",
|
"-fallbackfee=0.0002",
|
||||||
"-server=1",
|
"-server=1",
|
||||||
"-keypool=1",
|
|
||||||
"-listen=0",
|
"-listen=0",
|
||||||
|
"-keypool=1",
|
||||||
f"-port={self.p2p_port}",
|
f"-port={self.p2p_port}",
|
||||||
f"-rpcport={self.rpc_port}",
|
f"-rpcport={self.rpc_port}"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
signal.signal(signal.SIGTERM, self.cleanup)
|
signal.signal(signal.SIGTERM, self.cleanup)
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||||
#
|
#
|
||||||
import hashlib, hmac, bech32
|
import hashlib, hmac, bech32, os
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from io import BytesIO
|
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
|
||||||
@ -18,6 +19,7 @@ except ImportError:
|
|||||||
|
|
||||||
from helpers import hash160, str_to_path
|
from helpers import hash160, str_to_path
|
||||||
from base58 import encode_base58_checksum, decode_base58_checksum
|
from base58 import encode_base58_checksum, decode_base58_checksum
|
||||||
|
from constants import BIP_341_H
|
||||||
|
|
||||||
HARDENED = 2 ** 31
|
HARDENED = 2 ** 31
|
||||||
|
|
||||||
@ -119,6 +121,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 +199,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 +242,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 +255,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 +738,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 +766,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)
|
||||||
@ -752,3 +788,21 @@ class BIP32Node:
|
|||||||
|
|
||||||
def parent_fingerprint(self):
|
def parent_fingerprint(self):
|
||||||
return self.node.parent_fingerprint
|
return self.node.parent_fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
def ranged_unspendable_internal_key(chain_code=32 * b"\x01", subderiv="/<0;1>/*"):
|
||||||
|
# provide ranged provably unspendable key in serialized extended key format for core to understand it
|
||||||
|
# core does NOT understand 'unspend('
|
||||||
|
pk = b"\x02" + bytes.fromhex(BIP_341_H)
|
||||||
|
node = BIP32Node.from_chaincode_pubkey(chain_code, pk)
|
||||||
|
return node.hwif() + subderiv
|
||||||
|
|
||||||
|
|
||||||
|
def random_keys(num_keys, path="86h/1h/0h"):
|
||||||
|
keys = []
|
||||||
|
for _ in range(num_keys):
|
||||||
|
k = BIP32Node.from_master_secret(os.urandom(32))
|
||||||
|
key = f"[{k.fingerprint().hex()}/{path}]{k.subkey_for_path(path).hwif()}"
|
||||||
|
keys.append(key)
|
||||||
|
|
||||||
|
return keys
|
||||||
@ -2,18 +2,14 @@
|
|||||||
#
|
#
|
||||||
# construct Proof of Reserves transaction according to BIP-322
|
# construct Proof of Reserves transaction according to BIP-322
|
||||||
#
|
#
|
||||||
import struct, hashlib
|
import pytest, struct, hashlib
|
||||||
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 hash160, str_to_path, taptweak
|
from helpers import hash160, taptweak, str_to_path
|
||||||
from bip32 import BIP32Node, PublicKey
|
from bip32 import BIP32Node, PublicKey
|
||||||
from constants import simulator_fixed_tprv, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
from constants import simulator_fixed_tprv, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
||||||
from ctransaction import CTransaction, COutPoint, CTxIn, CTxOut, uint256_from_str
|
from ctransaction import CTransaction, COutPoint, CTxIn, CTxOut, uint256_from_str
|
||||||
from sighash import legacy_sighash, segwit_v0_sighash, taproot_sighash, SIGHASH_DEFAULT, SIGHASH_ALL
|
|
||||||
from pysecp256k1 import ec_pubkey_parse, ecdsa_signature_parse_der, ecdsa_verify
|
|
||||||
from pysecp256k1.extrakeys import xonly_pubkey_parse
|
|
||||||
from pysecp256k1.schnorrsig import schnorrsig_verify
|
|
||||||
|
|
||||||
|
|
||||||
def bip322_msg_hash(msg):
|
def bip322_msg_hash(msg):
|
||||||
@ -21,402 +17,302 @@ def bip322_msg_hash(msg):
|
|||||||
return hashlib.sha256(tag_hash + tag_hash + msg).digest()
|
return hashlib.sha256(tag_hash + tag_hash + msg).digest()
|
||||||
|
|
||||||
|
|
||||||
def ecdsa_verify_sig(pubkey, sig, digest):
|
@pytest.fixture
|
||||||
if not sig or sig[-1] != SIGHASH_ALL:
|
def create_msg_file(sim_root_dir, garbage_collector):
|
||||||
return False
|
|
||||||
try:
|
def doit(msg, msg_hash):
|
||||||
parsed = ecdsa_signature_parse_der(sig[:-1])
|
# carelessly overwrites
|
||||||
return bool(ecdsa_verify(parsed, ec_pubkey_parse(pubkey), digest))
|
fpath = f"{sim_root_dir}/MicroSD/{msg_hash.hex()}.txt"
|
||||||
except Exception:
|
with open(fpath, "w") as f:
|
||||||
return False
|
f.write(msg.decode())
|
||||||
|
garbage_collector.append(fpath)
|
||||||
|
|
||||||
|
return doit
|
||||||
|
|
||||||
|
|
||||||
def bip322_verify(psbt_bytes):
|
@pytest.fixture
|
||||||
"""Verify BIP-322 PSBT signatures without a full script interpreter.
|
def bip322_txn(dev, pytestconfig, create_msg_file):
|
||||||
|
|
||||||
Enforces the BIP-322 transaction shape, SIGHASH_ALL for ECDSA,
|
def doit(inputs, msg=b"POR", addr_fmt="p2wpkh", input_amount=1E8, to_sign_lock_time=0,
|
||||||
SIGHASH_DEFAULT/SIGHASH_ALL for taproot, and direct signature checks for
|
sighash=None, psbt_hacker=None, witness_utxo=[], to_sign_nVersion=0):
|
||||||
p2pkh, p2wpkh, p2sh-p2wpkh, sh, wsh, and p2tr key-path.
|
|
||||||
It intentionally omits consensus-level script evaluation rules such as
|
|
||||||
CLEANSTACK, MINIMALIF, NULLFAIL beyond empty CHECKMULTISIG dummy,
|
|
||||||
CODESEPARATOR/FindAndDelete handling, and NOP-upgrade checks; unsupported
|
|
||||||
scripts raise AssertionError.
|
|
||||||
"""
|
|
||||||
psbt = BasicPSBT().parse(psbt_bytes)
|
|
||||||
assert psbt.bip322_msg is not None
|
|
||||||
msg = psbt.bip322_msg
|
|
||||||
tx = CTransaction()
|
|
||||||
if psbt.txn:
|
|
||||||
tx.deserialize(BytesIO(psbt.txn))
|
|
||||||
else:
|
|
||||||
tx.nVersion = psbt.txn_version
|
|
||||||
tx.nLockTime = psbt.fallback_locktime or 0
|
|
||||||
for inp in psbt.inputs:
|
|
||||||
tx.vin.append(CTxIn(COutPoint(uint256_from_str(inp.previous_txid), inp.prevout_idx),
|
|
||||||
nSequence=inp.sequence if inp.sequence is not None else 0xffffffff))
|
|
||||||
for out in psbt.outputs:
|
|
||||||
tx.vout.append(CTxOut(out.amount, out.script))
|
|
||||||
|
|
||||||
inp0 = psbt.inputs[0]
|
msg_challenge = None
|
||||||
to_spend = None
|
|
||||||
if inp0.utxo:
|
|
||||||
to_spend = CTransaction()
|
|
||||||
to_spend.deserialize(BytesIO(inp0.utxo))
|
|
||||||
assert len(to_spend.vout) == 1
|
|
||||||
assert to_spend.vout[0].nValue == 0
|
|
||||||
script_pubkey = to_spend.vout[0].scriptPubKey
|
|
||||||
else:
|
|
||||||
assert inp0.witness_utxo
|
|
||||||
witness_utxo = CTxOut()
|
|
||||||
witness_utxo.deserialize(BytesIO(inp0.witness_utxo))
|
|
||||||
assert witness_utxo.nValue == 0
|
|
||||||
script_pubkey = witness_utxo.scriptPubKey
|
|
||||||
|
|
||||||
expected_to_spend = CTransaction()
|
num_ins = len(inputs)
|
||||||
expected_to_spend.nVersion = 0
|
|
||||||
expected_to_spend.nLockTime = 0
|
|
||||||
expected_to_spend.vin = [CTxIn(COutPoint(hash=0, n=0xffffffff),
|
|
||||||
scriptSig=b'\x00\x20' + bip322_msg_hash(msg),
|
|
||||||
nSequence=0)]
|
|
||||||
expected_to_spend.vout = [CTxOut(0, script_pubkey)]
|
|
||||||
expected_to_spend.calc_sha256()
|
|
||||||
if to_spend:
|
|
||||||
assert to_spend.serialize_without_witness() == expected_to_spend.serialize_without_witness()
|
|
||||||
to_spend = expected_to_spend
|
|
||||||
|
|
||||||
assert tx.nVersion in (0, 2)
|
psbt = BasicPSBT()
|
||||||
assert len(tx.vin) >= 1
|
|
||||||
assert tx.vin[0].prevout.hash == to_spend.sha256
|
|
||||||
assert tx.vin[0].prevout.n == 0
|
|
||||||
assert not (len(tx.vin) == 1 and (tx.vin[0].nSequence != 0 or tx.nLockTime != 0))
|
|
||||||
assert len(tx.vout) == 1
|
|
||||||
assert tx.vout[0].nValue == 0
|
|
||||||
assert tx.vout[0].scriptPubKey == b'\x6a'
|
|
||||||
|
|
||||||
prevouts = []
|
to_sign = CTransaction()
|
||||||
for idx, txin in enumerate(tx.vin):
|
to_sign.nLockTime = to_sign_lock_time
|
||||||
if idx == 0:
|
# must be set to 2 if BIP-68 is used (relative tx level lock)
|
||||||
prevouts.append((0, script_pubkey))
|
to_sign.nVersion = to_sign_nVersion
|
||||||
else:
|
master_xpub = dev.master_xpub or simulator_fixed_tprv
|
||||||
assert idx < len(psbt.inputs)
|
|
||||||
if psbt.inputs[idx].witness_utxo:
|
# we have a key; use it to provide "plausible" value inputs
|
||||||
prev = CTxOut()
|
mk = BIP32Node.from_wallet_key(master_xpub)
|
||||||
prev.deserialize(BytesIO(psbt.inputs[idx].witness_utxo))
|
mfp = mk.fingerprint()
|
||||||
|
|
||||||
|
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
|
||||||
|
psbt.outputs = []
|
||||||
|
|
||||||
|
for i, inp in enumerate(inputs):
|
||||||
|
sp = f"0/{i}"
|
||||||
|
af = addr_fmt
|
||||||
|
ia = input_amount
|
||||||
|
pubkey = None # public key
|
||||||
|
try:
|
||||||
|
if inp[0] is not None:
|
||||||
|
af = inp[0]
|
||||||
|
if inp[1] is not None:
|
||||||
|
sp = inp[1]
|
||||||
|
if inp[2] is not None:
|
||||||
|
ia = inp[2]
|
||||||
|
if inp[3] is not None:
|
||||||
|
pubkey = inp[3]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if pubkey:
|
||||||
|
int_path = [0]
|
||||||
|
sec = pubkey
|
||||||
else:
|
else:
|
||||||
prev_tx = CTransaction()
|
int_path = str_to_path(sp)
|
||||||
prev_tx.deserialize(BytesIO(psbt.inputs[idx].utxo))
|
sec = mk.subkey_for_path(sp).sec()
|
||||||
prev = prev_tx.vout[txin.prevout.n]
|
|
||||||
prevouts.append((prev.nValue, prev.scriptPubKey))
|
|
||||||
|
|
||||||
for idx, txin in enumerate(tx.vin):
|
subkey = PublicKey.parse(sec)
|
||||||
amount, spk = prevouts[idx]
|
|
||||||
|
|
||||||
inp = psbt.inputs[idx]
|
assert len(sec) == 33, "expect compressed"
|
||||||
if len(spk) == 25 and spk[:3] == b'\x76\xa9\x14' and spk[-2:] == b'\x88\xac':
|
|
||||||
assert len(inp.part_sigs) == 1
|
|
||||||
pub, sig = next(iter(inp.part_sigs.items()))
|
|
||||||
assert hash160(pub) == spk[3:23]
|
|
||||||
assert ecdsa_verify_sig(pub, sig, legacy_sighash(tx, idx, spk))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(spk) == 22 and spk[:2] == b'\x00\x14':
|
if af == "p2tr":
|
||||||
assert len(inp.part_sigs) == 1
|
tweaked_xonly = taptweak(sec[1:])
|
||||||
pub, sig = next(iter(inp.part_sigs.items()))
|
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + mfp + struct.pack(f'<{"I" * len(int_path)}',
|
||||||
assert hash160(pub) == spk[2:22]
|
*int_path)
|
||||||
script_code = b'\x76\xa9\x14' + spk[2:22] + b'\x88\xac'
|
scr = bytes([81, 32]) + tweaked_xonly
|
||||||
assert ecdsa_verify_sig(pub, sig, segwit_v0_sighash(tx, idx, script_code, amount))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(spk) == 34 and spk[:2] == b'\x00\x20':
|
elif af in ("p2wpkh", "p2sh-p2wpkh", "p2wpkh-p2sh"):
|
||||||
assert inp.witness_script
|
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
|
||||||
assert hashlib.sha256(inp.witness_script).digest() == spk[2:34]
|
scr = bytes([0x00, 0x14]) + subkey.h160()
|
||||||
assert inp.part_sigs
|
|
||||||
sighash = segwit_v0_sighash(tx, idx, inp.witness_script, amount)
|
if af != "p2wpkh":
|
||||||
for pub, sig in inp.part_sigs.items():
|
# use classic p2wpkh (from above) as redeem script
|
||||||
assert ecdsa_verify_sig(pub, sig, sighash)
|
psbt.inputs[i].redeem_script = scr
|
||||||
continue
|
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
|
||||||
|
|
||||||
|
elif af == "p2pkh":
|
||||||
|
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
|
||||||
|
scr = bytes([0x76, 0xa9, 0x14]) + subkey.h160() + bytes([0x88, 0xac])
|
||||||
|
|
||||||
if len(spk) == 34 and spk[:2] == b'\x51\x20':
|
|
||||||
assert inp.taproot_key_sig
|
|
||||||
if len(inp.taproot_key_sig) == 64:
|
|
||||||
sighash = SIGHASH_DEFAULT
|
|
||||||
sig = inp.taproot_key_sig
|
|
||||||
else:
|
else:
|
||||||
assert len(inp.taproot_key_sig) == 65
|
raise ValueError("unknown addr_fmt %s" % af)
|
||||||
sighash = inp.taproot_key_sig[-1]
|
|
||||||
sig = inp.taproot_key_sig[:-1]
|
|
||||||
digest = taproot_sighash(tx, idx, prevouts, sighash)
|
|
||||||
assert schnorrsig_verify(sig, digest, xonly_pubkey_parse(spk[2:34]))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(spk) == 23 and spk[:2] == b'\xa9\x14' and spk[-1:] == b'\x87':
|
if i == 0:
|
||||||
assert inp.redeem_script
|
# first input always spends to_spend
|
||||||
assert hash160(inp.redeem_script) == spk[2:22]
|
to_spend = CTransaction()
|
||||||
|
to_spend.nVersion = 0
|
||||||
if len(inp.redeem_script) == 22 and inp.redeem_script[:2] == b'\x00\x14':
|
out_point = COutPoint(hash=0, n=0xffffffff)
|
||||||
assert len(inp.part_sigs) == 1
|
msg_hash = bip322_msg_hash(msg)
|
||||||
pub, sig = next(iter(inp.part_sigs.items()))
|
create_msg_file(msg, msg_hash)
|
||||||
assert hash160(pub) == inp.redeem_script[2:22]
|
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
||||||
script_code = b'\x76\xa9\x14' + inp.redeem_script[2:22] + b'\x88\xac'
|
to_spend.vout = [CTxOut(0, scr)] # always zero val
|
||||||
assert ecdsa_verify_sig(pub, sig, segwit_v0_sighash(tx, idx, script_code, amount))
|
msg_challenge = scr
|
||||||
continue
|
else:
|
||||||
|
# other outputs that we want to prove ownership
|
||||||
if len(inp.redeem_script) == 34 and inp.redeem_script[:2] == b'\x00\x20':
|
to_spend = CTransaction()
|
||||||
assert inp.witness_script
|
to_spend.nVersion = 0
|
||||||
assert inp.redeem_script == b'\x00\x20' + hashlib.sha256(inp.witness_script).digest()
|
out_point = COutPoint(
|
||||||
assert inp.part_sigs
|
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
|
||||||
sighash = segwit_v0_sighash(tx, idx, inp.witness_script, amount)
|
73
|
||||||
for pub, sig in inp.part_sigs.items():
|
)
|
||||||
assert ecdsa_verify_sig(pub, sig, sighash)
|
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
|
||||||
continue
|
to_spend.vout.append(CTxOut(int(ia), scr))
|
||||||
|
|
||||||
assert inp.part_sigs
|
|
||||||
sighash = legacy_sighash(tx, idx, inp.redeem_script)
|
|
||||||
for pub, sig in inp.part_sigs.items():
|
|
||||||
assert ecdsa_verify_sig(pub, sig, sighash)
|
|
||||||
continue
|
|
||||||
|
|
||||||
assert False, "unsupported script"
|
|
||||||
|
|
||||||
|
|
||||||
def bip322_txn(inputs, msg=b"POR", addr_fmt="p2wpkh", input_amount=1E8, to_sign_lock_time=0,
|
if sighash is not None:
|
||||||
sighash=None, psbt_hacker=None, witness_utxo=[], to_sign_nVersion=0,
|
psbt.inputs[i].sighash = sighash
|
||||||
psbt_v2=False, master_xpub=None):
|
|
||||||
|
|
||||||
msg_challenge = None
|
to_spend.calc_sha256()
|
||||||
|
|
||||||
num_ins = len(inputs)
|
if i in witness_utxo:
|
||||||
|
psbt.inputs[i].witness_utxo = to_spend.vout[-1].serialize()
|
||||||
|
else:
|
||||||
|
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
|
||||||
|
|
||||||
psbt = BasicPSBT()
|
|
||||||
psbt.bip322_msg = msg
|
|
||||||
|
|
||||||
to_sign = CTransaction()
|
|
||||||
to_sign.nLockTime = to_sign_lock_time
|
|
||||||
# must be set to 2 if BIP-68 is used (relative tx level lock)
|
|
||||||
to_sign.nVersion = to_sign_nVersion
|
|
||||||
master_xpub = master_xpub or simulator_fixed_tprv
|
|
||||||
|
|
||||||
# we have a key; use it to provide "plausible" value inputs
|
|
||||||
mk = BIP32Node.from_wallet_key(master_xpub)
|
|
||||||
mfp = mk.fingerprint()
|
|
||||||
|
|
||||||
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
|
|
||||||
psbt.outputs = []
|
|
||||||
|
|
||||||
for i, inp in enumerate(inputs):
|
|
||||||
sp = f"0/{i}"
|
|
||||||
af = addr_fmt
|
|
||||||
ia = input_amount
|
|
||||||
pubkey = None # public key
|
|
||||||
try:
|
|
||||||
if inp[0] is not None:
|
|
||||||
af = inp[0]
|
|
||||||
if inp[1] is not None:
|
|
||||||
sp = inp[1]
|
|
||||||
if inp[2] is not None:
|
|
||||||
ia = inp[2]
|
|
||||||
if inp[3] is not None:
|
|
||||||
pubkey = inp[3]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if pubkey:
|
|
||||||
int_path = [0]
|
|
||||||
sec = pubkey
|
|
||||||
else:
|
|
||||||
int_path = str_to_path(sp)
|
|
||||||
sec = mk.subkey_for_path(sp).sec()
|
|
||||||
|
|
||||||
subkey = PublicKey.parse(sec)
|
|
||||||
|
|
||||||
assert len(sec) == 33, "expect compressed"
|
|
||||||
|
|
||||||
if af == "p2tr":
|
|
||||||
tweaked_xonly = taptweak(sec[1:])
|
|
||||||
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + mfp + struct.pack(f'<{"I" * len(int_path)}',
|
|
||||||
*int_path)
|
|
||||||
scr = bytes([81, 32]) + tweaked_xonly
|
|
||||||
|
|
||||||
elif af in ("p2wpkh", "p2sh-p2wpkh", "p2wpkh-p2sh"):
|
|
||||||
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
|
|
||||||
scr = bytes([0x00, 0x14]) + subkey.h160()
|
|
||||||
|
|
||||||
if af != "p2wpkh":
|
|
||||||
# use classic p2wpkh (from above) as redeem script
|
|
||||||
psbt.inputs[i].redeem_script = scr
|
|
||||||
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
|
|
||||||
|
|
||||||
elif af == "p2pkh":
|
|
||||||
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
|
|
||||||
scr = bytes([0x76, 0xa9, 0x14]) + subkey.h160() + bytes([0x88, 0xac])
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError("unknown addr_fmt %s" % af)
|
|
||||||
|
|
||||||
if i == 0:
|
|
||||||
# first input always spends to_spend
|
|
||||||
to_spend = CTransaction()
|
|
||||||
to_spend.nVersion = 0
|
|
||||||
out_point = COutPoint(hash=0, n=0xffffffff)
|
|
||||||
msg_hash = bip322_msg_hash(msg)
|
|
||||||
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
|
||||||
to_spend.vout = [CTxOut(0, scr)] # always zero val
|
|
||||||
msg_challenge = scr
|
|
||||||
else:
|
|
||||||
# other outputs that we want to prove ownership
|
|
||||||
to_spend = CTransaction()
|
|
||||||
to_spend.nVersion = 0
|
|
||||||
out_point = COutPoint(
|
|
||||||
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
|
|
||||||
73
|
|
||||||
)
|
|
||||||
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
|
|
||||||
to_spend.vout.append(CTxOut(int(ia), scr))
|
|
||||||
|
|
||||||
|
|
||||||
if sighash is not None:
|
|
||||||
psbt.inputs[i].sighash = sighash
|
|
||||||
|
|
||||||
to_spend.calc_sha256()
|
|
||||||
|
|
||||||
if i in witness_utxo:
|
|
||||||
psbt.inputs[i].witness_utxo = to_spend.vout[-1].serialize()
|
|
||||||
else:
|
|
||||||
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
|
|
||||||
|
|
||||||
if len(inputs) == 1:
|
|
||||||
# basic msg sign
|
|
||||||
seq = 0
|
|
||||||
else:
|
|
||||||
if to_sign_lock_time and not i:
|
if to_sign_lock_time and not i:
|
||||||
seq = 0xfffffffd
|
seq = 0xfffffffd
|
||||||
else:
|
else:
|
||||||
seq = 0xffffffff
|
seq = 0xffffffff
|
||||||
|
|
||||||
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
|
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
|
||||||
to_sign.vin.append(spendable)
|
to_sign.vin.append(spendable)
|
||||||
|
|
||||||
# just one zero amount output with script null data OP_RETURN
|
# just one zero amount output with script null data OP_RETURN
|
||||||
op_ret_o = BasicPSBTOutput(idx=0)
|
op_ret_o = BasicPSBTOutput(idx=0)
|
||||||
op_return_out = CTxOut(0, b'\x6a')
|
op_return_out = CTxOut(0, b'\x6a')
|
||||||
to_sign.vout.append(op_return_out)
|
to_sign.vout.append(op_return_out)
|
||||||
|
|
||||||
psbt.outputs.append(op_ret_o)
|
psbt.outputs.append(op_ret_o)
|
||||||
|
|
||||||
psbt.txn = to_sign.serialize_with_witness()
|
psbt.txn = to_sign.serialize_with_witness()
|
||||||
|
|
||||||
# last minute chance to mod PSBT object
|
# last minute chance to mod PSBT object
|
||||||
if psbt_hacker:
|
if psbt_hacker:
|
||||||
psbt_hacker(psbt)
|
psbt_hacker(psbt)
|
||||||
|
|
||||||
if psbt_v2:
|
rv = BytesIO()
|
||||||
psbt.parsed_txn = CTransaction()
|
psbt.serialize(rv)
|
||||||
psbt.parsed_txn.deserialize(BytesIO(psbt.txn))
|
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
|
||||||
psbt.to_v2()
|
|
||||||
|
|
||||||
rv = BytesIO()
|
return rv.getvalue(), msg_challenge
|
||||||
psbt.serialize(rv)
|
|
||||||
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
|
|
||||||
|
|
||||||
return rv.getvalue(), msg_challenge
|
return doit
|
||||||
|
|
||||||
|
|
||||||
def bip322_ms_txn(num_ins, M, keys, msg=b"POR", inp_af=AF_P2WSH, input_amount=1E8, path_mapper=None,
|
@pytest.fixture
|
||||||
lock_time=0, with_sigs=False, sighash=None, hack_psbt=None, to_sign_nVersion=0,
|
def bip322_ms_txn(pytestconfig, create_msg_file):
|
||||||
psbt_v2=False):
|
|
||||||
from test_multisig import make_ms_address
|
from test_multisig import make_ms_address
|
||||||
|
|
||||||
msg_challenge = None
|
def doit(num_ins, M, keys, msg=b"POR", inp_af=AF_P2WSH, input_amount=1E8, path_mapper=None,
|
||||||
|
lock_time=0, with_sigs=False, sighash=None, hack_psbt=None, to_sign_nVersion=0):
|
||||||
|
|
||||||
psbt = BasicPSBT()
|
msg_challenge = None
|
||||||
psbt.bip322_msg = msg
|
|
||||||
|
|
||||||
txn = CTransaction()
|
psbt = BasicPSBT()
|
||||||
txn.nVersion = to_sign_nVersion
|
|
||||||
txn.nLockTime = lock_time
|
|
||||||
|
|
||||||
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
|
txn = CTransaction()
|
||||||
psbt.outputs = []
|
txn.nVersion = to_sign_nVersion
|
||||||
|
txn.nLockTime = lock_time
|
||||||
|
|
||||||
for i in range(num_ins):
|
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
|
||||||
# make a fake txn to supply each of the inputs
|
psbt.outputs = []
|
||||||
# - each input is 1BTC
|
|
||||||
|
|
||||||
# addr where the fake money will be stored.
|
for i in range(num_ins):
|
||||||
addr, scriptPubKey, script, details = make_ms_address(
|
# make a fake txn to supply each of the inputs
|
||||||
M, keys, idx=i, addr_fmt=inp_af, path_mapper=path_mapper
|
# - each input is 1BTC
|
||||||
)
|
|
||||||
|
|
||||||
if inp_af == AF_P2WSH:
|
# addr where the fake money will be stored.
|
||||||
psbt.inputs[i].witness_script = script
|
addr, scriptPubKey, script, details = make_ms_address(
|
||||||
elif inp_af == AF_P2SH:
|
M, keys, idx=i, addr_fmt=inp_af, path_mapper=path_mapper
|
||||||
psbt.inputs[i].redeem_script = script
|
|
||||||
else:
|
|
||||||
assert inp_af == AF_P2WSH_P2SH
|
|
||||||
psbt.inputs[i].witness_script = script
|
|
||||||
psbt.inputs[i].redeem_script = b'\0\x20' + hashlib.sha256(script).digest()
|
|
||||||
|
|
||||||
for pubkey, xfp_path in details:
|
|
||||||
psbt.inputs[i].bip32_paths[pubkey] = b''.join(struct.pack('<I', j) for j in xfp_path)
|
|
||||||
if with_sigs and (xfp_path[0] != keys[-1][0]) and len(psbt.inputs[i].part_sigs) < (M-1): # only cosigner signatures are added
|
|
||||||
psbt.inputs[i].part_sigs[pubkey] = b"\x30" + 70*b"a"
|
|
||||||
|
|
||||||
if i == 0:
|
|
||||||
to_spend = CTransaction()
|
|
||||||
to_spend.nVersion = 0
|
|
||||||
out_point = COutPoint(hash=0, n=0xffffffff)
|
|
||||||
msg_hash = bip322_msg_hash(msg)
|
|
||||||
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
|
||||||
to_spend.vout.append(CTxOut(0, scriptPubKey))
|
|
||||||
msg_challenge = scriptPubKey
|
|
||||||
else:
|
|
||||||
# other outputs that we want to prove ownership
|
|
||||||
to_spend = CTransaction()
|
|
||||||
to_spend.nVersion = 0
|
|
||||||
out_point = COutPoint(
|
|
||||||
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
|
|
||||||
73
|
|
||||||
)
|
)
|
||||||
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
|
|
||||||
to_spend.vout.append(CTxOut(int(input_amount), scriptPubKey))
|
|
||||||
|
|
||||||
# always add whole txn as utxo
|
if inp_af == AF_P2WSH:
|
||||||
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
|
psbt.inputs[i].witness_script = script
|
||||||
if sighash is not None and (i != 0):
|
elif inp_af == AF_P2SH:
|
||||||
psbt.inputs[i].sighash = sighash
|
psbt.inputs[i].redeem_script = script
|
||||||
|
else:
|
||||||
|
assert inp_af == AF_P2WSH_P2SH
|
||||||
|
psbt.inputs[i].witness_script = script
|
||||||
|
psbt.inputs[i].redeem_script = b'\0\x20' + hashlib.sha256(script).digest()
|
||||||
|
|
||||||
to_spend.calc_sha256()
|
for pubkey, xfp_path in details:
|
||||||
|
psbt.inputs[i].bip32_paths[pubkey] = b''.join(struct.pack('<I', j) for j in xfp_path)
|
||||||
|
if with_sigs and (xfp_path[0] != keys[-1][0]): # only cosigner signatures are added
|
||||||
|
psbt.inputs[i].part_sigs[pubkey] = b"\x30" + 70*b"a"
|
||||||
|
|
||||||
|
if i == 0:
|
||||||
|
to_spend = CTransaction()
|
||||||
|
to_spend.nVersion = 0
|
||||||
|
out_point = COutPoint(hash=0, n=0xffffffff)
|
||||||
|
msg_hash = bip322_msg_hash(msg)
|
||||||
|
create_msg_file(msg, msg_hash)
|
||||||
|
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
||||||
|
to_spend.vout.append(CTxOut(0, scriptPubKey))
|
||||||
|
msg_challenge = scriptPubKey
|
||||||
|
else:
|
||||||
|
# other outputs that we want to prove ownership
|
||||||
|
to_spend = CTransaction()
|
||||||
|
to_spend.nVersion = 0
|
||||||
|
out_point = COutPoint(
|
||||||
|
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
|
||||||
|
73
|
||||||
|
)
|
||||||
|
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
|
||||||
|
to_spend.vout.append(CTxOut(int(input_amount), scriptPubKey))
|
||||||
|
|
||||||
|
# always add whole txn as utxo
|
||||||
|
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
|
||||||
|
if sighash is not None and (i != 0):
|
||||||
|
psbt.inputs[i].sighash = sighash
|
||||||
|
|
||||||
|
to_spend.calc_sha256()
|
||||||
|
|
||||||
if num_ins == 1:
|
|
||||||
# basic msg sign
|
|
||||||
seq = 0
|
|
||||||
else:
|
|
||||||
if lock_time and not i:
|
if lock_time and not i:
|
||||||
seq = 0xfffffffd
|
seq = 0xfffffffd
|
||||||
else:
|
else:
|
||||||
seq = 0xffffffff
|
seq = 0xffffffff
|
||||||
|
|
||||||
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
|
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
|
||||||
txn.vin.append(spendable)
|
txn.vin.append(spendable)
|
||||||
|
|
||||||
# just one zero amount output with script null data OP_RETURN
|
# just one zero amount output with script null data OP_RETURN
|
||||||
op_ret_o = BasicPSBTOutput(idx=0)
|
op_ret_o = BasicPSBTOutput(idx=0)
|
||||||
op_return_out = CTxOut(0, b'\x6a')
|
op_return_out = CTxOut(0, b'\x6a')
|
||||||
txn.vout.append(op_return_out)
|
txn.vout.append(op_return_out)
|
||||||
|
|
||||||
psbt.outputs.append(op_ret_o)
|
psbt.outputs.append(op_ret_o)
|
||||||
|
|
||||||
if hack_psbt:
|
if hack_psbt:
|
||||||
hack_psbt(psbt)
|
hack_psbt(psbt)
|
||||||
|
|
||||||
psbt.txn = txn.serialize_with_witness()
|
psbt.txn = txn.serialize_with_witness()
|
||||||
if psbt_v2:
|
|
||||||
psbt.parsed_txn = CTransaction()
|
|
||||||
psbt.parsed_txn.deserialize(BytesIO(psbt.txn))
|
|
||||||
psbt.to_v2()
|
|
||||||
|
|
||||||
rv = BytesIO()
|
rv = BytesIO()
|
||||||
psbt.serialize(rv)
|
psbt.serialize(rv)
|
||||||
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
|
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
|
||||||
|
|
||||||
|
return rv.getvalue(), msg_challenge
|
||||||
|
|
||||||
|
return doit
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bip322_from_classic_tx(dev, create_msg_file):
|
||||||
|
def doit(psbt, msg=b"POR"):
|
||||||
|
# takes in any PSBT and creates BIP-322 PSBT with all inputs as POR
|
||||||
|
# ignores & drops all outputs and replaces with one 0 val OP_RETURN
|
||||||
|
# 0th input is adjusted as specified in BIP-322 (to_spend)
|
||||||
|
po = BasicPSBT().parse(psbt)
|
||||||
|
|
||||||
|
to_sign = CTransaction()
|
||||||
|
to_sign.deserialize(BytesIO(po.txn))
|
||||||
|
to_sign.nVersion = 0 # required
|
||||||
|
to_sign.vout = [] # drop all outputs
|
||||||
|
# just one zero amount output with script null data OP_RETURN
|
||||||
|
op_ret_o = BasicPSBTOutput(idx=0)
|
||||||
|
op_return_out = CTxOut(0, b'\x6a')
|
||||||
|
to_sign.vout.append(op_return_out)
|
||||||
|
po.outputs = [op_ret_o]
|
||||||
|
|
||||||
|
if po.inputs[0].utxo:
|
||||||
|
i0_utxo = CTransaction()
|
||||||
|
i0_utxo.deserialize(BytesIO(po.inputs[0].utxo))
|
||||||
|
scriptPubKey = i0_utxo.vout[to_sign.vin[0].prevout.n].scriptPubKey
|
||||||
|
else:
|
||||||
|
assert po.inputs[0].witness_utxo
|
||||||
|
i0_wutxo = CTxOut()
|
||||||
|
i0_wutxo.deserialize(BytesIO(po.inputs[0].witness_utxo))
|
||||||
|
scriptPubKey = i0_wutxo.scriptPubKey
|
||||||
|
|
||||||
|
to_spend = CTransaction()
|
||||||
|
to_spend.nVersion = 0
|
||||||
|
out_point = COutPoint(hash=0, n=0xffffffff)
|
||||||
|
msg_hash = bip322_msg_hash(msg)
|
||||||
|
create_msg_file(msg, msg_hash)
|
||||||
|
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
|
||||||
|
to_spend.vout.append(CTxOut(0, scriptPubKey))
|
||||||
|
msg_challenge = scriptPubKey
|
||||||
|
|
||||||
|
to_spend.calc_sha256()
|
||||||
|
|
||||||
|
to_sign.vin[0] = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=0xffffffff)
|
||||||
|
po.inputs[0].utxo = to_spend.serialize_with_witness()
|
||||||
|
# if it has witness UTXO - get rid of it
|
||||||
|
po.inputs[0].witness_utxo = None
|
||||||
|
|
||||||
|
po.txn = to_sign.serialize_with_witness()
|
||||||
|
|
||||||
|
rv = BytesIO()
|
||||||
|
po.serialize(rv)
|
||||||
|
return rv.getvalue(), msg_challenge
|
||||||
|
|
||||||
|
return doit
|
||||||
|
|
||||||
return rv.getvalue(), msg_challenge
|
|
||||||
|
|||||||
@ -2,12 +2,10 @@
|
|||||||
#
|
#
|
||||||
import pytest, time, pdb, itertools
|
import pytest, time, pdb, itertools
|
||||||
from charcodes import KEY_ENTER
|
from charcodes import KEY_ENTER
|
||||||
from core_fixtures import _pick_menu_item, _cap_story, _press_select, _word_menu_entry
|
from core_fixtures import _pick_menu_item, _cap_story, _press_select
|
||||||
from core_fixtures import _need_keypress, _cap_menu, _sim_exec, _pass_word_quiz
|
from core_fixtures import _need_keypress, _cap_menu, _sim_exec
|
||||||
from run_sim_tests import ColdcardSimulator, clean_sim_data
|
from run_sim_tests import ColdcardSimulator, clean_sim_data
|
||||||
from ckcc_protocol.cli import wait_and_download
|
|
||||||
from ckcc_protocol.client import ColdcardDevice
|
from ckcc_protocol.client import ColdcardDevice
|
||||||
from ckcc_protocol.protocol import CCProtocolPacker
|
|
||||||
|
|
||||||
|
|
||||||
def _clone(source, target):
|
def _clone(source, target):
|
||||||
@ -47,7 +45,7 @@ def _clone(source, target):
|
|||||||
assert f"Bring that card back and press {'ENTER' if target_is_Q else 'OK'} to complete clone process" in story
|
assert f"Bring that card back and press {'ENTER' if target_is_Q else 'OK'} to complete clone process" in story
|
||||||
|
|
||||||
# SOURCE
|
# SOURCE
|
||||||
# clone with multisig wallet
|
# clone with miniscript wallet
|
||||||
sim_source = ColdcardSimulator(args=[source_sim_arg, "--ms", "--p2wsh",
|
sim_source = ColdcardSimulator(args=[source_sim_arg, "--ms", "--p2wsh",
|
||||||
"--set", "nfc=1", "--set", "vidsk=1"])
|
"--set", "nfc=1", "--set", "vidsk=1"])
|
||||||
sim_source.start(start_wait=6)
|
sim_source.start(start_wait=6)
|
||||||
@ -106,10 +104,10 @@ def _clone(source, target):
|
|||||||
sim_target.start(start_wait=6)
|
sim_target.start(start_wait=6)
|
||||||
device = ColdcardDevice(is_simulator=True)
|
device = ColdcardDevice(is_simulator=True)
|
||||||
_pick_menu_item(device, target_is_Q, "Settings")
|
_pick_menu_item(device, target_is_Q, "Settings")
|
||||||
_pick_menu_item(device, target_is_Q, "Multisig Wallets")
|
_pick_menu_item(device, target_is_Q, "Multisig/Miniscript")
|
||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
m = _cap_menu(device)
|
m = _cap_menu(device)
|
||||||
assert "2/4: P2WSH--2-of-4" in m
|
assert "P2WSH--2-of-4" in m
|
||||||
|
|
||||||
# check NFC/VDisk after clone - must be disabled
|
# check NFC/VDisk after clone - must be disabled
|
||||||
# USB enabled as we are on the simulator
|
# USB enabled as we are on the simulator
|
||||||
@ -125,99 +123,4 @@ def test_clone(source, target):
|
|||||||
_clone(source, target)
|
_clone(source, target)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
def test_backup_restore_delta_pin():
|
|
||||||
# SOURCE
|
|
||||||
# clone with multisig wallet
|
|
||||||
clean_sim_data() # remove all from previous
|
|
||||||
sim_source = ColdcardSimulator(args=["--ms", "--p2wsh", "--set", "nfc=1", "--set", "vidsk=1"],
|
|
||||||
segregate=True) # in /tmp/cc-simulators
|
|
||||||
sim_source.start(start_wait=6)
|
|
||||||
device_source = ColdcardDevice(is_simulator=True, sn=sim_source.socket)
|
|
||||||
_pick_menu_item(device_source, False, "Settings")
|
|
||||||
time.sleep(.1)
|
|
||||||
_pick_menu_item(device_source, False, "Login Settings")
|
|
||||||
time.sleep(.1)
|
|
||||||
_pick_menu_item(device_source, False, "Trick PINs")
|
|
||||||
time.sleep(.1)
|
|
||||||
_pick_menu_item(device_source, False, "Add New Trick")
|
|
||||||
time.sleep(.1)
|
|
||||||
|
|
||||||
# twice, first select, then verify
|
|
||||||
for _ in range(2):
|
|
||||||
pin = "11-11"
|
|
||||||
pre, suff = pin.split("-")
|
|
||||||
for ch in pre:
|
|
||||||
_need_keypress(device_source, ch)
|
|
||||||
time.sleep(.1)
|
|
||||||
_press_select(device_source, False)
|
|
||||||
|
|
||||||
time.sleep(.2)
|
|
||||||
|
|
||||||
for ch in suff:
|
|
||||||
_need_keypress(device_source, ch)
|
|
||||||
time.sleep(.1)
|
|
||||||
_press_select(device_source, False)
|
|
||||||
|
|
||||||
time.sleep(.2)
|
|
||||||
_pick_menu_item(device_source, False, "Delta Mode")
|
|
||||||
time.sleep(.1)
|
|
||||||
title, story = _cap_story(device_source)
|
|
||||||
assert "trick PIN must be same length as true PIN and differ only in final 4 positions" in story
|
|
||||||
_press_select(device_source, False)
|
|
||||||
time.sleep(.1)
|
|
||||||
_press_select(device_source, False)
|
|
||||||
time.sleep(.1)
|
|
||||||
m = _cap_menu(device_source)
|
|
||||||
assert "11-11" in m[1]
|
|
||||||
|
|
||||||
ok = device_source.send_recv(CCProtocolPacker.start_backup())
|
|
||||||
assert ok is None
|
|
||||||
time.sleep(1)
|
|
||||||
title, story = _cap_story(device_source)
|
|
||||||
assert "backup file password" in story
|
|
||||||
word_list = [item.split()[-1] for item in story.split("\n")[1:-4]]
|
|
||||||
assert len(word_list) == 12
|
|
||||||
_pass_word_quiz(device_source, False, word_list)
|
|
||||||
_press_select(device_source, False) # bkpw
|
|
||||||
result, chk = wait_and_download(device_source, CCProtocolPacker.get_backup_file(), 0)
|
|
||||||
sim_source.stop()
|
|
||||||
|
|
||||||
|
|
||||||
# TARGET Q (empty)
|
|
||||||
sim_target = ColdcardSimulator(args=["--q1", "-l"])
|
|
||||||
sim_target.start(start_wait=6)
|
|
||||||
device_target = ColdcardDevice(is_simulator=True)
|
|
||||||
|
|
||||||
name = "backup-delta.7z"
|
|
||||||
path = f"../unix/work/MicroSD/{name}"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(result)
|
|
||||||
|
|
||||||
_pick_menu_item(device_target, True, "Import Existing")
|
|
||||||
_pick_menu_item(device_target, True, "Restore Backup")
|
|
||||||
_pick_menu_item(device_target, True, name)
|
|
||||||
time.sleep(.1)
|
|
||||||
|
|
||||||
_word_menu_entry(device_target, True, word_list, has_checksum=False)
|
|
||||||
_press_select(device_target, True) # allow backup restore
|
|
||||||
time.sleep(.1)
|
|
||||||
_press_select(device_target, True) # best security practices config
|
|
||||||
time.sleep(.1)
|
|
||||||
_press_select(device_target, True) # success
|
|
||||||
|
|
||||||
sim_target.stop()
|
|
||||||
time.sleep(1)
|
|
||||||
sim_target = ColdcardSimulator(args=["--q1"])
|
|
||||||
sim_target.start(start_wait=6)
|
|
||||||
device_target = ColdcardDevice(is_simulator=True, sn=sim_target.socket)
|
|
||||||
_pick_menu_item(device_target, True, "Settings")
|
|
||||||
time.sleep(.1)
|
|
||||||
_pick_menu_item(device_target, True, "Login Settings")
|
|
||||||
time.sleep(.1)
|
|
||||||
_pick_menu_item(device_target, True, "Trick PINs")
|
|
||||||
time.sleep(.1)
|
|
||||||
m = _cap_menu(device_target)
|
|
||||||
assert "11-11" in m[1]
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
@ -1,22 +1,20 @@
|
|||||||
# (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, base64
|
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, bech32, pdb, base64
|
||||||
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, addr_from_display_format, seconds2human_readable
|
from helpers import B2A, U2SAT, hash160, taptweak, addr_from_display_format, seconds2human_readable
|
||||||
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
|
||||||
from api import bitcoind, match_key
|
from api import bitcoind, match_key
|
||||||
from api import bitcoind_wallet, bitcoind_d_wallet, bitcoind_d_wallet_w_sk, bitcoind_d_sim_sign, bitcoind_d_dev_watch
|
from api import bitcoind_wallet, bitcoind_d_wallet, bitcoind_d_wallet_w_sk, bitcoind_d_sim_sign, bitcoind_d_dev_watch
|
||||||
from api import bitcoind_d_sim_watch, finalize_v2_v0_convert
|
from api import bitcoind_d_sim_watch, finalize_v2_v0_convert
|
||||||
from electrum import electrum
|
|
||||||
from binascii import b2a_hex, a2b_hex
|
from binascii import b2a_hex, a2b_hex
|
||||||
from constants import *
|
from constants import *
|
||||||
from charcodes import *
|
from charcodes import *
|
||||||
from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen, _sim_eval
|
from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen, _sim_eval
|
||||||
from core_fixtures import _press_select, _pick_menu_item, _enter_complex, _dev_hw_label
|
from core_fixtures import _press_select, _pick_menu_item, _enter_complex, _dev_hw_label
|
||||||
from core_fixtures import _do_keypresses
|
|
||||||
from txn import render_address
|
from txn import render_address
|
||||||
from bbqr import split_qrs
|
from bbqr import split_qrs
|
||||||
|
|
||||||
@ -209,11 +207,14 @@ def enter_pin(enter_number, press_select, cap_screen, is_q1):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def do_keypresses(dev):
|
def do_keypresses(need_keypress):
|
||||||
# do a series of keypresses, any kind
|
# do a series of keypresses, any kind
|
||||||
f = functools.partial(_do_keypresses, dev)
|
def doit(value):
|
||||||
return f
|
for ch in value:
|
||||||
|
need_keypress(ch)
|
||||||
|
|
||||||
|
return doit
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def enter_text(need_keypress, is_q1):
|
def enter_text(need_keypress, is_q1):
|
||||||
@ -290,29 +291,33 @@ def get_setting(sim_execfile, sim_exec):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def addr_vs_path(master_xpub):
|
def addr_vs_path(master_xpub):
|
||||||
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"):
|
||||||
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
|
||||||
|
|
||||||
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 +365,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
|
||||||
@ -602,15 +606,12 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
|
|||||||
qr = cap_screen_qr().decode('ascii')
|
qr = cap_screen_qr().decode('ascii')
|
||||||
|
|
||||||
if isinstance(addr_fmt, str):
|
if isinstance(addr_fmt, str):
|
||||||
if addr_fmt == "p2tr":
|
try:
|
||||||
addr_fmt = AF_P2TR
|
addr_fmt = unmap_addr_fmt[addr_fmt]
|
||||||
else:
|
except KeyError:
|
||||||
try:
|
addr_fmt = msg_sign_unmap_addr_fmt[addr_fmt]
|
||||||
addr_fmt = unmap_addr_fmt[addr_fmt]
|
|
||||||
except KeyError:
|
|
||||||
addr_fmt = msg_sign_unmap_addr_fmt[addr_fmt]
|
|
||||||
|
|
||||||
if addr_fmt & AFC_BECH32:
|
if (addr_fmt & AFC_BECH32) or (addr_fmt & AFC_BECH32M):
|
||||||
qr = qr.lower()
|
qr = qr.lower()
|
||||||
|
|
||||||
# check text --if any-- matches QR contents
|
# check text --if any-- matches QR contents
|
||||||
@ -641,7 +642,7 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
|
|||||||
for c, line in zip("XXXXXXBACK", full_split):
|
for c, line in zip("XXXXXXBACK", full_split):
|
||||||
assert not line.endswith(c)
|
assert not line.endswith(c)
|
||||||
|
|
||||||
txt = None
|
txt = None # most of the time there is no address
|
||||||
else:
|
else:
|
||||||
if is_change:
|
if is_change:
|
||||||
assert "CHANGE BACK" in full
|
assert "CHANGE BACK" in full
|
||||||
@ -693,6 +694,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
|
@pytest.fixture
|
||||||
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)
|
||||||
@ -1026,16 +1033,20 @@ def settings_set(sim_exec):
|
|||||||
|
|
||||||
return doit
|
return doit
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def settings_append(sim_exec):
|
||||||
|
def doit(key, val):
|
||||||
|
x = sim_exec("x=settings.get('%s',[])\nx.append(%r)\nsettings.set('%s', x)" % (key, val, key))
|
||||||
|
assert x == ''
|
||||||
|
return doit
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def settings_get(sim_exec):
|
def settings_get(sim_exec):
|
||||||
|
|
||||||
def doit(key, def_val=None, prelogin=False):
|
def doit(key, def_val=None, prelogin=False):
|
||||||
if prelogin:
|
source = "from nvstore import SettingsObject;SettingsObject.prelogin()" if prelogin else "settings"
|
||||||
src = f"from nvstore import SettingsObject;RV.write(repr(SettingsObject.prelogin().get('{key}', {def_val!r})))"
|
cmd = f"RV.write(repr({source}.get('{key}', {def_val!r})))"
|
||||||
else:
|
resp = sim_exec(cmd)
|
||||||
src = f"RV.write(repr(settings.get('{key}', {def_val!r})))"
|
|
||||||
|
|
||||||
resp = sim_exec(src)
|
|
||||||
assert 'Traceback' not in resp, resp
|
assert 'Traceback' not in resp, resp
|
||||||
return eval(resp)
|
return eval(resp)
|
||||||
|
|
||||||
@ -1055,9 +1066,8 @@ def master_settings_get(sim_exec):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def settings_remove(sim_exec):
|
def settings_remove(sim_exec):
|
||||||
|
|
||||||
def doit(key, prelogin=False):
|
def doit(key):
|
||||||
source = "from nvstore import SettingsObject;SettingsObject.prelogin()" if prelogin else "settings"
|
x = sim_exec("settings.remove_key('%s')" % key)
|
||||||
x = sim_exec("%s.remove_key('%s')" % (source, key))
|
|
||||||
assert x == ''
|
assert x == ''
|
||||||
|
|
||||||
return doit
|
return doit
|
||||||
@ -1404,8 +1414,8 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
|
|||||||
def try_sign(start_sign, end_sign):
|
def try_sign(start_sign, end_sign):
|
||||||
|
|
||||||
def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False,
|
def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False,
|
||||||
exit_export_loop=True):
|
exit_export_loop=True, miniscript=None):
|
||||||
ip = start_sign(filename_or_data, finalize=finalize)
|
ip = start_sign(filename_or_data, finalize=finalize, miniscript=miniscript)
|
||||||
return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import,
|
return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import,
|
||||||
exit_export_loop=exit_export_loop)
|
exit_export_loop=exit_export_loop)
|
||||||
|
|
||||||
@ -1414,7 +1424,7 @@ def try_sign(start_sign, end_sign):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def start_sign(dev):
|
def start_sign(dev):
|
||||||
|
|
||||||
def doit(filename, finalize=False, stxn_flags=0x0):
|
def doit(filename, finalize=False, stxn_flags=0x0, miniscript=None):
|
||||||
if filename[0:5] == b'psbt\xff':
|
if filename[0:5] == b'psbt\xff':
|
||||||
ip = filename
|
ip = filename
|
||||||
filename = 'memory'
|
filename = 'memory'
|
||||||
@ -1426,7 +1436,8 @@ def start_sign(dev):
|
|||||||
|
|
||||||
ll, sha = dev.upload_file(ip)
|
ll, sha = dev.upload_file(ip)
|
||||||
|
|
||||||
dev.send_recv(CCProtocolPacker.sign_transaction(ll, sha, finalize, flags=stxn_flags))
|
dev.send_recv(CCProtocolPacker.sign_transaction(ll, sha, finalize, flags=stxn_flags,
|
||||||
|
miniscript_name=miniscript))
|
||||||
|
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
@ -1685,6 +1696,9 @@ def nfc_read_url(nfc_read, press_cancel):
|
|||||||
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)
|
||||||
@ -1852,7 +1866,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:
|
||||||
@ -1861,6 +1875,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)
|
||||||
@ -1901,9 +1916,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
|
||||||
@ -1927,6 +1940,9 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache
|
|||||||
if is_json:
|
if is_json:
|
||||||
assert fname.endswith(".json")
|
assert fname.endswith(".json")
|
||||||
|
|
||||||
|
if addr_fmt == AF_P2TR:
|
||||||
|
addr_fmt = AF_CLASSIC
|
||||||
|
|
||||||
contents, address = verify_detached_signature_file([fname], sig_fn, way, addr_fmt)
|
contents, address = verify_detached_signature_file([fname], sig_fn, way, addr_fmt)
|
||||||
|
|
||||||
if is_json:
|
if is_json:
|
||||||
@ -1963,7 +1979,7 @@ def file_tx_signing_done(virtdisk_path, microsd_path):
|
|||||||
|
|
||||||
txid = None
|
txid = None
|
||||||
for l in _split:
|
for l in _split:
|
||||||
if "TXID:" in l:
|
if "TXID" in l:
|
||||||
txid = l.split("\n")[-1].strip()
|
txid = l.split("\n")[-1].strip()
|
||||||
assert len(txid) == 64, "wrong txid"
|
assert len(txid) == 64, "wrong txid"
|
||||||
break
|
break
|
||||||
@ -1975,10 +1991,10 @@ def file_tx_signing_done(virtdisk_path, microsd_path):
|
|||||||
@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, nfc_read_txn, file_tx_signing_done):
|
cap_screen_qr, nfc_read_txn, file_tx_signing_done, 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, is_tx=False, encoding="base64"):
|
fpattern=None, qr_key=None, is_tx=False, encoding="base64", skip_query=False):
|
||||||
|
|
||||||
s_label = None
|
s_label = None
|
||||||
if label == "Address summary":
|
if label == "Address summary":
|
||||||
@ -1990,61 +2006,62 @@ 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} "
|
if way == "sd":
|
||||||
f"{'' if is_tx else 'file '}to SD Card") in story:
|
if (f"({key_map['sd']}) to save {s_label if s_label else label} "
|
||||||
need_keypress(key_map['sd'])
|
f"{'' if is_tx else '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_tx:
|
|
||||||
nfc_export = nfc_read_txn()
|
|
||||||
return nfc_export[1:]
|
|
||||||
|
|
||||||
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_tx:
|
||||||
|
nfc_export = nfc_read_txn()
|
||||||
|
return nfc_export[1:]
|
||||||
|
|
||||||
|
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:
|
|
||||||
assert is_q1
|
|
||||||
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
|
|
||||||
elif file_type in ("P", "T"):
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
except:
|
|
||||||
res = cap_screen_qr().decode('ascii')
|
|
||||||
try:
|
try:
|
||||||
return json.loads(res)
|
assert is_q1
|
||||||
|
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
|
||||||
|
elif file_type in ("P", "T"):
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
except:
|
except:
|
||||||
return res
|
res = cap_screen_qr().decode('ascii')
|
||||||
else:
|
try:
|
||||||
# virtual disk
|
return json.loads(res)
|
||||||
if f"({key_map['vdisk']}) to save to Virtual Disk" not in story:
|
except:
|
||||||
pytest.skip("Vdisk disabled")
|
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()
|
||||||
@ -2075,6 +2092,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:
|
||||||
@ -2224,7 +2243,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:
|
||||||
@ -2232,7 +2251,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
|
||||||
@ -2245,7 +2264,7 @@ def verify_backup_file(goto_home, pick_menu_item, cap_story, need_keypress):
|
|||||||
# Check on-device verify UX works.
|
# Check on-device verify UX works.
|
||||||
goto_home()
|
goto_home()
|
||||||
pick_menu_item('Advanced/Tools')
|
pick_menu_item('Advanced/Tools')
|
||||||
pick_menu_item('File Management')
|
pick_menu_item('Backup')
|
||||||
pick_menu_item('Verify Backup')
|
pick_menu_item('Verify Backup')
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
pick_menu_item(os.path.basename(fn))
|
pick_menu_item(os.path.basename(fn))
|
||||||
@ -2597,7 +2616,7 @@ def explorer_input_check(cap_story, press_cancel, need_keypress, is_q1, verify_q
|
|||||||
|
|
||||||
n = BIP32Node.from_wallet_key(simulator_fixed_xprv if chain == "BTC" else simulator_fixed_tprv)
|
n = BIP32Node.from_wallet_key(simulator_fixed_xprv if chain == "BTC" else simulator_fixed_tprv)
|
||||||
for pk, der in parsed_our_keys.items():
|
for pk, der in parsed_our_keys.items():
|
||||||
assert bytes.fromhex(pk) == n.subkey_for_path(der.split("/", 1)[-1]).sec()
|
assert bytes.fromhex(pk) == n.subkey_for_path(der.split("/", 1)[-1]).sec()[1 if af == "p2tr" else 0:]
|
||||||
|
|
||||||
# MULTISIG
|
# MULTISIG
|
||||||
if parsed_multisig is None:
|
if parsed_multisig is None:
|
||||||
@ -2617,7 +2636,10 @@ def explorer_input_check(cap_story, press_cancel, need_keypress, is_q1, verify_q
|
|||||||
|
|
||||||
# SIGHASH
|
# SIGHASH
|
||||||
if parsed_sighash is None:
|
if parsed_sighash is None:
|
||||||
assert sighash in [None, "ALL"]
|
if af == "p2tr":
|
||||||
|
assert sighash in [None, "DEFAULT"]
|
||||||
|
else:
|
||||||
|
assert sighash in [None, "ALL"]
|
||||||
else:
|
else:
|
||||||
assert sighash == parsed_sighash
|
assert sighash == parsed_sighash
|
||||||
|
|
||||||
@ -2644,8 +2666,7 @@ def txin_explorer(cap_story, press_cancel, need_keypress, is_q1, cap_menu,
|
|||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
title, story = cap_story()
|
title, story = cap_story()
|
||||||
ss = story.split("\n\n")
|
ss = story.split("\n\n")
|
||||||
if i < (num_inputs - 1):
|
assert "Press RIGHT to see next group" in ss[-1]
|
||||||
assert "RIGHT to see next group" in ss[-1]
|
|
||||||
if i:
|
if i:
|
||||||
assert " LEFT to go back" in ss[-1]
|
assert " LEFT to go back" in ss[-1]
|
||||||
else:
|
else:
|
||||||
@ -2720,8 +2741,7 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_addr
|
|||||||
_, story = cap_story()
|
_, story = cap_story()
|
||||||
ss = story.split("\n\n")
|
ss = story.split("\n\n")
|
||||||
assert len(ss) == (len(d) * 2) + 1
|
assert len(ss) == (len(d) * 2) + 1
|
||||||
if (i + n) < len(data):
|
assert "Press RIGHT to see next group" in ss[-1]
|
||||||
assert "RIGHT to see next group" in ss[-1]
|
|
||||||
if i:
|
if i:
|
||||||
assert " LEFT to go back" in ss[-1]
|
assert " LEFT to go back" in ss[-1]
|
||||||
else:
|
else:
|
||||||
@ -2762,6 +2782,9 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_addr
|
|||||||
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)
|
||||||
@ -2810,6 +2833,30 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_addr
|
|||||||
|
|
||||||
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, vdisk_disabled):
|
def skip_if_useless_way(is_q1, nfc_disabled, vdisk_disabled):
|
||||||
@ -2838,7 +2885,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):
|
||||||
@ -3020,39 +3068,24 @@ def import_wif_to_store(goto_home, pick_menu_item, cap_story, press_select, cap_
|
|||||||
|
|
||||||
return doit
|
return doit
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def bip322_txn(dev, pytestconfig):
|
|
||||||
from bip322 import bip322_txn
|
|
||||||
return functools.partial(bip322_txn, master_xpub=dev.master_xpub,
|
|
||||||
psbt_v2=pytestconfig.getoption('psbt2'))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def bip322_ms_txn(pytestconfig):
|
|
||||||
from bip322 import bip322_ms_txn
|
|
||||||
return functools.partial(bip322_ms_txn, psbt_v2=pytestconfig.getoption('psbt2'))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def bip322_verify():
|
|
||||||
from bip322 import bip322_verify
|
|
||||||
return bip322_verify
|
|
||||||
|
|
||||||
|
|
||||||
# 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, try_sign_bbqr, split_scan_bbqr
|
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll, try_sign_bbqr, split_scan_bbqr
|
||||||
|
from bip322 import bip322_txn, bip322_ms_txn, create_msg_file, bip322_from_classic_tx
|
||||||
from test_bip39pw import set_bip39_pw
|
from test_bip39pw import set_bip39_pw
|
||||||
from test_ccc import get_last_violation, setup_ccc, goto_ccc_menu, ccc_ms_setup, bitcoind_create_watch_only_wallet
|
from test_ccc import get_last_violation
|
||||||
from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
|
from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
|
||||||
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
|
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
|
||||||
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
|
from test_ephemeral import 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_hobble import set_hobble
|
from test_hobble import set_hobble
|
||||||
from test_msg import verify_msg_sign_story, sign_msg_from_text, msg_sign_export, sign_msg_from_address
|
from test_msg import verify_msg_sign_story, sign_msg_from_text, msg_sign_export, sign_msg_from_address
|
||||||
from test_multisig import import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn
|
from test_multisig import import_ms_wallet, make_multisig, fake_ms_txn
|
||||||
from test_multisig import make_ms_address, clear_ms, make_myself_wallet, import_multisig
|
from test_miniscript import (offer_minsc_import, get_cc_key, bitcoin_core_signer, import_miniscript, usb_miniscript_get,
|
||||||
|
usb_miniscript_addr, create_core_wallet, import_duplicate, address_explorer_check,
|
||||||
|
miniscript_descriptors, usb_miniscript_policy)
|
||||||
|
from test_multisig import make_ms_address, make_myself_wallet
|
||||||
|
from test_musig2 import build_musig_wallet
|
||||||
from test_notes import need_some_notes, need_some_passwords, goto_notes
|
from test_notes import need_some_notes, need_some_passwords, goto_notes
|
||||||
from test_nfc import try_sign_nfc, ndef_parse_txn_psbt
|
from test_nfc import try_sign_nfc, ndef_parse_txn_psbt
|
||||||
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
|
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
|
||||||
|
|||||||
@ -23,9 +23,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,
|
||||||
@ -33,27 +35,28 @@ 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',
|
||||||
AF_P2WSH: 'p2wsh',
|
AF_P2WSH: 'p2wsh',
|
||||||
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
|
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
|
||||||
AF_P2WSH_P2SH: 'p2wsh-p2sh',
|
AF_P2WSH_P2SH: 'p2sh-p2wsh',
|
||||||
AF_P2TR: "p2tr",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# 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']
|
||||||
|
|
||||||
# SIGHASH
|
# SIGHASH
|
||||||
SIGHASH_MAP = {
|
SIGHASH_MAP = {
|
||||||
|
"DEFAULT": 0,
|
||||||
"ALL": 1,
|
"ALL": 1,
|
||||||
"NONE": 2,
|
"NONE": 2,
|
||||||
"SINGLE": 3,
|
"SINGLE": 3,
|
||||||
@ -62,5 +65,9 @@ SIGHASH_MAP = {
|
|||||||
"SINGLE|ANYONECANPAY": 3 | 0x80,
|
"SINGLE|ANYONECANPAY": 3 | 0x80,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SIGHASH_MAP_NON_TAPROOT = {k:v for k, v in SIGHASH_MAP.items() if k != "DEFAULT"}
|
||||||
|
|
||||||
# (2**31) - 1 --> max unhardened, but we handle hardened via h elsewhere
|
# (2**31) - 1 --> max unhardened, but we handle hardened via h elsewhere
|
||||||
MAX_BIP32_IDX = 2147483647
|
MAX_BIP32_IDX = 2147483647
|
||||||
|
|
||||||
|
BIP_341_H = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
|
||||||
@ -7,7 +7,7 @@
|
|||||||
# Below functions are injected with proper scoped `device` in conftest.py
|
# Below functions are injected with proper scoped `device` in conftest.py
|
||||||
# using funtools.partial.
|
# using funtools.partial.
|
||||||
#
|
#
|
||||||
import time, re
|
import time
|
||||||
from charcodes import *
|
from charcodes import *
|
||||||
from ckcc_protocol.client import CCProtocolPacker
|
from ckcc_protocol.client import CCProtocolPacker
|
||||||
|
|
||||||
@ -149,116 +149,4 @@ def _enter_complex(device, is_Q, target, apply=False, b39pass=True):
|
|||||||
if apply:
|
if apply:
|
||||||
_pick_menu_item(device, is_Q, "APPLY")
|
_pick_menu_item(device, is_Q, "APPLY")
|
||||||
|
|
||||||
|
|
||||||
def _pass_word_quiz(device, is_Q, words, prefix='', preload=None):
|
|
||||||
if not preload:
|
|
||||||
_press_select(device, is_Q)
|
|
||||||
time.sleep(.01)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
last_title = None
|
|
||||||
while 1:
|
|
||||||
title, body = preload or _cap_story(device)
|
|
||||||
preload = None
|
|
||||||
|
|
||||||
if not title.startswith('Word ' + prefix): break
|
|
||||||
assert title.endswith(' is?')
|
|
||||||
assert not last_title or last_title != title, "gave wrong ans?"
|
|
||||||
|
|
||||||
wn = int(title.split()[1][len(prefix):])
|
|
||||||
assert 1 <= wn <= len(words)
|
|
||||||
wn -= 1
|
|
||||||
|
|
||||||
ans = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':']
|
|
||||||
assert len(ans) == 3
|
|
||||||
|
|
||||||
correct = ans.index(words[wn])
|
|
||||||
assert 0 <= correct < 3
|
|
||||||
|
|
||||||
# print("Pick %d: %s" % (correct, ans[correct]))
|
|
||||||
|
|
||||||
_need_keypress(device, chr(49 + correct))
|
|
||||||
time.sleep(.1)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
last_title = title
|
|
||||||
|
|
||||||
return count, title, body
|
|
||||||
|
|
||||||
|
|
||||||
def _do_keypresses(device, value):
|
|
||||||
for ch in value:
|
|
||||||
_need_keypress(device, ch)
|
|
||||||
|
|
||||||
def _word_menu_entry(device, is_Q, words, has_checksum=True, q_accept=True):
|
|
||||||
if is_Q:
|
|
||||||
# easier for us on Q, but have to anticipate the autocomplete
|
|
||||||
for n, w in enumerate(words, start=1):
|
|
||||||
_do_keypresses(device, w[0:2])
|
|
||||||
time.sleep(0.1)
|
|
||||||
if 'Next key' in _cap_screen(device):
|
|
||||||
_do_keypresses(device, w[2])
|
|
||||||
time.sleep(.01)
|
|
||||||
|
|
||||||
if 'Next key' in _cap_screen(device):
|
|
||||||
if len(w) > 3:
|
|
||||||
_do_keypresses(device, w[3])
|
|
||||||
else:
|
|
||||||
_do_keypresses(device, KEY_DOWN)
|
|
||||||
time.sleep(.01)
|
|
||||||
|
|
||||||
pat = rf'{n}:\s?{w}'
|
|
||||||
for x in range(10):
|
|
||||||
if re.search(pat, _cap_screen(device)):
|
|
||||||
break
|
|
||||||
time.sleep(0.02)
|
|
||||||
else:
|
|
||||||
raise RuntimeError('timeout')
|
|
||||||
|
|
||||||
if len(words) == 23:
|
|
||||||
_do_keypresses(device, KEY_DOWN)
|
|
||||||
time.sleep(.03)
|
|
||||||
cap_scr = _cap_screen(device)
|
|
||||||
while 'Next key' in cap_scr:
|
|
||||||
target = cap_scr.split("\n")[-1].replace("Next key: ", "")
|
|
||||||
# picks first choice!?
|
|
||||||
_do_keypresses(device, target[0])
|
|
||||||
time.sleep(.03)
|
|
||||||
cap_scr = _cap_screen(device)
|
|
||||||
else:
|
|
||||||
cap_scr = _cap_screen(device)
|
|
||||||
|
|
||||||
if has_checksum:
|
|
||||||
assert 'Valid words' in cap_scr
|
|
||||||
else:
|
|
||||||
assert 'Press ENTER if all done' in cap_scr
|
|
||||||
|
|
||||||
if q_accept:
|
|
||||||
_do_keypresses(device, '\r')
|
|
||||||
return
|
|
||||||
|
|
||||||
# do the massive drilling-down to pick a specific pass phrase
|
|
||||||
assert len(words) in {1, 12, 18, 23, 24}
|
|
||||||
|
|
||||||
for word in words:
|
|
||||||
while 1:
|
|
||||||
menu = _cap_menu(device)
|
|
||||||
which = None
|
|
||||||
for m in menu:
|
|
||||||
if '-' not in m:
|
|
||||||
if m == word:
|
|
||||||
which = m
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
assert m[-1] == '-'
|
|
||||||
if m == word[0:len(m)-1]+'-':
|
|
||||||
which = m
|
|
||||||
break
|
|
||||||
|
|
||||||
assert which, "cant find: " + word
|
|
||||||
|
|
||||||
_pick_menu_item(device, is_Q, which)
|
|
||||||
if '-' not in which:
|
|
||||||
break
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
BIN
testing/data/migration_640/big_boy.7z
Normal file
BIN
testing/data/migration_640/big_boy.7z
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user