From 30ea8a083196bc7bf92b5a29424881375ce44542 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 30 Jun 2024 10:27:58 +0200 Subject: [PATCH 01/31] hot: add hot wallet basics --- .github/workflows/build.yml | 1 + Cargo.lock | 343 +++++++++++++++++++++------- Cargo.toml | 26 ++- src/bin/bp-hot.rs | 53 +++++ src/bip43.rs | 435 ++++++++++++++++++++++++++++++++++++ src/hot/command.rs | 229 +++++++++++++++++++ src/hot/mod.rs | 83 +++++++ src/hot/seed.rs | 129 +++++++++++ src/hot/signer.rs | 90 ++++++++ src/lib.rs | 6 + 10 files changed, 1315 insertions(+), 80 deletions(-) create mode 100644 src/bin/bp-hot.rs create mode 100644 src/bip43.rs create mode 100644 src/hot/command.rs create mode 100644 src/hot/mod.rs create mode 100644 src/hot/seed.rs create mode 100644 src/hot/signer.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25aec25..004e641 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,7 @@ jobs: fail-fast: false matrix: feature: + - hot - serde - esplora - electrum diff --git a/Cargo.lock b/Cargo.lock index 28f1437..8d6e00c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -164,9 +199,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474d7cec9d0a1126fad1b224b767fcbf351c23b0309bb21ec210bcfd379926a5" +checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -176,9 +211,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7505fc3cb7acbf42699a43a79dd9caa4ed9e99861dfbb837c5c0fb5a0a8d2980" +checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" dependencies = [ "bindgen", "cc", @@ -232,16 +267,33 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.66", + "syn 2.0.68", "which", ] +[[package]] +name = "bip39" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +dependencies = [ + "bitcoin_hashes 0.11.0", + "serde", + "unicode-normalization", +] + [[package]] name = "bitcoin-io" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" + [[package]] name = "bitcoin_hashes" version = "0.14.0" @@ -254,9 +306,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -270,8 +322,7 @@ dependencies = [ [[package]] name = "bp-consensus" version = "0.11.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f126ed462c6b054ccd027d3ed59fdc0eed5d720cbada79e4d8b7ef8e4779d9b8" +source = "git+https://github.com/BP-WG/bp-core?branch=signer#5ef1851508f6b9e385af83016e25002f5617e7e2" dependencies = [ "amplify", "chrono", @@ -284,11 +335,10 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0feca5f56441ef824fe6238c7718f28a21c74ab945e7ac1d1cc191a6b221e241" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" dependencies = [ "amplify", - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "bp-consensus", "bp-invoice", "commit_verify", @@ -307,7 +357,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.9", + "rustls 0.23.10", "serde", "serde_json", "sha2", @@ -333,12 +383,11 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f051e70a99388097774781b109fc7dd4f4e9b17820be777c2655c37175981b9" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" dependencies = [ "amplify", "bech32", - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "bp-consensus", "serde", ] @@ -346,8 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573dcef793e4058c1a8c3632eb10d0486eff4979a2d753b51e8a06017922d8d7" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" dependencies = [ "amplify", "bp-consensus", @@ -362,19 +410,25 @@ dependencies = [ name = "bp-wallet" version = "0.11.0-beta.6.1" dependencies = [ + "aes-gcm", "amplify", "base64", + "bip39", "bp-electrum", "bp-esplora", "bp-std", "clap", + "colored", "descriptors", "env_logger", "log", "psbt", + "rand", + "rpassword", "serde", "serde_json", "serde_yaml", + "sha2", "shellexpand", "strict_encoding", "toml", @@ -394,9 +448,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332" dependencies = [ "jobserver", "libc", @@ -433,6 +487,16 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -446,9 +510,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.6" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -456,9 +520,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.6" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstream", "anstyle", @@ -468,14 +532,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -499,6 +563,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "commit_encoding_derive" version = "0.11.0-beta.5" @@ -564,9 +638,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.9" @@ -588,7 +672,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -599,7 +683,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -615,8 +699,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5af8b4d31550689f92ccb34142b13b635d8e4316cc4a73a0c7254cf8170d16" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" dependencies = [ "amplify", "bp-derive", @@ -663,9 +746,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "env_filter" @@ -758,6 +841,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.1" @@ -889,6 +982,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -930,9 +1032,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -948,12 +1050,12 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -974,15 +1076,15 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -992,9 +1094,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -1042,6 +1144,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "option-ext" version = "0.2.0" @@ -1060,12 +1168,30 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prettyplease" version = "0.2.20" @@ -1073,14 +1199,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1088,8 +1214,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500d928c69d44b90fa22f3209fb8d349b436ef8973754ccfa085a8db8392e422" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" dependencies = [ "amplify", "base64", @@ -1109,6 +1234,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -1122,9 +1277,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -1134,9 +1289,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -1145,9 +1300,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "ring" @@ -1173,6 +1328,27 @@ dependencies = [ "digest", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1208,9 +1384,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.9" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ "aws-lc-rs", "log", @@ -1251,6 +1427,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" dependencies = [ + "rand", "secp256k1-sys", "serde", ] @@ -1281,14 +1458,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" dependencies = [ "itoa", "ryu", @@ -1341,7 +1518,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1457,9 +1634,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -1474,9 +1651,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -1500,7 +1677,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1536,9 +1713,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -1603,13 +1780,23 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1639,14 +1826,14 @@ dependencies = [ "serde_json", "socks", "url", - "webpki-roots 0.26.2", + "webpki-roots 0.26.3", ] [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -1702,7 +1889,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -1724,7 +1911,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1743,9 +1930,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -1958,5 +2145,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] diff --git a/Cargo.toml b/Cargo.toml index f914460..84338d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,11 @@ name = "bp" path = "src/bin/bp.rs" required-features = ["cli"] +[[bin]] +name = "bp-hot" +path = "src/bin/bp-hot.rs" +required-features = ["hot"] + [lib] name = "bpwallet" @@ -55,11 +60,19 @@ bp-esplora = { workspace = true, optional = true } bp-electrum = { workspace = true, optional = true } psbt = { workspace = true } descriptors = { workspace = true } + +sha2 = "0.10.8" +rand = { version = "0.8.5", optional = true } +rpassword = { version = "7.3.1", optional = true } +aes-gcm = { version = "0.10.3", optional = true } +bip39 = { version = "2.0.0", optional = true } + serde_crate = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true, optional = true } toml = { workspace = true, optional = true } log = { version = "0.4", features = ["max_level_trace", "release_max_level_debug"] } +colored = { version = "2", optional = true } # Cli-only: base64 = { version = "0.22.1", optional = true } @@ -69,10 +82,19 @@ shellexpand = { version = "3.1.0", optional = true } [features] default = [] -all = ["electrum", "esplora", "fs", "cli", "clap", "log"] -cli = ["base64", "env_logger", "clap", "shellexpand", "fs", "serde", "electrum", "esplora", "log"] +all = ["hot", "electrum", "esplora", "fs", "cli", "clap", "log"] +hot = ["bp-std/signers", "bip39", "rand", "aes-gcm", "rpassword"] +cli = ["base64", "env_logger", "clap", "shellexpand", "fs", "serde", "electrum", "esplora", "log", "colored"] log = ["env_logger"] electrum = ["bp-electrum", "serde", "serde_json"] esplora = ["bp-esplora"] fs = ["serde"] serde = ["serde_crate", "serde_yaml", "toml", "bp-std/serde"] + +[patch.crates-io] +bp-consensus = { git = "https://github.com/BP-WG/bp-core", branch = "signer" } +bp-derive = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } +bp-invoice = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } +descriptors = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } +psbt = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } +bp-std = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } diff --git a/src/bin/bp-hot.rs b/src/bin/bp-hot.rs new file mode 100644 index 0000000..d24661a --- /dev/null +++ b/src/bin/bp-hot.rs @@ -0,0 +1,53 @@ +// Modern, minimalistic & standard-compliant hot wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2020-2023 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved. +// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate log; +extern crate serde_crate as serde; + +use std::process::ExitCode; + +use bpwallet::cli::LogLevel; +use bpwallet::hot::{DataError, HotArgs}; +use clap::Parser; + +fn main() -> ExitCode { + if let Err(err) = run() { + eprintln!("Error: {err}"); + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} + +fn run() -> Result<(), DataError> { + let args = HotArgs::parse(); + LogLevel::from_verbosity_flag_count(args.verbose).apply(); + trace!("Command-line arguments: {:#?}", &args); + + eprintln!("BP: command-line tool for working with seeds and private keys in bitcoin protocol"); + eprintln!(" by LNP/BP Standards Association\n"); + + // TODO: Update arguments basing on the configuration + debug!("Executing command: {}", args.command); + args.exec() +} diff --git a/src/bip43.rs b/src/bip43.rs new file mode 100644 index 0000000..f9eb91b --- /dev/null +++ b/src/bip43.rs @@ -0,0 +1,435 @@ +// Modern, minimalistic & standard-compliant cold wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2020-2023 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved. +// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::str::FromStr; + +use bpstd::{DerivationIndex, DerivationPath, HardenedIndex, Idx, IdxBase, NormalIndex}; + +/// Errors in parsing derivation scheme string representation +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Error, Display)] +#[display(doc_comments)] +pub enum ParseBip43Error { + /// invalid blockchain name {0}; it must be either `bitcoin`, `testnet` or + /// hardened index number + InvalidBlockchainName(String), + + /// LNPBP-43 blockchain index {0} must be hardened + UnhardenedBlockchainIndex(u32), + + /// invalid LNPBP-43 identity representation {0} + InvalidIdentityIndex(String), + + /// invalid BIP-43 purpose {0} + InvalidPurposeIndex(String), + + /// BIP-{0} support is not implemented (of BIP with this number does not + /// exist) + UnimplementedBip(u16), + + /// derivation path can't be recognized as one of BIP-43-based standards + UnrecognizedBipScheme, + + /// BIP-43 scheme must have form of `bip43/h` + InvalidBip43Scheme, + + /// BIP-48 scheme must have form of `bip48-native` or `bip48-nested` + InvalidBip48Scheme, + + /// invalid derivation path `{0}` + InvalidDerivationPath(String), +} + +/// Specific derivation scheme after BIP-43 standards +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)] +#[cfg_attr(feature = "clap", derive(ValueEnum))] +#[non_exhaustive] +pub enum Bip43 { + /// Account-based P2PKH derivation. + /// + /// `m / 44' / coin_type' / account'` + #[display("bip44", alt = "m/44h")] + Bip44, + + /// Account-based native P2WPKH derivation. + /// + /// `m / 84' / coin_type' / account'` + #[display("bip84", alt = "m/84h")] + Bip84, + + /// Account-based legacy P2WPH-in-P2SH derivation. + /// + /// `m / 49' / coin_type' / account'` + #[display("bip49", alt = "m/49h")] + Bip49, + + /// Account-based single-key P2TR derivation. + /// + /// `m / 86' / coin_type' / account'` + #[display("bip86", alt = "m/86h")] + Bip86, + + /// Cosigner-index-based multisig derivation. + /// + /// `m / 45' / cosigner_index + #[display("bip45", alt = "m/45h")] + Bip45, + + /// Account-based multisig derivation with sorted keys & P2WSH nested. + /// scripts + /// + /// `m / 48' / coin_type' / account' / 1'` + #[display("bip48-nested", alt = "m/48h//1h")] + Bip48Nested, + + /// Account-based multisig derivation with sorted keys & P2WSH native. + /// scripts + /// + /// `m / 48' / coin_type' / account' / 2'` + #[display("bip48-native", alt = "m/48h//2h")] + Bip48Native, + + /// Account- & descriptor-based derivation for multi-sig wallets. + /// + /// `m / 87' / coin_type' / account'` + #[display("bip87", alt = "m/87h")] + Bip87, + + /// Generic BIP43 derivation with custom (non-standard) purpose value. + /// + /// `m / purpose'` + #[display("bip43/{purpose}", alt = "m/{purpose}")] + #[clap(skip)] + Bip43 { + /// Purpose value + purpose: HardenedIndex, + }, +} + +impl FromStr for Bip43 { + type Err = ParseBip43Error; + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + let bip = s.strip_prefix("bip").or_else(|| s.strip_prefix("m/")); + Ok(match bip { + Some("44") => Bip43::Bip44, + Some("84") => Bip43::Bip84, + Some("49") => Bip43::Bip49, + Some("86") => Bip43::Bip86, + Some("45") => Bip43::Bip45, + Some(bip48) if bip48.starts_with("48//") => match bip48 + .strip_prefix("48//") + .and_then(|index| HardenedIndex::from_str(index).ok()) + { + Some(script_type) if script_type == 1u8 => Bip43::Bip48Nested, + Some(script_type) if script_type == 2u8 => Bip43::Bip48Native, + _ => { + return Err(ParseBip43Error::InvalidBip48Scheme); + } + }, + Some("48-nested") => Bip43::Bip48Nested, + Some("48-native") => Bip43::Bip48Native, + Some("87") => Bip43::Bip87, + Some(bip43) if bip43.starts_with("43/") => match bip43.strip_prefix("43/") { + Some(purpose) => { + let purpose = HardenedIndex::from_str(purpose) + .map_err(|_| ParseBip43Error::InvalidPurposeIndex(purpose.to_owned()))?; + Bip43::Bip43 { purpose } + } + None => return Err(ParseBip43Error::InvalidBip43Scheme), + }, + Some(_) | None => return Err(ParseBip43Error::UnrecognizedBipScheme), + }) + } +} + +impl Bip43 { + /// Constructs derivation standard corresponding to a single-sig P2PKH. + pub const PKH: Bip43 = Bip43::Bip44; + /// Constructs derivation standard corresponding to a single-sig + /// P2WPKH-in-P2SH. + pub const WPKH_SH: Bip43 = Bip43::Bip49; + /// Constructs derivation standard corresponding to a single-sig P2WPKH. + pub const WPKH: Bip43 = Bip43::Bip84; + /// Constructs derivation standard corresponding to a single-sig P2TR. + pub const TR_SINGLE: Bip43 = Bip43::Bip86; + /// Constructs derivation standard corresponding to a multi-sig P2SH BIP45. + pub const MULTI_SH_SORTED: Bip43 = Bip43::Bip45; + /// Constructs derivation standard corresponding to a multi-sig sorted + /// P2WSH-in-P2SH. + pub const MULTI_WSH_SH: Bip43 = Bip43::Bip48Nested; + /// Constructs derivation standard corresponding to a multi-sig sorted + /// P2WSH. + pub const MULTI_WSH: Bip43 = Bip43::Bip48Native; + /// Constructs derivation standard corresponding to a multi-sig BIP87. + pub const DESCRIPTOR: Bip43 = Bip43::Bip87; +} + +/// Methods for derivation standard enumeration types. +pub trait DerivationStandard: Eq + Clone { + /// Deduces derivation standard used by the provided derivation path, if + /// possible. + fn deduce(derivation: &DerivationPath) -> Option + where Self: Sized; + + /// Get hardened index matching BIP-43 purpose value, if any. + fn purpose(&self) -> Option; + + /// Depth of the account extended public key according to the given + /// standard. + /// + /// Returns `None` if the standard does not provide information on + /// account-level xpubs. + fn account_depth(&self) -> Option; + + /// Depth of the derivation path defining `coin_type` key, i.e. the used + /// blockchain. + /// + /// Returns `None` if the standard does not provide information on + /// blockchain/coin type. + fn coin_type_depth(&self) -> Option; + + /// Returns information whether the account xpub in this standard is the + /// last hardened derivation path step, or there might be more hardened + /// steps (like `script_type` in BIP-48). + /// + /// Returns `None` if the standard does not provide information on + /// account-level xpubs. + fn is_account_last_hardened(&self) -> Option; + + /// Checks which bitcoin network corresponds to a given derivation path + /// according to the used standard requirements. + fn is_testnet(&self, path: &DerivationPath) -> Result>; + + /// Extracts hardened index from a derivation path position defining coin + /// type information (used blockchain), if present. + /// + /// # Returns + /// + /// - `Err(None)` error if the path doesn't contain any coin index information; + /// - `Err(`[`NormalIndex`]`)` error if the coin type in the derivation path was an unhardened + /// index. + /// - `Ok(`[`HardenedIndex`]`)` with the coin type index otherwise. + fn extract_coin_type( + &self, + path: &DerivationPath, + ) -> Result> { + let coin = self.coin_type_depth().and_then(|i| path.get(i as usize)).ok_or(None)?; + match coin { + DerivationIndex::Normal(idx) => Err(Some(*idx)), + DerivationIndex::Hardened(idx) => Ok(*idx), + } + } + + /// Extracts hardened index from a derivation path position defining account + /// number, if present. + /// + /// # Returns + /// + /// - `Err(None)` error if the path doesn't contain any account number information; + /// - `Err(`[`NormalIndex`]`)` error if the account number in the derivation path was an + /// unhardened index. + /// - `Ok(`[`HardenedIndex`]`)` with the account number otherwise. + fn extract_account_index( + &self, + path: &DerivationPath, + ) -> Result> { + let coin = self.account_depth().and_then(|i| path.get(i as usize)).ok_or(None)?; + match coin { + DerivationIndex::Normal(idx) => Err(Some(*idx)), + DerivationIndex::Hardened(idx) => Ok(*idx), + } + } + + /// Returns string representation of the template derivation path for an + /// account-level keys. Account key is represented by `*` wildcard fragment. + fn account_template_string(&self, testnet: bool) -> String; + + /// Construct derivation path for the account xpub. + fn to_origin_derivation(&self, testnet: bool) -> DerivationPath; + + /// Construct derivation path up to the provided account index segment. + fn to_account_derivation( + &self, + account_index: HardenedIndex, + testnet: bool, + ) -> DerivationPath; + + /// Construct full derivation path including address index and case + /// (main, change etc). + fn to_key_derivation( + &self, + account_index: HardenedIndex, + testnet: bool, + keychain: NormalIndex, + index: NormalIndex, + ) -> DerivationPath; +} + +impl DerivationStandard for Bip43 { + fn deduce(derivation: &DerivationPath) -> Option { + let mut iter = derivation.into_iter(); + let first = iter.next().map(HardenedIndex::try_from).transpose().ok()??; + let fourth = iter.nth(3).map(HardenedIndex::try_from); + Some(match (first.child_number(), fourth) { + (44, ..) => Bip43::Bip44, + (84, ..) => Bip43::Bip84, + (49, ..) => Bip43::Bip49, + (86, ..) => Bip43::Bip86, + (45, ..) => Bip43::Bip45, + (87, ..) => Bip43::Bip87, + (48, Some(Ok(script_type))) if script_type == 1u8 => Bip43::Bip48Nested, + (48, Some(Ok(script_type))) if script_type == 2u8 => Bip43::Bip48Native, + (48, _) => return None, + (purpose, ..) if derivation.len() > 2 && purpose > 2 => Bip43::Bip43 { + purpose: HardenedIndex::hardened(purpose as u16), + }, + _ => return None, + }) + } + + fn purpose(&self) -> Option { + Some(match self { + Bip43::Bip44 => HardenedIndex::hardened(44), + Bip43::Bip84 => HardenedIndex::hardened(84), + Bip43::Bip49 => HardenedIndex::hardened(49), + Bip43::Bip86 => HardenedIndex::hardened(86), + Bip43::Bip45 => HardenedIndex::hardened(45), + Bip43::Bip48Nested | Bip43::Bip48Native => HardenedIndex::hardened(48), + Bip43::Bip87 => HardenedIndex::hardened(87), + Bip43::Bip43 { purpose } => *purpose, + }) + } + + fn account_depth(&self) -> Option { + Some(match self { + Bip43::Bip45 => return None, + Bip43::Bip44 + | Bip43::Bip84 + | Bip43::Bip49 + | Bip43::Bip86 + | Bip43::Bip87 + | Bip43::Bip48Nested + | Bip43::Bip48Native + | Bip43::Bip43 { .. } => 3, + }) + } + + fn coin_type_depth(&self) -> Option { + Some(match self { + Bip43::Bip45 => return None, + Bip43::Bip44 + | Bip43::Bip84 + | Bip43::Bip49 + | Bip43::Bip86 + | Bip43::Bip87 + | Bip43::Bip48Nested + | Bip43::Bip48Native + | Bip43::Bip43 { .. } => 2, + }) + } + + fn is_account_last_hardened(&self) -> Option { + Some(match self { + Bip43::Bip45 => false, + Bip43::Bip44 + | Bip43::Bip84 + | Bip43::Bip49 + | Bip43::Bip86 + | Bip43::Bip87 + | Bip43::Bip43 { .. } => true, + Bip43::Bip48Nested | Bip43::Bip48Native => false, + }) + } + + fn is_testnet(&self, path: &DerivationPath) -> Result> { + match self.extract_coin_type(path) { + Err(None) => Err(None), + Err(Some(idx)) => Err(Some(idx.into())), + Ok(HardenedIndex::ZERO) => Ok(false), + Ok(HardenedIndex::ONE) => Ok(true), + Ok(idx) => Err(Some(idx.into())), + } + } + + fn account_template_string(&self, testnet: bool) -> String { + let coin_type = if testnet { HardenedIndex::ONE } else { HardenedIndex::ZERO }; + match self { + Bip43::Bip45 + | Bip43::Bip44 + | Bip43::Bip84 + | Bip43::Bip49 + | Bip43::Bip86 + | Bip43::Bip87 + | Bip43::Bip43 { .. } => format!("{:#}/{}/*h", self, coin_type), + Bip43::Bip48Nested => { + format!("{:#}", self).replace("//", &format!("/{}/*h/", coin_type)) + } + Bip43::Bip48Native => { + format!("{:#}", self).replace("//", &format!("/{}/*h/", coin_type)) + } + } + } + + fn to_origin_derivation(&self, testnet: bool) -> DerivationPath { + let mut path = Vec::with_capacity(2); + if let Some(purpose) = self.purpose() { + path.push(purpose.into()) + } + path.push(if testnet { HardenedIndex::ONE } else { HardenedIndex::ZERO }); + path.into() + } + + fn to_account_derivation( + &self, + account_index: HardenedIndex, + testnet: bool, + ) -> DerivationPath { + let mut path = Vec::with_capacity(4); + path.push(account_index); + if self == &Bip43::Bip48Native { + path.push(HardenedIndex::from(2u8).into()); + } else if self == &Bip43::Bip48Nested { + path.push(HardenedIndex::from(1u8).into()); + } + let mut derivation = self.to_origin_derivation(testnet); + derivation.extend(&path); + derivation + } + + fn to_key_derivation( + &self, + account_index: HardenedIndex, + testnet: bool, + keychain: NormalIndex, + index: NormalIndex, + ) -> DerivationPath { + let mut derivation = self + .to_account_derivation(account_index, testnet) + .into_iter() + .map(DerivationIndex::from) + .collect::(); + derivation.push(keychain.into()); + derivation.push(index.into()); + derivation + } +} diff --git a/src/hot/command.rs b/src/hot/command.rs new file mode 100644 index 0000000..16ffe43 --- /dev/null +++ b/src/hot/command.rs @@ -0,0 +1,229 @@ +// Modern, minimalistic & standard-compliant hot wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2020-2023 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved. +// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::{Path, PathBuf}; + +use amplify::{Display, IoError}; +use bip39::Mnemonic; +use bpstd::{HardenedIndex, XprivAccount}; +use clap::Subcommand; +use colored::Colorize; + +use crate::hot::{DataError, SecureIo, Seed, SeedType}; +use crate::Bip43; + +/// Command-line arguments +#[derive(Parser)] +#[derive(Clone, Eq, PartialEq, Debug)] +#[command(author, version, about)] +pub struct HotArgs { + /// Set verbosity level. + /// + /// Can be used multiple times to increase verbosity. + #[clap(short, long, global = true, action = clap::ArgAction::Count)] + pub verbose: u8, + + /// Command to execute. + #[clap(subcommand)] + pub command: HotCommand, +} + +#[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)] +pub enum HotCommand { + /// Generate new seed and saves it as an encoded file + #[display("seed")] + Seed { + /// File to save generated seed data and extended master key + output_file: PathBuf, + }, + + /// Derive new extended private key from the seed and saves it into a separate file as a new + /// signing account + #[display("derive")] + Derive { + /// Seed file containing extended master key, created previously with `seed` command. + seed_file: PathBuf, + + /// Derivation scheme. + #[clap( + short, + long, + long_help = "Possible values are: +- bip44: used for P2PKH (not recommended) +- bip84: used for P2WPKH +- bip49: used for P2WPKH-in-P2SH +- bip86: used for P2TR with single key (no MuSig, no multisig) +- bip45: used for legacy multisigs (P2SH, not recommended) +- bip48//1h: used for P2WSH-in-P2SH multisigs (deterministic order) +- bip48//2h: used for P2WSH multisigs (deterministic order) +- bip87: used for modern multisigs with descriptors (pre-MuSig) +- bip43/h: any other non-standard purpose field", + default_value = "bip86" + )] + scheme: Bip43, + + /// Account derivation number (should be hardened, i.e. with `h` or `'` suffix). + #[clap(short, long, default_value = "0'")] + account: HardenedIndex, + + /// Use the seed for bitcoin mainnet + #[clap(long)] + mainnet: bool, + + /// Output file for storing account-based extended private key + output_file: PathBuf, + }, + + /// Print information about seed or the signing account. + #[display("info")] + Info { + /// File containing either seed information or extended private key for the account, + /// previously created with `seed` and `derive` commands. + file: PathBuf, + + /// Print private information, including mnemonic, extended private keys and + /// signatures + #[clap(short = 'P', long)] + print_private: bool, + }, + + /// Sign PSBT with the provided account keys + #[display("sign")] + Sign { + /// File containing PSBT + psbt_file: PathBuf, + + /// Signing account file used to (partially co-)sign PSBT + signing_account: PathBuf, + }, +} + +impl HotArgs { + pub fn exec(self) -> Result<(), DataError> { + match self.command { + HotCommand::Seed { output_file } => seed(&output_file)?, + HotCommand::Derive { + seed_file, + scheme, + account, + mainnet, + output_file, + } => derive(&seed_file, scheme, account, mainnet, &output_file)?, + HotCommand::Info { + file, + print_private, + } => info(&file, print_private)?, + HotCommand::Sign { .. } => { + todo!() + } + }; + Ok(()) + } +} + +fn seed(output_file: &Path) -> Result<(), IoError> { + let seed = Seed::random(SeedType::Bit128); + let seed_password = rpassword::prompt_password("Seed password")?; + seed.write(output_file, &seed_password)?; + + info_seed(seed, false); + + Ok(()) +} + +fn info(file: &Path, print_private: bool) -> Result<(), IoError> { + let password = rpassword::prompt_password("File password")?; + if let Ok(seed) = Seed::read(file, &password) { + info_seed(seed, print_private) + } else if let Ok(account) = XprivAccount::read(file, &password) { + info_account(account, print_private) + } else { + eprintln!("{} can't detect file format for `{}`", "Error:".bright_red(), file.display()); + } + Ok(()) +} + +fn info_seed(seed: Seed, print_private: bool) { + if print_private { + let mnemonic = Mnemonic::from_entropy(seed.as_entropy()).expect("invalid seed"); + println!("\n{:-18} {}", "Mnemonic:".bright_white(), mnemonic.to_string().black().dimmed()); + } + + let xpriv = seed.master_xpriv(false); + let xpub = xpriv.to_xpub(); + + println!("{}", "Master key:".bright_white()); + println!( + "{:-18} {}", + " - fingerprint:".bright_white(), + xpub.fingerprint().to_string().bright_green() + ); + println!("{:-18} {}", " - mainnet:", if xpub.is_testnet() { "no" } else { "yes" }); + println!("{:-18} {}", " - id:".bright_white(), xpub.identifier()); + if print_private { + println!("{:-18} {}", " - xprv:".bright_white(), xpriv.to_string().black().dimmed()); + } + println!("{:-18} {}", " - xpub:".bright_white(), xpub.to_string().bright_green()); +} + +fn info_account(account: XprivAccount, print_private: bool) { + let xpub = account.to_xpub_account(); + println!("\n{} {}", "Account:".bright_white(), xpub); + println!( + "{:-18} {}", + " - fingerprint:".bright_white(), + xpub.account_fp().to_string().bright_green() + ); + println!("{:-18} {}", " - id:".bright_white(), xpub.account_id()); + println!("{:-18} [{}]", " - key origin:".bright_white(), xpub.origin(),); + if print_private { + let account_xpriv = account.xpriv(); + println!( + "{:-18} {}", + " - xpriv:".bright_white(), + account_xpriv.to_string().black().dimmed() + ); + // TODO: Add Zpriv etc + } + println!("{:-18} {}", " - xpub:".bright_white(), xpub.to_string().bright_green()); + // TODO: Add Zpub etc +} + +fn derive( + seed_file: &Path, + scheme: Bip43, + account: HardenedIndex, + mainnet: bool, + output_file: &Path, +) -> Result<(), DataError> { + let seed_password = rpassword::prompt_password("Seed password")?; + let account_password = rpassword::prompt_password("Account password")?; + + let seed = Seed::read(seed_file, &seed_password)?; + let account = seed.derive(scheme, !mainnet, account); + + account.write(output_file, &account_password)?; + + info_account(account, false); + + Ok(()) +} diff --git a/src/hot/mod.rs b/src/hot/mod.rs new file mode 100644 index 0000000..7575af7 --- /dev/null +++ b/src/hot/mod.rs @@ -0,0 +1,83 @@ +// Modern, minimalistic & standard-compliant hot wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2020-2024 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2020-2024 LNP/BP Standards Association. All rights reserved. +// Copyright (C) 2020-2024 Dr Maxim Orlovsky. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod seed; +mod command; +mod signer; + +pub use command::{HotArgs, HotCommand}; +pub use io::{decrypt, encrypt, DataError, SecureIo}; +pub use seed::{Seed, SeedType}; + +mod io { + use std::io; + use std::path::Path; + + use aes_gcm::aead::{Aead, Nonce, OsRng}; + use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; + use amplify::IoError; + use sha2::{Digest, Sha256}; + + pub fn encrypt(source: Vec, key: impl AsRef<[u8]>) -> Vec { + let key = Sha256::digest(key.as_ref()); + let key = aes_gcm::Key::::from_slice(key.as_slice()); + + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let cipher = Aes256Gcm::new(key); + + let ciphered_data = cipher.encrypt(&nonce, source.as_ref()).expect("failed to encrypt"); + + let mut data = nonce.to_vec(); + data.extend(ciphered_data); + data + } + + pub fn decrypt(encrypted: &[u8], key: impl AsRef<[u8]>) -> Result, aes_gcm::Error> { + let key = Sha256::digest(key.as_ref()); + let key = aes_gcm::Key::::from_slice(key.as_slice()); + let nonce = Nonce::::from_slice(&encrypted[..12]); + Aes256Gcm::new(key).decrypt(&nonce, &encrypted[12..]) + } + + #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] + #[display(doc_comments)] + pub enum DataError { + #[from] + #[from(io::Error)] + #[display(inner)] + Io(IoError), + + #[from(aes_gcm::Error)] + /// invalid password. + Password, + } + + pub trait SecureIo { + fn read

(file: P, password: &str) -> Result + where + P: AsRef, + Self: Sized; + + fn write

(&self, file: P, password: &str) -> io::Result<()> + where P: AsRef; + } +} diff --git a/src/hot/seed.rs b/src/hot/seed.rs new file mode 100644 index 0000000..1a3b3ea --- /dev/null +++ b/src/hot/seed.rs @@ -0,0 +1,129 @@ +// Modern, minimalistic & standard-compliant hot wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2020-2024 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2020-2024 LNP/BP Standards Association. All rights reserved. +// Copyright (C) 2020-2024 Dr Maxim Orlovsky. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::Path; +use std::str::FromStr; +use std::{fs, io}; + +use bip39::Mnemonic; +use bpstd::{HardenedIndex, XkeyOrigin, Xpriv, XprivAccount}; +use rand::RngCore; + +use crate::bip43::DerivationStandard; +use crate::hot::{decrypt, encrypt, DataError, SecureIo}; +use crate::Bip43; + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] +#[repr(u16)] +pub enum SeedType { + Bit128 = 128, + Bit160 = 160, + Bit192 = 192, + Bit224 = 224, + Bit256 = 256, +} + +impl SeedType { + #[inline] + pub fn bit_len(self) -> usize { self as usize } + + #[inline] + pub fn byte_len(self) -> usize { + match self { + SeedType::Bit128 => 16, + SeedType::Bit160 => 160 / 8, + SeedType::Bit192 => 192 / 8, + SeedType::Bit224 => 224 / 8, + SeedType::Bit256 => 32, + } + } + + #[inline] + pub fn word_len(self) -> usize { + match self { + SeedType::Bit128 => 12, + SeedType::Bit160 => 15, + SeedType::Bit192 => 18, + SeedType::Bit224 => 21, + SeedType::Bit256 => 24, + } + } +} + +pub struct Seed(Box<[u8]>); + +impl Seed { + pub fn random(seed_type: SeedType) -> Seed { + let mut entropy = vec![0u8; seed_type.byte_len()]; + rand::thread_rng().fill_bytes(&mut entropy); + Seed(Box::from(entropy)) + } + + #[inline] + pub fn as_entropy(&self) -> &[u8] { &self.0 } + + #[inline] + pub fn master_xpriv(&self, testnet: bool) -> Xpriv { + Xpriv::new_master(testnet, self.as_entropy()) + } + + pub fn derive(&self, scheme: Bip43, testnet: bool, account: HardenedIndex) -> XprivAccount { + let master_xpriv = self.master_xpriv(testnet); + let master_xpub = master_xpriv.to_xpub(); + let derivation = scheme.to_account_derivation(account, testnet); + let account_xpriv = master_xpriv.derive_priv(derivation.as_ref()); + + let origin = XkeyOrigin::new(master_xpub.fingerprint(), derivation); + XprivAccount::new(account_xpriv, origin) + } +} + +impl SecureIo for Seed { + fn read

(file: P, password: &str) -> Result + where P: AsRef { + let data = fs::read(file)?; + let data = decrypt(&data, password)?; + let s = String::from_utf8(data).map_err(|_| DataError::Password)?; + let mnemonic = Mnemonic::from_str(&s).map_err(|_| DataError::Password)?; + Ok(Seed(Box::from(mnemonic.to_entropy()))) + } + + fn write

(&self, file: P, password: &str) -> io::Result<()> + where P: AsRef { + fs::write(file, encrypt(self.0.to_vec(), password)) + } +} + +impl SecureIo for XprivAccount { + fn read

(file: P, password: &str) -> Result + where P: AsRef { + let data = fs::read(file)?; + let data = decrypt(&data, password)?; + let s = String::from_utf8(data).map_err(|_| DataError::Password)?; + XprivAccount::from_str(&s).map_err(|_| DataError::Password) + } + + fn write

(&self, file: P, password: &str) -> io::Result<()> + where P: AsRef { + fs::write(file, encrypt(self.to_string().into_bytes(), password)) + } +} diff --git a/src/hot/signer.rs b/src/hot/signer.rs new file mode 100644 index 0000000..bb94bec --- /dev/null +++ b/src/hot/signer.rs @@ -0,0 +1,90 @@ +// Modern, minimalistic & standard-compliant hot wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2020-2024 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2020-2024 LNP/BP Standards Association. All rights reserved. +// Copyright (C) 2020-2024 Dr Maxim Orlovsky. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use bpstd::secp256k1::ecdsa::Signature; +use bpstd::{ + Address, KeyOrigin, LegacyPk, Satisfy, Sats, Sighash, TapLeafHash, TapMerklePath, TapSighash, + XOnlyPk, XkeyOrigin, Xpriv, +}; +use descriptors::Descriptor; +use psbt::{Psbt, Rejected, Sign}; + +pub struct SignTxInfo { + pub fee: Sats, + pub inputs: Sats, + pub beneficiaries: HashSet, +} + +pub struct ConsoleSigner<'descr, 'me, D: Descriptor> +where Self: 'me +{ + descriptor: &'descr D, + origin: XkeyOrigin, + xpriv: Xpriv, + satisfier: XprivSatisfier<'me>, +} + +pub struct XprivSatisfier<'xpriv> { + xpriv: &'xpriv Xpriv, + // TODO: Support key- and script-path selection +} + +impl<'descr, 'me, D: Descriptor> Sign for ConsoleSigner<'descr, 'me, D> +where Self: 'me +{ + type Satisfier<'s> = &'s XprivSatisfier<'s> where Self: 's + 'me; + + fn approve(&self, _psbt: &Psbt) -> Result, Rejected> { Ok(&self.satisfier) } +} + +impl<'a, 'xpriv> Satisfy for &'a XprivSatisfier<'xpriv> { + fn signature_ecdsa( + &self, + message: Sighash, + pk: LegacyPk, + origin: Option<&KeyOrigin>, + ) -> Option { + todo!() + } + + fn signature_bip340( + &self, + message: TapSighash, + pk: XOnlyPk, + origin: Option<&KeyOrigin>, + ) -> Option { + todo!() + } + + fn should_satisfy_script_path( + &self, + index: usize, + merkle_path: &TapMerklePath, + leaf: TapLeafHash, + ) -> bool { + todo!() + } + + fn should_satisfy_key_path(&self, index: usize) -> bool { todo!() } +} diff --git a/src/lib.rs b/src/lib.rs index 9fc81ed..c71b9f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,11 +40,17 @@ mod layer2; pub mod coinselect; #[cfg(feature = "cli")] pub mod cli; +#[cfg(feature = "hot")] +pub mod hot; +mod bip43; +pub use bip43::{Bip43, DerivationStandard, ParseBip43Error}; pub use data::{ BlockHeight, BlockInfo, MiningInfo, Party, TxCredit, TxDebit, TxStatus, WalletAddr, WalletTx, WalletUtxo, }; +#[cfg(feature = "hot")] +pub use hot::{HotArgs, HotCommand, Seed, SeedType}; pub use indexers::Indexer; #[cfg(any(feature = "electrum", feature = "esplora"))] pub use indexers::{AnyIndexer, AnyIndexerError}; From e3831a0eb730d2c4fece4a986df643ce4e983bad Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 30 Jun 2024 11:14:02 +0200 Subject: [PATCH 02/31] hot: improve passwords workflow and security --- Cargo.lock | 10 ++++----- src/hot/command.rs | 50 +++++++++++++++++++++++++++++++++++++++------ src/hot/mod.rs | 14 ++++++++++--- src/hot/password.rs | 29 ++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/hot/password.rs diff --git a/Cargo.lock b/Cargo.lock index 8d6e00c..19a9538 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" dependencies = [ "amplify", "bitcoin_hashes 0.14.0", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" dependencies = [ "amplify", "bech32", @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" dependencies = [ "amplify", "bp-consensus", @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" dependencies = [ "amplify", "bp-derive", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#bc4c99c26f5be1fc24066e6c141045c92d7041ea" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" dependencies = [ "amplify", "base64", diff --git a/src/hot/command.rs b/src/hot/command.rs index 16ffe43..23df32c 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -28,7 +28,7 @@ use bpstd::{HardenedIndex, XprivAccount}; use clap::Subcommand; use colored::Colorize; -use crate::hot::{DataError, SecureIo, Seed, SeedType}; +use crate::hot::{calculate_entropy, DataError, SecureIo, Seed, SeedType}; use crate::Bip43; /// Command-line arguments @@ -60,6 +60,11 @@ pub enum HotCommand { /// signing account #[display("derive")] Derive { + /// Do not ask for a password and default to an empty-line password. For testing purposes + /// only. + #[clap(short = 'N', long, conflicts_with = "mainnet")] + no_password: bool, + /// Seed file containing extended master key, created previously with `seed` command. seed_file: PathBuf, @@ -109,6 +114,11 @@ pub enum HotCommand { /// Sign PSBT with the provided account keys #[display("sign")] Sign { + /// Do not ask for a password and default to an empty-line password. For testing purposes + /// only. + #[clap(short = 'N', long)] + no_password: bool, + /// File containing PSBT psbt_file: PathBuf, @@ -122,12 +132,13 @@ impl HotArgs { match self.command { HotCommand::Seed { output_file } => seed(&output_file)?, HotCommand::Derive { + no_password, seed_file, scheme, account, mainnet, output_file, - } => derive(&seed_file, scheme, account, mainnet, &output_file)?, + } => derive(&seed_file, scheme, account, mainnet, &output_file, no_password)?, HotCommand::Info { file, print_private, @@ -142,7 +153,16 @@ impl HotArgs { fn seed(output_file: &Path) -> Result<(), IoError> { let seed = Seed::random(SeedType::Bit128); - let seed_password = rpassword::prompt_password("Seed password")?; + let seed_password = loop { + let seed_password = rpassword::prompt_password("Seed password: ")?; + let entropy = calculate_entropy(&seed_password); + eprintln!("Password entropy: ~{entropy:.0} bits"); + if !seed_password.is_empty() && entropy >= 64.0 { + break seed_password; + } + eprintln!("Entropy is too low, please try with a different password") + }; + seed.write(output_file, &seed_password)?; info_seed(seed, false); @@ -151,7 +171,7 @@ fn seed(output_file: &Path) -> Result<(), IoError> { } fn info(file: &Path, print_private: bool) -> Result<(), IoError> { - let password = rpassword::prompt_password("File password")?; + let password = rpassword::prompt_password("File password: ")?; if let Ok(seed) = Seed::read(file, &password) { info_seed(seed, print_private) } else if let Ok(account) = XprivAccount::read(file, &password) { @@ -214,9 +234,27 @@ fn derive( account: HardenedIndex, mainnet: bool, output_file: &Path, + no_password: bool, ) -> Result<(), DataError> { - let seed_password = rpassword::prompt_password("Seed password")?; - let account_password = rpassword::prompt_password("Account password")?; + let seed_password = rpassword::prompt_password("Seed password: ")?; + + let account_password = if !mainnet && no_password { + s!("") + } else { + loop { + let account_password = rpassword::prompt_password("Account password: ")?; + let entropy = calculate_entropy(&seed_password); + eprintln!("Password entropy: ~{entropy:.0} bits"); + if !account_password.is_empty() && entropy >= 64.0 { + break account_password; + } + if !mainnet { + eprintln!("Entropy is too low, but since we are on testnet we accept that"); + break account_password; + } + eprintln!("Entropy is too low, please try with a different password") + } + }; let seed = Seed::read(seed_file, &seed_password)?; let account = seed.derive(scheme, !mainnet, account); diff --git a/src/hot/mod.rs b/src/hot/mod.rs index 7575af7..be94670 100644 --- a/src/hot/mod.rs +++ b/src/hot/mod.rs @@ -23,9 +23,11 @@ mod seed; mod command; mod signer; +mod password; pub use command::{HotArgs, HotCommand}; pub use io::{decrypt, encrypt, DataError, SecureIo}; +pub use password::calculate_entropy; pub use seed::{Seed, SeedType}; mod io { @@ -35,6 +37,7 @@ mod io { use aes_gcm::aead::{Aead, Nonce, OsRng}; use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use amplify::IoError; + use psbt::{PsbtError, SignError}; use sha2::{Digest, Sha256}; pub fn encrypt(source: Vec, key: impl AsRef<[u8]>) -> Vec { @@ -59,16 +62,21 @@ mod io { } #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] - #[display(doc_comments)] + #[display(inner)] pub enum DataError { #[from] #[from(io::Error)] - #[display(inner)] Io(IoError), + #[display("invalid password.")] #[from(aes_gcm::Error)] - /// invalid password. Password, + + #[from] + Psbt(PsbtError), + + #[from] + Sign(SignError), } pub trait SecureIo { diff --git a/src/hot/password.rs b/src/hot/password.rs new file mode 100644 index 0000000..352b268 --- /dev/null +++ b/src/hot/password.rs @@ -0,0 +1,29 @@ +// Taken from https://github.com/dewan-ahmed/PassMeRust/blob/main/src/entropy.rs + +pub fn calculate_entropy(password: &str) -> f64 { + let charset = calculate_charset(password); + let length = password.len(); + + length as f64 * charset.log2() +} + +fn calculate_charset(password: &str) -> f64 { + let mut charset = 0u32; + + if password.bytes().any(|byte| byte >= b'0' && byte <= b'9') { + charset += 10; // Numbers + } + if password.bytes().any(|byte| byte >= b'a' && byte <= b'z') { + charset += 26; // Lowercase letters + } + if password.bytes().any(|byte| byte >= b'A' && byte <= b'Z') { + charset += 26; // Uppercase letters + } + if password.bytes().any(|byte| { + byte < b'0' || (byte > b'9' && byte < b'A') || (byte > b'Z' && byte < b'a') || byte > b'z' + }) { + charset += 33; // Special characters, rough estimation + } + + charset as f64 +} From c50d7288a260e5b39010d4674767eb9bfddfce42 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 30 Jun 2024 11:14:16 +0200 Subject: [PATCH 03/31] hot: add PSBT signing with testnet signer --- src/hot/command.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/hot/command.rs b/src/hot/command.rs index 23df32c..4d0e825 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -20,13 +20,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fs; use std::path::{Path, PathBuf}; use amplify::{Display, IoError}; use bip39::Mnemonic; +use bpstd::signers::TestSigner; use bpstd::{HardenedIndex, XprivAccount}; use clap::Subcommand; use colored::Colorize; +use psbt::Psbt; use crate::hot::{calculate_entropy, DataError, SecureIo, Seed, SeedType}; use crate::Bip43; @@ -143,9 +146,11 @@ impl HotArgs { file, print_private, } => info(&file, print_private)?, - HotCommand::Sign { .. } => { - todo!() - } + HotCommand::Sign { + no_password, + psbt_file, + signing_account, + } => sign(&psbt_file, &signing_account, no_password)?, }; Ok(()) } @@ -265,3 +270,28 @@ fn derive( Ok(()) } + +fn sign(psbt_file: &Path, account_file: &Path, no_password: bool) -> Result<(), DataError> { + let password = if no_password { s!("") } else { rpassword::prompt_password("Password: ")? }; + let account = XprivAccount::read(account_file, &password)?; + + let data = fs::read(psbt_file)?; + let mut psbt = Psbt::deserialize(&data)?; + + eprintln!("PSBT version: {:#}", psbt.version); + eprintln!("Transaction id: {}", psbt.txid()); + eprintln!("Signing key: {}", account.to_xpub_account()); + eprintln!("Signing using testnet signer"); + + let signer = TestSigner::new(&account); + let sig_count = psbt.sign(&signer)?; + + fs::write(psbt_file, psbt.serialize(psbt.version))?; + eprintln!( + "Done {} signatures, saved to {}\n", + sig_count.to_string().bright_green(), + psbt_file.display() + ); + println!("\n{}\n", psbt); + Ok(()) +} From 510b28f4058eb00305bd4e32c9045bbcbe542dbb Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 30 Jun 2024 11:17:19 +0200 Subject: [PATCH 04/31] hot: improve doc comments --- src/hot/command.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/hot/command.rs b/src/hot/command.rs index 4d0e825..78591c5 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -39,13 +39,13 @@ use crate::Bip43; #[derive(Clone, Eq, PartialEq, Debug)] #[command(author, version, about)] pub struct HotArgs { - /// Set verbosity level. + /// Set verbosity level /// - /// Can be used multiple times to increase verbosity. + /// Can be used multiple times to increase verbosity #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - /// Command to execute. + /// Command to execute #[clap(subcommand)] pub command: HotCommand, } @@ -64,11 +64,11 @@ pub enum HotCommand { #[display("derive")] Derive { /// Do not ask for a password and default to an empty-line password. For testing purposes - /// only. + /// only #[clap(short = 'N', long, conflicts_with = "mainnet")] no_password: bool, - /// Seed file containing extended master key, created previously with `seed` command. + /// Seed file containing extended master key, created previously with `seed` command seed_file: PathBuf, /// Derivation scheme. @@ -83,14 +83,13 @@ pub enum HotCommand { - bip45: used for legacy multisigs (P2SH, not recommended) - bip48//1h: used for P2WSH-in-P2SH multisigs (deterministic order) - bip48//2h: used for P2WSH multisigs (deterministic order) -- bip87: used for modern multisigs with descriptors (pre-MuSig) -- bip43/h: any other non-standard purpose field", +- bip87: used for modern multisigs with descriptors (pre-MuSig)", default_value = "bip86" )] scheme: Bip43, - /// Account derivation number (should be hardened, i.e. with `h` or `'` suffix). - #[clap(short, long, default_value = "0'")] + /// Account derivation number (should be hardened, i.e. with `h` suffix) + #[clap(short, long, default_value = "0h")] account: HardenedIndex, /// Use the seed for bitcoin mainnet @@ -101,11 +100,11 @@ pub enum HotCommand { output_file: PathBuf, }, - /// Print information about seed or the signing account. + /// Print information about seed or the signing account #[display("info")] Info { /// File containing either seed information or extended private key for the account, - /// previously created with `seed` and `derive` commands. + /// previously created with `seed` and `derive` commands file: PathBuf, /// Print private information, including mnemonic, extended private keys and From 55ac67e878a1b6dd430299c7aed2d380b52056ab Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Thu, 4 Jul 2024 15:51:06 +0200 Subject: [PATCH 05/31] bip43: fix use of clap macro feature gate --- src/bip43.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bip43.rs b/src/bip43.rs index f9eb91b..0228aa6 100644 --- a/src/bip43.rs +++ b/src/bip43.rs @@ -117,7 +117,7 @@ pub enum Bip43 { /// /// `m / purpose'` #[display("bip43/{purpose}", alt = "m/{purpose}")] - #[clap(skip)] + #[cfg_attr(feature = "clap", clap(skip))] Bip43 { /// Purpose value purpose: HardenedIndex, From da4e224fd3b5327adba0798204cf3c10f0f37992 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Thu, 4 Jul 2024 16:18:44 +0200 Subject: [PATCH 06/31] chore: fix feature-flags builds --- .github/workflows/build.yml | 1 + Cargo.toml | 4 ++-- convert/Cargo.toml | 2 +- src/hot/mod.rs | 5 ++++- src/lib.rs | 4 +++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 004e641..2f46509 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,7 @@ jobs: - electrum - fs - cli + - cli,hot - serde,esplora steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 84338d2..9d61b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ categories = ["cryptography::cryptocurrencies"] authors = ["Dr Maxim Orlovsky "] homepage = "https://lnp-bp.org" repository = "https://github.com/BP-WG/bp-wallet" -rust-version = "1.70.0" # Due to clap +rust-version = "1.75.0" # Due to bp-std edition = "2021" license = "Apache-2.0" @@ -47,7 +47,7 @@ required-features = ["cli"] [[bin]] name = "bp-hot" path = "src/bin/bp-hot.rs" -required-features = ["hot"] +required-features = ["hot", "cli"] [lib] name = "bpwallet" diff --git a/convert/Cargo.toml b/convert/Cargo.toml index e4b1b97..3a8d64e 100644 --- a/convert/Cargo.toml +++ b/convert/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" authors = ["Dr Maxim Orlovsky "] homepage = "https://lnp-bp.org" repository = "https://github.com/BP-WG/bp-wallet" -rust-version = "1.60.0" # Due to rust-amplify +rust-version = "1.75.0" # Due to bp-std edition = "2021" license = "Apache-2.0" diff --git a/src/hot/mod.rs b/src/hot/mod.rs index be94670..301c269 100644 --- a/src/hot/mod.rs +++ b/src/hot/mod.rs @@ -21,10 +21,13 @@ // limitations under the License. mod seed; +#[cfg(feature = "cli")] mod command; -mod signer; +#[cfg(feature = "cli")] +pub mod signer; mod password; +#[cfg(feature = "cli")] pub use command::{HotArgs, HotCommand}; pub use io::{decrypt, encrypt, DataError, SecureIo}; pub use password::calculate_entropy; diff --git a/src/lib.rs b/src/lib.rs index c71b9f5..638aa3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,8 +49,10 @@ pub use data::{ BlockHeight, BlockInfo, MiningInfo, Party, TxCredit, TxDebit, TxStatus, WalletAddr, WalletTx, WalletUtxo, }; +#[cfg(all(feature = "cli", feature = "hot"))] +pub use hot::{HotArgs, HotCommand}; #[cfg(feature = "hot")] -pub use hot::{HotArgs, HotCommand, Seed, SeedType}; +pub use hot::{Seed, SeedType}; pub use indexers::Indexer; #[cfg(any(feature = "electrum", feature = "esplora"))] pub use indexers::{AnyIndexer, AnyIndexerError}; From 7ea023d44245a67fae5f3d85ecae097a4a718662 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 13 Jul 2024 22:37:09 +0200 Subject: [PATCH 07/31] hot: update to use refactored signer types --- Cargo.lock | 11 ++++++----- src/hot/command.rs | 4 ++-- src/hot/seed.rs | 2 +- src/hot/signer.rs | 24 ++++++++++++------------ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19a9538..b95d26f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" dependencies = [ "amplify", "bitcoin_hashes 0.14.0", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" dependencies = [ "amplify", "bech32", @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" dependencies = [ "amplify", "bp-consensus", @@ -403,6 +403,7 @@ dependencies = [ "bp-invoice", "descriptors", "psbt", + "secp256k1", "serde", ] @@ -699,7 +700,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" dependencies = [ "amplify", "bp-derive", @@ -1214,7 +1215,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#e92197460ac85d81d812e86b0e347fcc7be3c371" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" dependencies = [ "amplify", "base64", diff --git a/src/hot/command.rs b/src/hot/command.rs index 78591c5..13e4db5 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -25,7 +25,7 @@ use std::path::{Path, PathBuf}; use amplify::{Display, IoError}; use bip39::Mnemonic; -use bpstd::signers::TestSigner; +use bpstd::signers::TestnetRefSigner; use bpstd::{HardenedIndex, XprivAccount}; use clap::Subcommand; use colored::Colorize; @@ -282,7 +282,7 @@ fn sign(psbt_file: &Path, account_file: &Path, no_password: bool) -> Result<(), eprintln!("Signing key: {}", account.to_xpub_account()); eprintln!("Signing using testnet signer"); - let signer = TestSigner::new(&account); + let signer = TestnetRefSigner::new(&account); let sig_count = psbt.sign(&signer)?; fs::write(psbt_file, psbt.serialize(psbt.version))?; diff --git a/src/hot/seed.rs b/src/hot/seed.rs index 1a3b3ea..e0841ea 100644 --- a/src/hot/seed.rs +++ b/src/hot/seed.rs @@ -90,7 +90,7 @@ impl Seed { let master_xpriv = self.master_xpriv(testnet); let master_xpub = master_xpriv.to_xpub(); let derivation = scheme.to_account_derivation(account, testnet); - let account_xpriv = master_xpriv.derive_priv(derivation.as_ref()); + let account_xpriv = master_xpriv.derive_priv(&derivation); let origin = XkeyOrigin::new(master_xpub.fingerprint(), derivation); XprivAccount::new(account_xpriv, origin) diff --git a/src/hot/signer.rs b/src/hot/signer.rs index bb94bec..f7fa4c8 100644 --- a/src/hot/signer.rs +++ b/src/hot/signer.rs @@ -24,11 +24,11 @@ use std::collections::HashSet; use bpstd::secp256k1::ecdsa::Signature; use bpstd::{ - Address, KeyOrigin, LegacyPk, Satisfy, Sats, Sighash, TapLeafHash, TapMerklePath, TapSighash, + Address, KeyOrigin, LegacyPk, Sats, Sighash, Sign, TapLeafHash, TapMerklePath, TapSighash, XOnlyPk, XkeyOrigin, Xpriv, }; use descriptors::Descriptor; -use psbt::{Psbt, Rejected, Sign}; +use psbt::{Psbt, Rejected, Signer}; pub struct SignTxInfo { pub fee: Sats, @@ -42,24 +42,24 @@ where Self: 'me descriptor: &'descr D, origin: XkeyOrigin, xpriv: Xpriv, - satisfier: XprivSatisfier<'me>, + signer: XprivSigner<'me>, } -pub struct XprivSatisfier<'xpriv> { +pub struct XprivSigner<'xpriv> { xpriv: &'xpriv Xpriv, // TODO: Support key- and script-path selection } -impl<'descr, 'me, D: Descriptor> Sign for ConsoleSigner<'descr, 'me, D> +impl<'descr, 'me, D: Descriptor> Signer for ConsoleSigner<'descr, 'me, D> where Self: 'me { - type Satisfier<'s> = &'s XprivSatisfier<'s> where Self: 's + 'me; + type Sign<'s> = &'s XprivSigner<'s> where Self: 's + 'me; - fn approve(&self, _psbt: &Psbt) -> Result, Rejected> { Ok(&self.satisfier) } + fn approve(&self, _psbt: &Psbt) -> Result, Rejected> { Ok(&self.signer) } } -impl<'a, 'xpriv> Satisfy for &'a XprivSatisfier<'xpriv> { - fn signature_ecdsa( +impl<'a, 'xpriv> Sign for &'a XprivSigner<'xpriv> { + fn sign_ecdsa( &self, message: Sighash, pk: LegacyPk, @@ -68,7 +68,7 @@ impl<'a, 'xpriv> Satisfy for &'a XprivSatisfier<'xpriv> { todo!() } - fn signature_bip340( + fn sign_bip340( &self, message: TapSighash, pk: XOnlyPk, @@ -77,7 +77,7 @@ impl<'a, 'xpriv> Satisfy for &'a XprivSatisfier<'xpriv> { todo!() } - fn should_satisfy_script_path( + fn should_sign_script_path( &self, index: usize, merkle_path: &TapMerklePath, @@ -86,5 +86,5 @@ impl<'a, 'xpriv> Satisfy for &'a XprivSatisfier<'xpriv> { todo!() } - fn should_satisfy_key_path(&self, index: usize) -> bool { todo!() } + fn should_sign_key_path(&self, index: usize) -> bool { todo!() } } From 60f8c3319f4ceca5edd9d8d9da91fc9e8cb0b63a Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 13 Jul 2024 22:53:31 +0200 Subject: [PATCH 08/31] hot: implement Sign for XprivSigner --- src/hot/signer.rs | 50 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/hot/signer.rs b/src/hot/signer.rs index f7fa4c8..4faa7bd 100644 --- a/src/hot/signer.rs +++ b/src/hot/signer.rs @@ -22,10 +22,11 @@ use std::collections::HashSet; -use bpstd::secp256k1::ecdsa::Signature; +use amplify::Wrapper; +use bpstd::secp256k1::{ecdsa, schnorr as bip340}; use bpstd::{ Address, KeyOrigin, LegacyPk, Sats, Sighash, Sign, TapLeafHash, TapMerklePath, TapSighash, - XOnlyPk, XkeyOrigin, Xpriv, + XOnlyPk, Xpriv, XprivAccount, }; use descriptors::Descriptor; use psbt::{Psbt, Rejected, Signer}; @@ -40,13 +41,12 @@ pub struct ConsoleSigner<'descr, 'me, D: Descriptor> where Self: 'me { descriptor: &'descr D, - origin: XkeyOrigin, - xpriv: Xpriv, + account: XprivAccount, signer: XprivSigner<'me>, } pub struct XprivSigner<'xpriv> { - xpriv: &'xpriv Xpriv, + account: &'xpriv XprivAccount, // TODO: Support key- and script-path selection } @@ -58,14 +58,32 @@ where Self: 'me fn approve(&self, _psbt: &Psbt) -> Result, Rejected> { Ok(&self.signer) } } +impl<'xpriv> XprivSigner<'xpriv> { + fn derive(&self, origin: Option<&KeyOrigin>) -> Option { + let origin = origin?; + if !self.account.origin().is_subset_of(origin) { + return None; + } + Some( + self.account + .xpriv() + .derive_priv(&origin.derivation()[self.account.origin().derivation().len()..]), + ) + } +} + impl<'a, 'xpriv> Sign for &'a XprivSigner<'xpriv> { fn sign_ecdsa( &self, message: Sighash, pk: LegacyPk, origin: Option<&KeyOrigin>, - ) -> Option { - todo!() + ) -> Option { + let sk = self.derive(origin)?; + if sk.to_compr_pk().to_inner() != pk.pubkey { + return None; + } + Some(sk.to_private_ecdsa().sign_ecdsa(message.into())) } fn sign_bip340( @@ -73,18 +91,22 @@ impl<'a, 'xpriv> Sign for &'a XprivSigner<'xpriv> { message: TapSighash, pk: XOnlyPk, origin: Option<&KeyOrigin>, - ) -> Option { - todo!() + ) -> Option { + let sk = self.derive(origin)?; + if sk.to_xonly_pk() != pk { + return None; + } + Some(sk.to_keypair_bip340().sign_schnorr(message.into())) } fn should_sign_script_path( &self, - index: usize, - merkle_path: &TapMerklePath, - leaf: TapLeafHash, + _index: usize, + _merkle_path: &TapMerklePath, + _leaf: TapLeafHash, ) -> bool { - todo!() + true } - fn should_sign_key_path(&self, index: usize) -> bool { todo!() } + fn should_sign_key_path(&self, _index: usize) -> bool { true } } From 48f0c01862ef32a8aa212bf81398610f50adf292 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 14 Jul 2024 11:14:17 +0200 Subject: [PATCH 09/31] cli: add PSBT finalization --- Cargo.lock | 10 +++---- src/cli/command.rs | 67 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b95d26f..90fcf25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" dependencies = [ "amplify", "bitcoin_hashes 0.14.0", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" dependencies = [ "amplify", "bech32", @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" dependencies = [ "amplify", "bp-consensus", @@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" dependencies = [ "amplify", "bp-derive", @@ -1215,7 +1215,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#a40a4ea21fb1db8372c4c0b902d18c68b1a055f0" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" dependencies = [ "amplify", "base64", diff --git a/src/cli/command.rs b/src/cli/command.rs index 85063e9..8ad38c1 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -24,11 +24,12 @@ use std::convert::Infallible; use std::fs::File; use std::path::PathBuf; use std::process::exit; -use std::{error, fs}; +use std::{error, fs, io}; +use amplify::IoError; use bpstd::psbt::{Beneficiary, TxParams}; -use bpstd::{Derive, IdxBase, Keychain, NormalIndex, Sats}; -use psbt::{ConstructionError, Payment, PsbtConstructor, PsbtVer}; +use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats}; +use psbt::{ConstructionError, Payment, Psbt, PsbtConstructor, PsbtVer}; use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; @@ -132,12 +133,30 @@ pub enum BpCommand { /// Name of PSBT file to save. If not given, prints PSBT to STDOUT psbt: Option, }, + + /// Finalizes PSBT, optionally extracting and publishing the signed transaction. + #[display("finalize")] + Finalize { + /// Extract and send the signed transaction to the network. + #[clap(short, long)] + publish: bool, + + /// Name of PSBT file to finalize. + psbt: PathBuf, + + /// File to save the extracted signed transaction. + tx: Option, + }, } #[derive(Debug, Display, Error, From)] #[non_exhaustive] #[display(inner)] pub enum ExecError { + #[from] + #[from(io::Error)] + Io(IoError), + #[from] Load(LoadError), @@ -147,6 +166,9 @@ pub enum ExecError { #[from] ConstructPsbt(ConstructionError), + #[from] + DecodePsbt(psbt::DecodeError), + #[cfg(feature = "electrum")] /// error querying electrum server. /// @@ -431,6 +453,45 @@ impl Exec for Args { }, } } + BpCommand::Finalize { publish, psbt, tx } => { + eprint!("Reading PSBT from file {} ... ", psbt.display()); + let mut psbt_file = File::open(psbt)?; + let mut psbt = Psbt::decode(&mut psbt_file)?; + eprintln!("success"); + if psbt.is_finalized() { + eprintln!("The PSBT is already finalized"); + } else { + let wallet = self.bp_wallet::(&config)?; + eprint!("Finalizing PSBT ... "); + let inputs = psbt.finalize(wallet.descriptor()); + eprint!("{inputs} of {} inputs were finalized", psbt.inputs().count()); + if psbt.is_finalized() { + eprintln!(", transaction is ready for the extraction"); + } else { + eprintln!(", but some non-finalized inputs remains"); + } + } + if *publish || tx.is_some() { + eprint!("Extracting signed transaction ... "); + match psbt.extract() { + Ok(extracted) => { + eprintln!("success"); + if let Some(file) = tx { + eprint!("Saving transaction to file {} ...", file.display()); + let mut file = File::create(file)?; + extracted.consensus_encode(&mut file)?; + eprintln!("success"); + } + if *publish { + todo!("sending transaction to network") + } + } + Err(e) => { + eprintln!("PSBT still contains {} non-finalized inputs, failing", e.0); + } + } + } + } }; println!(); From c36f15b4215951cf33888ee8d0f4e44ab3a346a4 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:25:08 +0200 Subject: [PATCH 10/31] hot: fix storing seed as mnemonic --- src/hot/seed.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hot/seed.rs b/src/hot/seed.rs index e0841ea..85b70e6 100644 --- a/src/hot/seed.rs +++ b/src/hot/seed.rs @@ -109,7 +109,8 @@ impl SecureIo for Seed { fn write

(&self, file: P, password: &str) -> io::Result<()> where P: AsRef { - fs::write(file, encrypt(self.0.to_vec(), password)) + let mnemonic = Mnemonic::from_entropy(&self.0).expect("mnemonic generator is broken"); + fs::write(file, encrypt(mnemonic.to_string().into_bytes(), password)) } } From 71281bf9f9d493738c216ea0979fcff998095b85 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:26:09 +0200 Subject: [PATCH 11/31] hot: distinguish seed and account password errors --- src/hot/mod.rs | 8 +++++--- src/hot/seed.rs | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/hot/mod.rs b/src/hot/mod.rs index 301c269..dc899d9 100644 --- a/src/hot/mod.rs +++ b/src/hot/mod.rs @@ -71,9 +71,11 @@ mod io { #[from(io::Error)] Io(IoError), - #[display("invalid password.")] - #[from(aes_gcm::Error)] - Password, + #[display("invalid seed password.")] + SeedPassword, + + #[display("invalid account key password.")] + AccountPassword, #[from] Psbt(PsbtError), diff --git a/src/hot/seed.rs b/src/hot/seed.rs index 85b70e6..e978213 100644 --- a/src/hot/seed.rs +++ b/src/hot/seed.rs @@ -101,9 +101,9 @@ impl SecureIo for Seed { fn read

(file: P, password: &str) -> Result where P: AsRef { let data = fs::read(file)?; - let data = decrypt(&data, password)?; - let s = String::from_utf8(data).map_err(|_| DataError::Password)?; - let mnemonic = Mnemonic::from_str(&s).map_err(|_| DataError::Password)?; + let data = decrypt(&data, password).map_err(|_| DataError::SeedPassword)?; + let s = String::from_utf8(data).map_err(|_| DataError::SeedPassword)?; + let mnemonic = Mnemonic::from_str(&s).map_err(|_| DataError::SeedPassword)?; Ok(Seed(Box::from(mnemonic.to_entropy()))) } @@ -118,9 +118,9 @@ impl SecureIo for XprivAccount { fn read

(file: P, password: &str) -> Result where P: AsRef { let data = fs::read(file)?; - let data = decrypt(&data, password)?; - let s = String::from_utf8(data).map_err(|_| DataError::Password)?; - XprivAccount::from_str(&s).map_err(|_| DataError::Password) + let data = decrypt(&data, password).map_err(|_| DataError::AccountPassword)?; + let s = String::from_utf8(data).map_err(|_| DataError::AccountPassword)?; + XprivAccount::from_str(&s).map_err(|_| DataError::AccountPassword) } fn write

(&self, file: P, password: &str) -> io::Result<()> From 32285a650fab790759189cb301a5c2c7cd86e68a Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:27:01 +0200 Subject: [PATCH 12/31] hot: improve encryption workflows --- src/hot/command.rs | 31 +++++++++++++++++++++++++------ src/hot/mod.rs | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/hot/command.rs b/src/hot/command.rs index 13e4db5..dfd1112 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -155,19 +155,31 @@ impl HotArgs { } } -fn seed(output_file: &Path) -> Result<(), IoError> { +fn seed(output_file: &Path) -> Result<(), DataError> { let seed = Seed::random(SeedType::Bit128); let seed_password = loop { let seed_password = rpassword::prompt_password("Seed password: ")?; let entropy = calculate_entropy(&seed_password); eprintln!("Password entropy: ~{entropy:.0} bits"); - if !seed_password.is_empty() && entropy >= 64.0 { - break seed_password; + if seed_password.is_empty() || entropy < 64.0 { + eprintln!("Entropy is too low, please try with a different password"); + continue; } - eprintln!("Entropy is too low, please try with a different password") + + let password2 = rpassword::prompt_password("Repeat the password: ")?; + if password2 != seed_password { + eprintln!("Passwords do not match, please try again"); + continue; + } + break seed_password; }; seed.write(output_file, &seed_password)?; + if let Err(e) = Seed::read(output_file, &seed_password) { + eprintln!("Unable to save seed file"); + fs::remove_file(output_file)?; + return Err(e.into()); + } info_seed(seed, false); @@ -264,6 +276,11 @@ fn derive( let account = seed.derive(scheme, !mainnet, account); account.write(output_file, &account_password)?; + if let Err(e) = XprivAccount::read(output_file, &account_password) { + eprintln!("Unable to save account file"); + fs::remove_file(output_file)?; + return Err(e.into()); + } info_account(account, false); @@ -271,16 +288,18 @@ fn derive( } fn sign(psbt_file: &Path, account_file: &Path, no_password: bool) -> Result<(), DataError> { + eprintln!("Signing {} with {}", psbt_file.display(), account_file.display()); let password = if no_password { s!("") } else { rpassword::prompt_password("Password: ")? }; let account = XprivAccount::read(account_file, &password)?; + eprintln!("Signing key: {}", account.to_xpub_account()); + eprintln!("Signing using testnet signer"); + let data = fs::read(psbt_file)?; let mut psbt = Psbt::deserialize(&data)?; eprintln!("PSBT version: {:#}", psbt.version); eprintln!("Transaction id: {}", psbt.txid()); - eprintln!("Signing key: {}", account.to_xpub_account()); - eprintln!("Signing using testnet signer"); let signer = TestnetRefSigner::new(&account); let sig_count = psbt.sign(&signer)?; diff --git a/src/hot/mod.rs b/src/hot/mod.rs index dc899d9..812db55 100644 --- a/src/hot/mod.rs +++ b/src/hot/mod.rs @@ -51,6 +51,7 @@ mod io { let cipher = Aes256Gcm::new(key); let ciphered_data = cipher.encrypt(&nonce, source.as_ref()).expect("failed to encrypt"); + debug_assert_eq!(Aes256Gcm::new(key).decrypt(&nonce, &ciphered_data[..]), Ok(source)); let mut data = nonce.to_vec(); data.extend(ciphered_data); From ed99bd18b12c14ac63a2930acf864c3fc0a070d8 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:27:44 +0200 Subject: [PATCH 13/31] cli: improve command descriptions --- src/cli/args.rs | 1 + src/cli/command.rs | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index 1fadef0..f158916 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -51,6 +51,7 @@ pub struct Args, }, - /// Create a wallet + /// Create a named wallet #[display("create")] Create { /// The name for the new wallet @@ -88,7 +88,7 @@ pub enum BpCommand { #[display(inner)] General(Command), - /// List wallet balance with additional optional details + /// List wallet balance and UTXOs #[display("balance")] Balance { /// Print balance for each individual address @@ -130,11 +130,11 @@ pub enum BpCommand { /// Fee fee: Sats, - /// Name of PSBT file to save. If not given, prints PSBT to STDOUT + /// Name of a PSBT file to save. If not given, prints PSBT to STDOUT psbt: Option, }, - /// Finalizes PSBT, optionally extracting and publishing the signed transaction. + /// Finalize a PSBT, optionally extracting and publishing the signed transaction. #[display("finalize")] Finalize { /// Extract and send the signed transaction to the network. From 0469c050a69b1b2df91c32948d45e6d64e15ce00 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:28:02 +0200 Subject: [PATCH 14/31] chore: update dependencies --- Cargo.lock | 165 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 4 +- 2 files changed, 84 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90fcf25..eb3806c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,7 +267,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.71", "which", ] @@ -322,7 +322,7 @@ dependencies = [ [[package]] name = "bp-consensus" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-core?branch=signer#5ef1851508f6b9e385af83016e25002f5617e7e2" +source = "git+https://github.com/BP-WG/bp-core?branch=signer#4bad1a1a46463e14c6b762384c04f1d9b8c19f5f" dependencies = [ "amplify", "chrono", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" dependencies = [ "amplify", "bitcoin_hashes 0.14.0", @@ -357,7 +357,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.10", + "rustls 0.23.11", "serde", "serde_json", "sha2", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" dependencies = [ "amplify", "bech32", @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" dependencies = [ "amplify", "bp-consensus", @@ -449,13 +449,12 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.102" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -485,7 +484,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -511,9 +510,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -521,9 +520,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -540,7 +539,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -654,9 +653,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -664,27 +663,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -700,7 +699,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" dependencies = [ "amplify", "bp-derive", @@ -1200,7 +1199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1215,7 +1214,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#7e0ad15eac0fd7d31692b66fd35cad82bc0da735" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" dependencies = [ "amplify", "base64", @@ -1385,9 +1384,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "aws-lc-rs", "log", @@ -1406,9 +1405,9 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "aws-lc-rs", "ring", @@ -1444,29 +1443,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -1494,9 +1493,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64", "chrono", @@ -1512,14 +1511,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1652,9 +1651,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -1663,22 +1662,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1714,9 +1713,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -1750,9 +1749,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ "indexmap 2.2.6", "serde", @@ -1890,7 +1889,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", "wasm-bindgen-shared", ] @@ -1912,7 +1911,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1978,7 +1977,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1996,7 +1995,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2016,18 +2015,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2038,9 +2037,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -2050,9 +2049,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -2062,15 +2061,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -2080,9 +2079,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -2092,9 +2091,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -2104,9 +2103,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -2116,9 +2115,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -2146,5 +2145,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] diff --git a/Cargo.toml b/Cargo.toml index 9d61b20..df0cd9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ toml = "0.8.2" [package] name = "bp-wallet" version.workspace = true -description = "Modern, minimalistic & standard-compliant bitcoin wallet runtime" +description = "Modern, minimalistic & standard-compliant bitcoin wallet" keywords.workspace = true categories.workspace = true readme = "README.md" @@ -93,8 +93,8 @@ serde = ["serde_crate", "serde_yaml", "toml", "bp-std/serde"] [patch.crates-io] bp-consensus = { git = "https://github.com/BP-WG/bp-core", branch = "signer" } -bp-derive = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } bp-invoice = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } +bp-derive = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } descriptors = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } psbt = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } bp-std = { git = "https://github.com/BP-WG/bp-std", branch = "signer" } From 5ec871cf7ec5878011fe3fed2cd5d30ed6ea673a Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:32:14 +0200 Subject: [PATCH 15/31] cli: remove unnecessary scheme arg long description --- src/hot/command.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/hot/command.rs b/src/hot/command.rs index dfd1112..3211aa0 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -72,20 +72,7 @@ pub enum HotCommand { seed_file: PathBuf, /// Derivation scheme. - #[clap( - short, - long, - long_help = "Possible values are: -- bip44: used for P2PKH (not recommended) -- bip84: used for P2WPKH -- bip49: used for P2WPKH-in-P2SH -- bip86: used for P2TR with single key (no MuSig, no multisig) -- bip45: used for legacy multisigs (P2SH, not recommended) -- bip48//1h: used for P2WSH-in-P2SH multisigs (deterministic order) -- bip48//2h: used for P2WSH multisigs (deterministic order) -- bip87: used for modern multisigs with descriptors (pre-MuSig)", - default_value = "bip86" - )] + #[clap(short, long, default_value = "bip86")] scheme: Bip43, /// Account derivation number (should be hardened, i.e. with `h` suffix) From 74699da247574b7cfe88ffc015c16cc614a190c8 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:33:30 +0200 Subject: [PATCH 16/31] cli: use singe indexer error type --- src/cli/command.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index 3393e01..aa6d769 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -169,21 +169,14 @@ pub enum ExecError { #[from] DecodePsbt(psbt::DecodeError), - #[cfg(feature = "electrum")] - /// error querying electrum server. + /// error querying indexer. /// /// {0} #[from] + #[cfg_attr(feature = "electrum", from(electrum::Error))] + #[cfg_attr(feature = "esplora", from(esplora::Error))] #[display(doc_comments)] - Electrum(electrum::Error), - - #[cfg(feature = "esplora")] - /// error querying esplora server. - /// - /// {0} - #[from] - #[display(doc_comments)] - Esplora(esplora::Error), + Indexer(AnyIndexerError), } impl Exec for Args { From 28823690bc12e69b0277708829e5c718d01c6daf Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:33:58 +0200 Subject: [PATCH 17/31] cli: fix list and create wallet commands --- src/cli/command.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index aa6d769..31c87d6 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -187,9 +187,10 @@ impl Exec for Args { match &self.command { Command::List => { let dir = self.general.base_dir(); - let Ok(dir) = fs::read_dir(dir).map_err(|err| { + let Ok(dir) = fs::read_dir(dir).inspect_err(|err| { error!("Error reading wallet directory: {err:?}"); eprintln!("System directory is not initialized"); + println!("no wallets found"); }) else { return Ok(()); }; @@ -206,10 +207,15 @@ impl Exec for Args { continue; } let name = wallet.file_name().into_string().expect("invalid directory name"); - println!( + print!( "{name}{}", - if config.default_wallet == name { "\t[default]" } else { "" } + if config.default_wallet == name { "\t[default]" } else { "\t\t" } ); + let Ok(wallet) = self.bp_wallet::(&config) else { + println!("# broken wallet descriptor"); + continue; + }; + println!("\t{}", wallet.descriptor()); count += 1; } if count == 0 { @@ -234,7 +240,7 @@ impl Exec for Args { let name = name.to_string(); wallet.set_fs_config(FsConfig { path: self.general.wallet_dir(&name), - autosave: false, + autosave: true, })?; wallet.set_name(name); if let Err(err) = wallet.save() { From 31962f74b47ebec30247e61ac6ba6a32527125bb Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:34:24 +0200 Subject: [PATCH 18/31] cli: add PSBT inspect command --- src/cli/command.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/cli/command.rs b/src/cli/command.rs index 31c87d6..9ab53ee 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -112,6 +112,12 @@ pub enum BpCommand { details: bool, }, + /// Inspect PSBT file + Inspect { + /// Name of a PSBT file to inspect + psbt: PathBuf, + }, + /// Compose a new PSBT for bitcoin payment #[display("construct")] Construct { @@ -408,6 +414,16 @@ impl Exec for Args { } } } + BpCommand::Inspect { psbt } => { + eprint!("Reading PSBT from file {} ... ", psbt.display()); + let mut psbt_file = File::open(psbt)?; + let psbt = Psbt::decode(&mut psbt_file)?; + eprintln!("success"); + println!( + "{}", + serde_yaml::to_string(&psbt).expect("unable to generate YAML representation") + ); + } BpCommand::Construct { v2, to: beneficiaries, From eec746abdede806d9f78ffa88108a9bef04e7769 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:37:09 +0200 Subject: [PATCH 19/31] cli: improve descriptor representation --- src/cli/opts.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli/opts.rs b/src/cli/opts.rs index 18e6c39..51ec49f 100644 --- a/src/cli/opts.rs +++ b/src/cli/opts.rs @@ -78,7 +78,7 @@ pub struct ResolverOpt { } pub trait DescriptorOpts: clap::Args + Clone + Eq + Debug { - type Descr: Descriptor + serde::Serialize + for<'de> serde::Deserialize<'de>; + type Descr: Descriptor + Display + serde::Serialize + for<'de> serde::Deserialize<'de>; fn is_some(&self) -> bool; fn descriptor(&self) -> Option; } @@ -86,11 +86,11 @@ pub trait DescriptorOpts: clap::Args + Clone + Eq + Debug { #[derive(Args, Clone, PartialEq, Eq, Debug)] #[group(multiple = false)] pub struct DescrStdOpts { - /// Use wpkh(KEY) descriptor as wallet + /// Use wpkh(WPKH) descriptor as wallet #[arg(long, global = true)] pub wpkh: Option, - /// Use tr(KEY) descriptor as wallet + /// Use tr(TR_KEY_ONLY) descriptor as wallet #[arg(long, global = true)] pub tr_key_only: Option, } @@ -113,10 +113,11 @@ impl DescriptorOpts for DescrStdOpts { #[derive(Args, Clone, PartialEq, Eq, Debug)] #[group(multiple = false)] pub struct WalletOpts { + /// Use specific named wallet #[arg(short = 'w', long = "wallet", global = true)] pub name: Option, - /// Path to wallet directory. + /// Use wallet from a given path #[arg( short = 'W', long, @@ -145,7 +146,7 @@ pub struct GeneralOpts { pub data_dir: PathBuf, /// Network to use. - #[arg(short, long, global = true, default_value = "testnet", env = "LNPBP_NETWORK")] + #[arg(short, long, global = true, default_value = "testnet3", env = "LNPBP_NETWORK")] pub network: Network, } From fbe7eb050902f49607d6726baf779618d7f70288 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 13:50:03 +0200 Subject: [PATCH 20/31] chore: fix build issues --- src/cli/command.rs | 2 +- src/cli/opts.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index 9ab53ee..5a155c9 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -35,7 +35,7 @@ use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; use crate::wallet::fs::{LoadError, StoreError}; use crate::wallet::Save; -use crate::{coinselect, FsConfig, OpType, WalletAddr, WalletUtxo}; +use crate::{coinselect, AnyIndexerError, FsConfig, OpType, WalletAddr, WalletUtxo}; #[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)] pub enum Command { diff --git a/src/cli/opts.rs b/src/cli/opts.rs index ceb22b8..6060d86 100644 --- a/src/cli/opts.rs +++ b/src/cli/opts.rs @@ -20,7 +20,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::path::{Path, PathBuf}; use bpstd::{Network, XpubDerivable}; From 8f96783a8370dd175099c304ad09f66bc0120048 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 13:50:32 +0200 Subject: [PATCH 21/31] cli: make indexer construction with a dedicated method --- src/cli/args.rs | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index a7c6c4a..0d757c9 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -94,6 +94,26 @@ impl Args { conf_path } + pub fn indexer(&self) -> Result { + let network = self.general.network.to_string(); + Ok(match (&self.resolver.esplora, &self.resolver.electrum, &self.resolver.mempool) { + (None, Some(url), None) => AnyIndexer::Electrum(Box::new(electrum::Client::new(url)?)), + (Some(url), None, None) => AnyIndexer::Esplora(Box::new(IndexerClient::new_esplora( + &url.replace("{network}", &network), + )?)), + (None, None, Some(url)) => AnyIndexer::Mempool(Box::new(IndexerClient::new_mempool( + &url.replace("{network}", &network), + )?)), + _ => { + eprintln!( + " - error: no blockchain indexer specified; use either --esplora --mempool or \ + --electrum argument" + ); + exit(1); + } + }) + } + pub fn bp_wallet( &self, conf: &Config, @@ -137,26 +157,7 @@ impl Args { }; if sync { - let network = self.general.network.to_string(); - let indexer = - match (&self.resolver.esplora, &self.resolver.electrum, &self.resolver.mempool) { - (None, Some(url), None) => AnyIndexer::Electrum(Box::new( - electrum::Client::new(&url.replace("{network}", &network))?, - )), - (Some(url), None, None) => AnyIndexer::Esplora(Box::new( - IndexerClient::new_esplora(&url.replace("{network}", &network))?, - )), - (None, None, Some(url)) => AnyIndexer::Mempool(Box::new( - IndexerClient::new_mempool(&url.replace("{network}", &network))?, - )), - _ => { - eprintln!( - " - error: no blockchain indexer specified; use either --esplora \ - --mempool or --electrum argument" - ); - exit(1); - } - }; + let indexer = self.indexer()?; eprint!("Syncing"); if let MayError { err: Some(errors), .. From aade7b6e100d1c0cd37c93f250dd71d0d97670f9 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 10:40:27 +0200 Subject: [PATCH 22/31] indexer: add command for publishing a transaction --- src/indexers/any.rs | 12 ++++++++++++ src/indexers/electrum.rs | 5 +++++ src/indexers/esplora.rs | 4 +++- src/indexers/mod.rs | 3 +++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/indexers/any.rs b/src/indexers/any.rs index 71de6d4..13cc9b9 100644 --- a/src/indexers/any.rs +++ b/src/indexers/any.rs @@ -19,6 +19,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bpstd::Tx; use descriptors::Descriptor; use crate::{Indexer, Layer2, MayError, WalletCache, WalletDescr}; @@ -119,6 +120,17 @@ impl Indexer for AnyIndexer { } } } + + fn publish(&self, tx: &Tx) -> Result<(), Self::Error> { + match self { + #[cfg(feature = "electrum")] + AnyIndexer::Electrum(inner) => inner.publish(tx).map_err(|e| e.into()), + #[cfg(feature = "esplora")] + AnyIndexer::Esplora(inner) => inner.publish(tx).map_err(|e| e.into()), + #[cfg(feature = "mempool")] + AnyIndexer::Mempool(inner) => inner.publish(tx).map_err(|e| e.into()), + } + } } #[cfg(feature = "electrum")] diff --git a/src/indexers/electrum.rs b/src/indexers/electrum.rs index 29612cf..a9d4748 100644 --- a/src/indexers/electrum.rs +++ b/src/indexers/electrum.rs @@ -350,4 +350,9 @@ impl Indexer for Client { ) -> MayError> { todo!() } + + fn publish(&self, tx: &Tx) -> Result<(), Self::Error> { + self.transaction_broadcast(tx)?; + Ok(()) + } } diff --git a/src/indexers/esplora.rs b/src/indexers/esplora.rs index 3cd3e3a..29def2b 100644 --- a/src/indexers/esplora.rs +++ b/src/indexers/esplora.rs @@ -23,7 +23,7 @@ use std::collections::BTreeMap; use std::num::NonZeroU32; -use bpstd::{Address, DerivedAddr, LockTime, Outpoint, SeqNo, Witness}; +use bpstd::{Address, DerivedAddr, LockTime, Outpoint, SeqNo, Tx, Witness}; use descriptors::Descriptor; use esplora::{BlockingClient, Error}; @@ -301,4 +301,6 @@ impl Indexer for Client { ) -> MayError> { todo!() } + + fn publish(&self, tx: &Tx) -> Result<(), Self::Error> { self.inner.broadcast(tx) } } diff --git a/src/indexers/mod.rs b/src/indexers/mod.rs index e2f35b9..a396dc7 100644 --- a/src/indexers/mod.rs +++ b/src/indexers/mod.rs @@ -31,6 +31,7 @@ mod any; #[cfg(any(feature = "electrum", feature = "esplora", feature = "mempool"))] pub use any::{AnyIndexer, AnyIndexerError}; +use bpstd::Tx; use descriptors::Descriptor; #[cfg(any(feature = "esplora", feature = "mempool"))] pub use esplora::Client; @@ -53,4 +54,6 @@ pub trait Indexer { descr: &WalletDescr, cache: &mut WalletCache, ) -> MayError>; + + fn publish(&self, tx: &Tx) -> Result<(), Self::Error>; } From 45e74612fefb7ed70f3fa47824c42443ef91d600 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 13:51:00 +0200 Subject: [PATCH 23/31] cli: publish transaction upon finalization --- src/cli/command.rs | 58 ++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index 5a155c9..887a15f 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -35,7 +35,7 @@ use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; use crate::wallet::fs::{LoadError, StoreError}; use crate::wallet::Save; -use crate::{coinselect, AnyIndexerError, FsConfig, OpType, WalletAddr, WalletUtxo}; +use crate::{coinselect, AnyIndexerError, FsConfig, Indexer, OpType, WalletAddr, WalletUtxo}; #[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)] pub enum Command { @@ -468,9 +468,13 @@ impl Exec for Args { }, } } - BpCommand::Finalize { publish, psbt, tx } => { - eprint!("Reading PSBT from file {} ... ", psbt.display()); - let mut psbt_file = File::open(psbt)?; + BpCommand::Finalize { + publish, + psbt: psbt_path, + tx, + } => { + eprint!("Reading PSBT from file {} ... ", psbt_path.display()); + let mut psbt_file = File::open(psbt_path)?; let mut psbt = Psbt::decode(&mut psbt_file)?; eprintln!("success"); if psbt.is_finalized() { @@ -483,28 +487,42 @@ impl Exec for Args { if psbt.is_finalized() { eprintln!(", transaction is ready for the extraction"); } else { - eprintln!(", but some non-finalized inputs remains"); + eprintln!(" and some non-finalized inputs remains"); } } - if *publish || tx.is_some() { - eprint!("Extracting signed transaction ... "); - match psbt.extract() { - Ok(extracted) => { + + eprint!("Saving PSBT file ... "); + let mut psbt_file = File::create(psbt_path)?; + psbt.encode(psbt.version, &mut psbt_file)?; + eprintln!("success"); + + match psbt.extract() { + Ok(extracted) => { + eprintln!("success"); + eprint!("Extracting signed transaction ... "); + if !*publish && tx.is_none() { + println!("{extracted}"); + } + if let Some(file) = tx { + eprint!("Saving transaction to file {} ...", file.display()); + let mut file = File::create(file)?; + extracted.consensus_encode(&mut file)?; eprintln!("success"); - if let Some(file) = tx { - eprint!("Saving transaction to file {} ...", file.display()); - let mut file = File::create(file)?; - extracted.consensus_encode(&mut file)?; - eprintln!("success"); - } - if *publish { - todo!("sending transaction to network") - } } - Err(e) => { - eprintln!("PSBT still contains {} non-finalized inputs, failing", e.0); + if *publish { + self.indexer()?.publish(&extracted)?; } } + Err(e) if *publish || tx.is_some() => { + eprintln!( + "PSBT still contains {} non-finalized inputs, failing to extract \ + transaction", + e.0 + ); + } + Err(e) => { + eprintln!("{} more inputs still have to be finalized", e.0) + } } } }; From 8d8b3cda4c858cdcf00501b26630124cf9415d9f Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 18:05:04 +0200 Subject: [PATCH 24/31] hot: sighash debugging function --- Cargo.lock | 12 ++++---- src/cli/args.rs | 2 +- src/hot/command.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++- src/indexers/any.rs | 10 ++++++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb3806c..a40b088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,7 +322,7 @@ dependencies = [ [[package]] name = "bp-consensus" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-core?branch=signer#4bad1a1a46463e14c6b762384c04f1d9b8c19f5f" +source = "git+https://github.com/BP-WG/bp-core?branch=signer#2d5818d8df30a48f160717831849b309411bb32e" dependencies = [ "amplify", "chrono", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" dependencies = [ "amplify", "bitcoin_hashes 0.14.0", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" dependencies = [ "amplify", "bech32", @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" dependencies = [ "amplify", "bp-consensus", @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" dependencies = [ "amplify", "bp-derive", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#9454bd8351af2edeadd166a5b22d145c9a1ce2aa" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" dependencies = [ "amplify", "base64", diff --git a/src/cli/args.rs b/src/cli/args.rs index 0d757c9..36dbb5c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -106,7 +106,7 @@ impl Args { )?)), _ => { eprintln!( - " - error: no blockchain indexer specified; use either --esplora --mempool or \ + "Error: no blockchain indexer specified; use either --esplora --mempool or \ --electrum argument" ); exit(1); diff --git a/src/hot/command.rs b/src/hot/command.rs index 3211aa0..ceae892 100644 --- a/src/hot/command.rs +++ b/src/hot/command.rs @@ -23,10 +23,11 @@ use std::fs; use std::path::{Path, PathBuf}; +use amplify::hex::ToHex; use amplify::{Display, IoError}; use bip39::Mnemonic; use bpstd::signers::TestnetRefSigner; -use bpstd::{HardenedIndex, XprivAccount}; +use bpstd::{HardenedIndex, SighashCache, Tx, XprivAccount}; use clap::Subcommand; use colored::Colorize; use psbt::Psbt; @@ -114,6 +115,13 @@ pub enum HotCommand { /// Signing account file used to (partially co-)sign PSBT signing_account: PathBuf, }, + + /// Analyze PSBT and print debug signing information + #[display("sighash")] + Sighash { + /// File containing PSBT + psbt_file: PathBuf, + }, } impl HotArgs { @@ -137,6 +145,7 @@ impl HotArgs { psbt_file, signing_account, } => sign(&psbt_file, &signing_account, no_password)?, + HotCommand::Sighash { psbt_file } => sighash(&psbt_file)?, }; Ok(()) } @@ -300,3 +309,66 @@ fn sign(psbt_file: &Path, account_file: &Path, no_password: bool) -> Result<(), println!("\n{}\n", psbt); Ok(()) } + +fn sighash(psbt_file: &Path) -> Result<(), DataError> { + let data = fs::read(psbt_file)?; + let psbt = Psbt::deserialize(&data)?; + + let tx = psbt.to_unsigned_tx(); + let txid = tx.txid(); + let prevouts = psbt.inputs().map(psbt::Input::prev_txout).cloned().collect::>(); + let mut sig_hasher = SighashCache::new(Tx::from(tx), prevouts) + .expect("inputs and prevouts match algorithmically"); + println!( + "PSBT contains transaction with id {} and {} inputs", + txid.to_string().bright_green(), + psbt.inputs().count() + ); + println!("Input #\tSig type\tSighash algo\tSighash type\tSighash\t\t\t\t\t\t\t\t\tScript code"); + for input in psbt.inputs() { + let (ty, algo) = match (input.is_bip340(), input.is_segwit_v0()) { + (true, _) => ("BIP340", "Taproot"), + (false, true) => ("ECDSA", "SegWitV0"), + (false, false) => ("ECDSA", "Legacy"), + }; + let sighash_type = match input.sighash_type { + None if input.is_bip340() => s!("DEFAULT"), + None => s!("unspecified (assumed ALL)"), + Some(sighash_type) => sighash_type.to_string(), + }; + print!("{}\t{}\t\t{}\t\t{}\t\t", input.index() + 1, ty, algo, sighash_type); + + if input.is_bip340() { + match sig_hasher.tap_sighash_key(input.index(), input.sighash_type) { + Ok(sighash) => println!("{sighash}\tn/a"), + Err(e) => println!("{e}"), + } + } else if input.is_segwit_v0() { + let Some(script_code) = input.script_code() else { + println!("no witness script is given, which is required to compute script code"); + continue; + }; + match sig_hasher.segwit_sighash( + input.index(), + &script_code, + input.value(), + input.sighash_type.unwrap_or_default(), + ) { + Ok(sighash) => println!("{sighash}\t"), + Err(e) => println!("{e}"), + } + println!("{}", script_code.to_hex()); + } else { + match sig_hasher.legacy_sighash( + input.index(), + &input.prev_txout().script_pubkey, + input.sighash_type.unwrap_or_default().to_consensus_u32(), + ) { + Ok(sighash) => println!("{sighash}\tn/a"), + Err(e) => println!("{e}"), + } + } + } + + Ok(()) +} diff --git a/src/indexers/any.rs b/src/indexers/any.rs index 13cc9b9..1cf42c9 100644 --- a/src/indexers/any.rs +++ b/src/indexers/any.rs @@ -41,6 +41,16 @@ pub enum AnyIndexer { Mempool(Box), } +impl AnyIndexer { + pub fn name(&self) -> &'static str { + match self { + AnyIndexer::Electrum(_) => "electrum", + AnyIndexer::Esplora(_) => "esplora", + AnyIndexer::Mempool(_) => "mempool", + } + } +} + #[allow(clippy::large_enum_variant)] #[derive(Debug, Display, Error)] #[display(doc_comments)] From a51916bea4fc1eed44553b7627a7cb43fa2c2a83 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 18:05:15 +0200 Subject: [PATCH 25/31] cli: add PSBT extraction command --- src/cli/command.rs | 195 ++++++++++++++++++++++++++++++--------------- 1 file changed, 129 insertions(+), 66 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index 887a15f..e19e03e 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -22,14 +22,15 @@ use std::convert::Infallible; use std::fs::File; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::exit; use std::{error, fs, io}; use amplify::IoError; use bpstd::psbt::{Beneficiary, TxParams}; -use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats}; -use psbt::{ConstructionError, Payment, Psbt, PsbtConstructor, PsbtVer}; +use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats, Tx}; +use descriptors::Descriptor; +use psbt::{ConstructionError, Payment, Psbt, PsbtConstructor, PsbtVer, UnfinalizedInputs}; use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; @@ -140,7 +141,7 @@ pub enum BpCommand { psbt: Option, }, - /// Finalize a PSBT, optionally extracting and publishing the signed transaction. + /// Finalize a PSBT, optionally extracting and publishing the signed transaction #[display("finalize")] Finalize { /// Extract and send the signed transaction to the network. @@ -153,6 +154,21 @@ pub enum BpCommand { /// File to save the extracted signed transaction. tx: Option, }, + + /// Extract a signed transaction from PSBT. The PSBT file itself is not modified. + #[display("finalize")] + Extract { + /// Send the extracted transaction to the network. + #[clap(short, long)] + publish: bool, + + /// Name of PSBT file to take the transaction from + psbt: PathBuf, + + /// File to save the extracted signed transaction. If not provided, the transaction is + /// print to STDOUT. + tx: Option, + }, } #[derive(Debug, Display, Error, From)] @@ -175,9 +191,10 @@ pub enum ExecError { #[from] DecodePsbt(psbt::DecodeError), - /// error querying indexer. - /// - /// {0} + #[from] + Unfinalized(UnfinalizedInputs), + + /// indexer failed with {0} #[from] #[cfg_attr(feature = "electrum", from(electrum::Error))] #[cfg_attr(feature = "esplora", from(esplora::Error))] @@ -415,10 +432,7 @@ impl Exec for Args { } } BpCommand::Inspect { psbt } => { - eprint!("Reading PSBT from file {} ... ", psbt.display()); - let mut psbt_file = File::open(psbt)?; - let psbt = Psbt::decode(&mut psbt_file)?; - eprintln!("success"); + let psbt = psbt_read(&psbt)?; println!( "{}", serde_yaml::to_string(&psbt).expect("unable to generate YAML representation") @@ -453,75 +467,50 @@ impl Exec for Args { // TODO: Support lock time and RBFs let params = TxParams::with(*fee); - let (psbt, _) = wallet.construct_psbt(coins, beneficiaries, params)?; - let ver = if *v2 { PsbtVer::V2 } else { PsbtVer::V0 }; - - eprintln!("{}", serde_yaml::to_string(&psbt).unwrap()); - match psbt_file { - Some(file_name) => { - let mut psbt_file = File::create(file_name).map_err(StoreError::from)?; - psbt.encode(ver, &mut psbt_file).map_err(StoreError::from)?; - } - None => match ver { - PsbtVer::V0 => println!("{psbt}"), - PsbtVer::V2 => println!("{psbt:#}"), - }, - } + let (mut psbt, _) = wallet.construct_psbt(coins, beneficiaries, params)?; + psbt.version = if *v2 { PsbtVer::V2 } else { PsbtVer::V0 }; + psbt_write_or_print(&psbt, psbt_file.as_deref())?; } BpCommand::Finalize { publish, psbt: psbt_path, tx, } => { - eprint!("Reading PSBT from file {} ... ", psbt_path.display()); - let mut psbt_file = File::open(psbt_path)?; - let mut psbt = Psbt::decode(&mut psbt_file)?; - eprintln!("success"); + let mut psbt = psbt_read(&psbt_path)?; if psbt.is_finalized() { eprintln!("The PSBT is already finalized"); } else { let wallet = self.bp_wallet::(&config)?; - eprint!("Finalizing PSBT ... "); - let inputs = psbt.finalize(wallet.descriptor()); - eprint!("{inputs} of {} inputs were finalized", psbt.inputs().count()); - if psbt.is_finalized() { - eprintln!(", transaction is ready for the extraction"); - } else { - eprintln!(" and some non-finalized inputs remains"); - } + psbt_finalize(&mut psbt, wallet.descriptor())?; } - eprint!("Saving PSBT file ... "); - let mut psbt_file = File::create(psbt_path)?; - psbt.encode(psbt.version, &mut psbt_file)?; - eprintln!("success"); - - match psbt.extract() { - Ok(extracted) => { + psbt_write(&psbt, &psbt_path)?; + if let Ok(tx) = psbt_extract(&psbt, *publish, tx.as_deref()) { + if *publish { + let indexer = self.indexer()?; + eprint!("Publishing transaction via {} ... ", indexer.name()); + indexer.publish(&tx)?; eprintln!("success"); - eprint!("Extracting signed transaction ... "); - if !*publish && tx.is_none() { - println!("{extracted}"); - } - if let Some(file) = tx { - eprint!("Saving transaction to file {} ...", file.display()); - let mut file = File::create(file)?; - extracted.consensus_encode(&mut file)?; - eprintln!("success"); - } - if *publish { - self.indexer()?.publish(&extracted)?; - } } - Err(e) if *publish || tx.is_some() => { - eprintln!( - "PSBT still contains {} non-finalized inputs, failing to extract \ - transaction", - e.0 - ); - } - Err(e) => { - eprintln!("{} more inputs still have to be finalized", e.0) + } + } + BpCommand::Extract { + publish, + psbt: psbt_path, + tx, + } => { + let mut psbt = psbt_read(&psbt_path)?; + if !psbt.is_finalized() { + let wallet = self.bp_wallet::(&config)?; + psbt_finalize(&mut psbt, wallet.descriptor())?; + } + + if let Ok(tx) = psbt_extract(&psbt, *publish, tx.as_deref()) { + if *publish { + let indexer = self.indexer()?; + eprint!("Publishing transaction via {} ... ", indexer.name()); + indexer.publish(&tx)?; + eprintln!("success"); } } } @@ -532,3 +521,77 @@ impl Exec for Args { Ok(()) } } + +fn psbt_read(psbt_path: &Path) -> Result { + eprint!("Reading PSBT from file {} ... ", psbt_path.display()); + let mut psbt_file = File::open(psbt_path)?; + let psbt = Psbt::decode(&mut psbt_file)?; + eprintln!("success"); + Ok(psbt) +} + +fn psbt_write(psbt: &Psbt, psbt_path: &Path) -> Result<(), ExecError> { + eprint!("Saving PSBT to file {} ... ", psbt_path.display()); + let mut psbt_file = File::create(&psbt_path)?; + psbt.encode(psbt.version, &mut psbt_file)?; + eprintln!("success"); + Ok(()) +} + +fn psbt_write_or_print(psbt: &Psbt, psbt_path: Option<&Path>) -> Result<(), ExecError> { + match psbt_path { + Some(file_name) => { + psbt_write(&psbt, file_name)?; + } + None => match psbt.version { + PsbtVer::V0 => println!("{psbt}"), + PsbtVer::V2 => println!("{psbt:#}"), + }, + } + Ok(()) +} + +fn psbt_finalize, K, V>( + psbt: &mut Psbt, + descriptor: &D, +) -> Result<(), ExecError> { + eprint!("Finalizing PSBT ... "); + let inputs = psbt.finalize(descriptor); + eprint!("{inputs} of {} inputs were finalized", psbt.inputs().count()); + if psbt.is_finalized() { + eprintln!(", transaction is ready for the extraction"); + } else { + eprintln!(" and some non-finalized inputs remains"); + } + Ok(()) +} + +fn psbt_extract(psbt: &Psbt, publish: bool, tx: Option<&Path>) -> Result { + eprint!("Extracting signed transaction ... "); + match psbt.extract() { + Ok(extracted) => { + eprintln!("success"); + if !publish && tx.is_none() { + println!("{extracted}"); + } + if let Some(file) = tx { + eprint!("Saving transaction to file {} ...", file.display()); + let mut file = File::create(file)?; + extracted.consensus_encode(&mut file)?; + eprintln!("success"); + } + Ok(extracted) + } + Err(e) if publish || tx.is_some() => { + eprintln!( + "PSBT still contains {} non-finalized inputs, failing to extract transaction", + e.0 + ); + Err(e.into()) + } + Err(e) => { + eprintln!("{} more inputs still have to be finalized", e.0); + Err(e.into()) + } + } +} From 99efb71d80e5dc65d6b8e8979b9f48d75c54291c Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Tue, 16 Jul 2024 19:48:57 +0200 Subject: [PATCH 26/31] hot: complete debugging taproot key-only signing --- Cargo.lock | 12 ++++++------ src/cli/command.rs | 18 ++++++++++++++++-- src/hot/signer.rs | 33 +++++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a40b088..61d905b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,7 +322,7 @@ dependencies = [ [[package]] name = "bp-consensus" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-core?branch=signer#2d5818d8df30a48f160717831849b309411bb32e" +source = "git+https://github.com/BP-WG/bp-core?branch=signer#ecc7f690f95ee68ab4b7926012306e1b0e89a83e" dependencies = [ "amplify", "chrono", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bp-derive" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#fbdc3e8db6978447d390471d08aa3c5eebd513a7" dependencies = [ "amplify", "bitcoin_hashes 0.14.0", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "bp-invoice" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#fbdc3e8db6978447d390471d08aa3c5eebd513a7" dependencies = [ "amplify", "bech32", @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "bp-std" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#fbdc3e8db6978447d390471d08aa3c5eebd513a7" dependencies = [ "amplify", "bp-consensus", @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "descriptors" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#fbdc3e8db6978447d390471d08aa3c5eebd513a7" dependencies = [ "amplify", "bp-derive", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "psbt" version = "0.11.0-beta.6" -source = "git+https://github.com/BP-WG/bp-std?branch=signer#5666fb9fa28f94acfe0c63711e3639a36eeb402c" +source = "git+https://github.com/BP-WG/bp-std?branch=signer#fbdc3e8db6978447d390471d08aa3c5eebd513a7" dependencies = [ "amplify", "base64", diff --git a/src/cli/command.rs b/src/cli/command.rs index e19e03e..ad445b8 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -29,6 +29,7 @@ use std::{error, fs, io}; use amplify::IoError; use bpstd::psbt::{Beneficiary, TxParams}; use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats, Tx}; +use colored::Colorize; use descriptors::Descriptor; use psbt::{ConstructionError, Payment, Psbt, PsbtConstructor, PsbtVer, UnfinalizedInputs}; use strict_encoding::Ident; @@ -113,6 +114,9 @@ pub enum BpCommand { details: bool, }, + /// Inspect transaction + Tx { tx: Tx }, + /// Inspect PSBT file Inspect { /// Name of a PSBT file to inspect @@ -431,6 +435,12 @@ impl Exec for Args { } } } + BpCommand::Tx { tx } => { + println!( + "{}", + serde_yaml::to_string(&tx).expect("unable to generate YAML representation") + ); + } BpCommand::Inspect { psbt } => { let psbt = psbt_read(&psbt)?; println!( @@ -557,7 +567,11 @@ fn psbt_finalize, K, V>( ) -> Result<(), ExecError> { eprint!("Finalizing PSBT ... "); let inputs = psbt.finalize(descriptor); - eprint!("{inputs} of {} inputs were finalized", psbt.inputs().count()); + eprint!( + "{} of {} inputs were finalized", + inputs.to_string().bright_green(), + psbt.inputs().count() + ); if psbt.is_finalized() { eprintln!(", transaction is ready for the extraction"); } else { @@ -585,7 +599,7 @@ fn psbt_extract(psbt: &Psbt, publish: bool, tx: Option<&Path>) -> Result { eprintln!( "PSBT still contains {} non-finalized inputs, failing to extract transaction", - e.0 + e.0.to_string().bright_red() ); Err(e.into()) } diff --git a/src/hot/signer.rs b/src/hot/signer.rs index 4faa7bd..da4841b 100644 --- a/src/hot/signer.rs +++ b/src/hot/signer.rs @@ -25,8 +25,8 @@ use std::collections::HashSet; use amplify::Wrapper; use bpstd::secp256k1::{ecdsa, schnorr as bip340}; use bpstd::{ - Address, KeyOrigin, LegacyPk, Sats, Sighash, Sign, TapLeafHash, TapMerklePath, TapSighash, - XOnlyPk, Xpriv, XprivAccount, + Address, InternalKeypair, InternalPk, KeyOrigin, LegacyPk, Sats, Sighash, Sign, TapLeafHash, + TapMerklePath, TapNodeHash, TapSighash, XOnlyPk, Xpriv, XprivAccount, }; use descriptors::Descriptor; use psbt::{Psbt, Rejected, Signer}; @@ -59,7 +59,7 @@ where Self: 'me } impl<'xpriv> XprivSigner<'xpriv> { - fn derive(&self, origin: Option<&KeyOrigin>) -> Option { + fn derive_subkey(&self, origin: Option<&KeyOrigin>) -> Option { let origin = origin?; if !self.account.origin().is_subset_of(origin) { return None; @@ -79,20 +79,41 @@ impl<'a, 'xpriv> Sign for &'a XprivSigner<'xpriv> { pk: LegacyPk, origin: Option<&KeyOrigin>, ) -> Option { - let sk = self.derive(origin)?; + let sk = self.derive_subkey(origin)?; if sk.to_compr_pk().to_inner() != pk.pubkey { return None; } Some(sk.to_private_ecdsa().sign_ecdsa(message.into())) } - fn sign_bip340( + fn sign_bip340_key_only( + &self, + message: TapSighash, + pk: InternalPk, + origin: Option<&KeyOrigin>, + merkle_root: Option, + ) -> Option { + let xpriv = self.derive_subkey(origin)?; + if xpriv.to_xonly_pk() != pk.to_xonly_pk() { + return None; + } + let output_pair = + InternalKeypair::from(xpriv.to_keypair_bip340()).to_output_keypair(merkle_root).0; + if output_pair.x_only_public_key().0.serialize() + != pk.to_output_pk(merkle_root).0.to_byte_array() + { + return None; + } + Some(output_pair.sign_schnorr(message.into())) + } + + fn sign_bip340_script_path( &self, message: TapSighash, pk: XOnlyPk, origin: Option<&KeyOrigin>, ) -> Option { - let sk = self.derive(origin)?; + let sk = self.derive_subkey(origin)?; if sk.to_xonly_pk() != pk { return None; } From 88d822f9dc547c96b1ecfd73555bb958823e59ac Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Wed, 17 Jul 2024 09:25:15 +0200 Subject: [PATCH 27/31] cli: fix wallet list command --- src/cli/command.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index ad445b8..ff4cea2 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -28,16 +28,18 @@ use std::{error, fs, io}; use amplify::IoError; use bpstd::psbt::{Beneficiary, TxParams}; -use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats, Tx}; +use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats, Tx, XpubDerivable}; use colored::Colorize; -use descriptors::Descriptor; +use descriptors::{Descriptor, StdDescr}; use psbt::{ConstructionError, Payment, Psbt, PsbtConstructor, PsbtVer, UnfinalizedInputs}; use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; use crate::wallet::fs::{LoadError, StoreError}; use crate::wallet::Save; -use crate::{coinselect, AnyIndexerError, FsConfig, Indexer, OpType, WalletAddr, WalletUtxo}; +use crate::{ + coinselect, AnyIndexerError, FsConfig, Indexer, OpType, Wallet, WalletAddr, WalletUtxo, +}; #[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)] pub enum Command { @@ -224,21 +226,23 @@ impl Exec for Args { println!("Known wallets:"); let mut count = 0usize; for wallet in dir { - let Ok(wallet) = wallet else { + let Ok(entry) = wallet else { continue; }; - let Ok(meta) = wallet.metadata() else { + let Ok(meta) = entry.metadata() else { continue; }; if !meta.is_dir() { continue; } - let name = wallet.file_name().into_string().expect("invalid directory name"); + let name = entry.file_name().into_string().expect("invalid directory name"); print!( "{name}{}", if config.default_wallet == name { "\t[default]" } else { "\t\t" } ); - let Ok(wallet) = self.bp_wallet::(&config) else { + let Ok((wallet, _warnings)) = + Wallet::::load(&entry.path(), true) + else { println!("# broken wallet descriptor"); continue; }; From 0ac8180cf0196729ac6dc664dd4f6120a3f54d86 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Wed, 17 Jul 2024 09:25:29 +0200 Subject: [PATCH 28/31] cli: print out wallet descriptor in balance and history commands --- src/cli/command.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cli/command.rs b/src/cli/command.rs index ff4cea2..b990186 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -357,6 +357,7 @@ impl Exec for Args { utxo: true, } => { let wallet = self.bp_wallet::(&config)?; + println!("Balance of {}", wallet.descriptor()); println!("\nHeight\t{:>12}\t{:68}\tAddress", "Amount, ṩ", "Outpoint"); for row in wallet.coins() { println!( @@ -376,6 +377,7 @@ impl Exec for Args { utxo: true, } => { let wallet = self.bp_wallet::(&config)?; + println!("Balance of {}", wallet.descriptor()); println!("\nHeight\t{:>12}\t{:68}", "Amount, ṩ", "Outpoint"); for (derived_addr, utxos) in wallet.address_coins() { println!("{}\t{}", derived_addr.addr, derived_addr.terminal); @@ -393,6 +395,7 @@ impl Exec for Args { } BpCommand::History { txid, details } => { let wallet = self.bp_wallet::(&config)?; + println!("History of {}", wallet.descriptor()); println!( "\nHeight\t{:<1$}\t Amount, ṩ\tFee rate, ṩ/vbyte", "Txid", From 36aa426b374182c0f20a0a275de899d88e64c516 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Wed, 17 Jul 2024 09:36:45 +0200 Subject: [PATCH 29/31] chore: fix use of colored crate in cli feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a9d7a22..02ae980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ shellexpand = { version = "3.1.0", optional = true } default = [] all = ["electrum", "esplora", "mempool", "fs", "cli", "clap", "log"] hot = ["bp-std/signers", "bip39", "rand", "aes-gcm", "rpassword"] -cli = ["base64", "env_logger", "clap", "shellexpand", "fs", "serde", "electrum", "esplora", "mempool", "log"] +cli = ["base64", "env_logger", "clap", "shellexpand", "fs", "serde", "electrum", "esplora", "mempool", "log", "colored"] log = ["env_logger"] electrum = ["bp-electrum", "serde", "serde_json"] esplora = ["bp-esplora"] From 54568a36e0e2268705cbf9b1ba16322096fe1ed3 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Wed, 17 Jul 2024 11:40:29 +0200 Subject: [PATCH 30/31] ci: fix feature gates --- .github/workflows/build.yml | 1 + src/cli/args.rs | 6 +++--- src/indexers/any.rs | 3 +++ src/indexers/esplora.rs | 1 + src/indexers/mempool.rs | 2 +- src/indexers/mod.rs | 8 +++----- src/lib.rs | 4 ++-- 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f46509..0802240 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,7 @@ jobs: - serde - esplora - electrum + - mempool - fs - cli - cli,hot diff --git a/src/cli/args.rs b/src/cli/args.rs index 36dbb5c..8a9c716 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -32,7 +32,7 @@ use strict_encoding::Ident; use crate::cli::{ Config, DescrStdOpts, DescriptorOpts, ExecError, GeneralOpts, ResolverOpt, WalletOpts, }; -use crate::indexers::Client as IndexerClient; +use crate::indexers::esplora; use crate::{AnyIndexer, MayError, Wallet}; /// Command-line arguments @@ -98,10 +98,10 @@ impl Args { let network = self.general.network.to_string(); Ok(match (&self.resolver.esplora, &self.resolver.electrum, &self.resolver.mempool) { (None, Some(url), None) => AnyIndexer::Electrum(Box::new(electrum::Client::new(url)?)), - (Some(url), None, None) => AnyIndexer::Esplora(Box::new(IndexerClient::new_esplora( + (Some(url), None, None) => AnyIndexer::Esplora(Box::new(esplora::Client::new_esplora( &url.replace("{network}", &network), )?)), - (None, None, Some(url)) => AnyIndexer::Mempool(Box::new(IndexerClient::new_mempool( + (None, None, Some(url)) => AnyIndexer::Mempool(Box::new(esplora::Client::new_mempool( &url.replace("{network}", &network), )?)), _ => { diff --git a/src/indexers/any.rs b/src/indexers/any.rs index 1cf42c9..c950932 100644 --- a/src/indexers/any.rs +++ b/src/indexers/any.rs @@ -44,8 +44,11 @@ pub enum AnyIndexer { impl AnyIndexer { pub fn name(&self) -> &'static str { match self { + #[cfg(feature = "electrum")] AnyIndexer::Electrum(_) => "electrum", + #[cfg(feature = "esplora")] AnyIndexer::Esplora(_) => "esplora", + #[cfg(feature = "mempool")] AnyIndexer::Mempool(_) => "mempool", } } diff --git a/src/indexers/esplora.rs b/src/indexers/esplora.rs index 29def2b..5eab654 100644 --- a/src/indexers/esplora.rs +++ b/src/indexers/esplora.rs @@ -152,6 +152,7 @@ fn get_scripthash_txs_all( let mut res = Vec::new(); let mut last_seen = None; let script = derive.addr.script_pubkey(); + #[cfg(feature = "mempool")] let address = derive.addr.to_string(); loop { diff --git a/src/indexers/mempool.rs b/src/indexers/mempool.rs index a534cc8..b615a8b 100644 --- a/src/indexers/mempool.rs +++ b/src/indexers/mempool.rs @@ -23,7 +23,7 @@ use bpstd::Txid; use esplora::BlockingClient; -impl super::Client { +impl super::esplora::Client { /// Creates a new mempool client with the specified URL. /// /// # Arguments diff --git a/src/indexers/mod.rs b/src/indexers/mod.rs index a396dc7..2ffb561 100644 --- a/src/indexers/mod.rs +++ b/src/indexers/mod.rs @@ -21,11 +21,11 @@ // limitations under the License. #[cfg(feature = "electrum")] -mod electrum; +pub mod electrum; #[cfg(feature = "esplora")] -mod esplora; +pub mod esplora; #[cfg(feature = "mempool")] -mod mempool; +pub mod mempool; #[cfg(any(feature = "electrum", feature = "esplora", feature = "mempool"))] mod any; @@ -33,8 +33,6 @@ mod any; pub use any::{AnyIndexer, AnyIndexerError}; use bpstd::Tx; use descriptors::Descriptor; -#[cfg(any(feature = "esplora", feature = "mempool"))] -pub use esplora::Client; use crate::{Layer2, MayError, WalletCache, WalletDescr}; diff --git a/src/lib.rs b/src/lib.rs index 638aa3d..770d915 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ extern crate clap; #[cfg(feature = "log")] extern crate log; -mod indexers; +pub mod indexers; mod util; mod data; mod rows; @@ -54,7 +54,7 @@ pub use hot::{HotArgs, HotCommand}; #[cfg(feature = "hot")] pub use hot::{Seed, SeedType}; pub use indexers::Indexer; -#[cfg(any(feature = "electrum", feature = "esplora"))] +#[cfg(any(feature = "electrum", feature = "esplora", feature = "mempool"))] pub use indexers::{AnyIndexer, AnyIndexerError}; pub use layer2::{ Layer2, Layer2Cache, Layer2Coin, Layer2Data, Layer2Descriptor, Layer2Tx, NoLayer2, From 0b724856cf48b744b7b149c220c4a30b67dd9ef0 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Wed, 17 Jul 2024 11:57:04 +0200 Subject: [PATCH 31/31] indexers: make esplora client accessible via deref --- src/indexers/esplora.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/indexers/esplora.rs b/src/indexers/esplora.rs index 5eab654..3e64889 100644 --- a/src/indexers/esplora.rs +++ b/src/indexers/esplora.rs @@ -22,6 +22,7 @@ use std::collections::BTreeMap; use std::num::NonZeroU32; +use std::ops::{Deref, DerefMut}; use bpstd::{Address, DerivedAddr, LockTime, Outpoint, SeqNo, Tx, Witness}; use descriptors::Descriptor; @@ -42,9 +43,20 @@ pub struct Client { pub(crate) kind: ClientKind, } +impl Deref for Client { + type Target = BlockingClient; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } +} + /// Represents the kind of client used for interacting with the Esplora indexer. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub enum ClientKind { + #[default] Esplora, #[cfg(feature = "mempool")] Mempool,