From d81611592936e33ffdf59730e93f913681e2960e Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 20 Mar 2024 16:32:54 +0100 Subject: [PATCH] use sp_backend --- .gitignore | 1 + Cargo.lock | 330 +++++++++++++++----- Cargo.toml | 4 +- src/constants.rs | 26 -- src/daemon.rs | 160 ++++++---- src/db.rs | 55 ---- src/main.rs | 785 ++++++++++++++++++++++++++++++++++------------ src/scan.rs | 302 ++++++++++++++++++ src/sp.rs | 155 ---------- src/spclient.rs | 787 ----------------------------------------------- 10 files changed, 1242 insertions(+), 1363 deletions(-) create mode 100644 .gitignore delete mode 100644 src/constants.rs delete mode 100644 src/db.rs create mode 100644 src/scan.rs delete mode 100644 src/sp.rs delete mode 100644 src/spclient.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock index a591bca..0749449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "atty" @@ -109,18 +109,33 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[package]] +name = "bitcoin" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" +dependencies = [ + "bech32 0.9.1", + "bitcoin-private", + "bitcoin_hashes 0.12.0", + "hex_lit", + "secp256k1 0.27.0", + "serde", +] + [[package]] name = "bitcoin" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" dependencies = [ + "base64 0.21.7", "bech32 0.10.0-beta", "bitcoin-internals", - "bitcoin_hashes", + "bitcoin_hashes 0.13.0", "hex-conservative", "hex_lit", - "secp256k1", + "secp256k1 0.28.2", "serde", ] @@ -133,6 +148,22 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", + "serde", +] + [[package]] name = "bitcoin_hashes" version = "0.13.0" @@ -163,7 +194,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662" dependencies = [ - "bitcoin", + "bitcoin 0.31.1", "serde", "serde_json", ] @@ -174,7 +205,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff1e21acea23b5ec33f912350f18adee9a08bd513dca9f66f0e2cfe9d756ef46" dependencies = [ - "bitcoin", + "bitcoin 0.31.1", "zmq", "zmq-sys", ] @@ -196,9 +227,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" @@ -214,9 +245,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -281,9 +312,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ "crossbeam-utils", ] @@ -410,6 +441,24 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "electrum-client" +version = "0.18.0" +source = "git+https://github.com/cygnet3/rust-electrum-client?branch=sp_tweaks#9332bb9681e9b47d806000da48408229d712e4e6" +dependencies = [ + "bitcoin 0.30.2", + "bitcoin-private", + "byteorder", + "libc", + "log", + "rustls", + "serde", + "serde_json", + "webpki", + "webpki-roots", + "winapi", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -517,9 +566,9 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -532,9 +581,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -556,9 +605,9 @@ checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] name = "http" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -629,9 +678,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -691,9 +740,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" @@ -712,13 +761,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -742,7 +791,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.6", + "hermit-abi 0.3.9", "libc", ] @@ -799,9 +848,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -847,9 +896,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -879,9 +928,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -894,12 +943,49 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.17" @@ -915,38 +1001,69 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sdk_relay" version = "0.1.0" dependencies = [ "anyhow", - "bitcoin", "bitcoincore-rpc", "bitcoincore-zmq", + "electrum-client", "env_logger", "futures-util", + "hex", "log", "serde", "serde_json", "serde_with", - "silentpayments", + "sp_backend", "tokio", "tokio-stream", "tokio-tungstenite", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes 0.12.0", + "secp256k1-sys 0.8.1", + "serde", +] + [[package]] name = "secp256k1" version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.13.0", "rand", - "secp256k1-sys", + "secp256k1-sys 0.9.2", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.9.2" @@ -958,18 +1075,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -998,15 +1115,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.3", + "indexmap 2.2.5", "serde", "serde_derive", "serde_json", @@ -1016,9 +1133,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", @@ -1039,14 +1156,15 @@ dependencies = [ [[package]] name = "silentpayments" -version = "0.1.0" -source = "git+https://github.com/cygnet3/rust-silentpayments?branch=master#e915f5f8daef4b39ea32902963c143a5c0ed1746" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3a0169e01dc753fe00f82d0b08a68d49e69f912f592e95ff5ade8b623728a5" dependencies = [ "bech32 0.9.1", "bimap", - "bitcoin_hashes", + "bitcoin_hashes 0.13.0", "hex", - "secp256k1", + "secp256k1 0.28.2", "serde", "serde_json", ] @@ -1068,14 +1186,32 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "sp_backend" +version = "0.1.0" +source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#0213188a95921081f5c74e5099ac46e6737a07d0" +dependencies = [ + "anyhow", + "bitcoin 0.31.1", + "serde", + "serde_json", + "silentpayments", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.10.0" @@ -1084,9 +1220,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.49" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -1095,9 +1231,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.2.0" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", "heck", @@ -1108,9 +1244,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "termcolor" @@ -1123,18 +1259,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -1201,7 +1337,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1217,9 +1353,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -1240,9 +1376,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.10" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", @@ -1261,11 +1397,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", @@ -1311,13 +1447,19 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -1337,9 +1479,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "version-compare" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" @@ -1349,9 +1491,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -1417,6 +1559,25 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1466,6 +1627,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1582,9 +1752,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d90f4e0f530c4c69f62b80d839e9ef3855edc9cba471a160c4d692deed62b401" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 350c35f..d43db65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,17 +5,17 @@ edition = "2021" [dependencies] anyhow = "1.0" -bitcoin = { version = "0.31.1", features = ["serde"] } bitcoincore-rpc = { version = "0.18" } bitcoincore-zmq = "1.4.0" electrum-client = { git = "https://github.com/cygnet3/rust-electrum-client", branch = "sp_tweaks" } env_logger = "0.9" futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] } +hex = "0.4.3" log = "0.4.20" serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" serde_with = "3.6.0" -silentpayments = { git = "https://github.com/cygnet3/rust-silentpayments", branch = "master", features = ['utils'] } +sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "sp_client" } tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", "sync"] } tokio-stream = "0.1" tokio-tungstenite = "0.21.0" diff --git a/src/constants.rs b/src/constants.rs deleted file mode 100644 index 94721a8..0000000 --- a/src/constants.rs +++ /dev/null @@ -1,26 +0,0 @@ -use bitcoin::Amount; - -type SecretKeyString = String; -type PublicKeyString = String; - -pub const PSBT_SP_PREFIX: &str = "sp"; -pub const PSBT_SP_SUBTYPE: u8 = 0; -pub const PSBT_SP_TWEAK_KEY: &str = "tweak"; -pub const PSBT_SP_ADDRESS_KEY: &str = "address"; - -pub const NUMS: &str = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; - -pub const DUST: Amount = Amount::from_sat(546); - -pub struct LogEntry { - // pub time_millis: i64, - // pub level: i32, - // pub tag: String, - pub msg: String, -} - -pub struct SyncStatus { - pub peer_count: u32, - pub blockheight: u64, - pub bestblockhash: String, -} diff --git a/src/daemon.rs b/src/daemon.rs index c955cc9..5c97fa7 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,9 +1,16 @@ -use anyhow::{anyhow, Context, Result, Error}; +use anyhow::{Context, Error, Result}; -use bitcoin::{consensus::deserialize, hashes::hex::FromHex}; -use bitcoin::{block, Address, Amount, BlockHash, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid}; -use bitcoincore_rpc::json::{CreateRawTransactionInput, ListUnspentQueryOptions, ListUnspentResultEntry}; +use bitcoincore_rpc::json::{ + CreateRawTransactionInput, ListUnspentQueryOptions, ListUnspentResultEntry, + WalletCreateFundedPsbtOptions, +}; use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; +use sp_backend::bitcoin::bip158::BlockFilter; +use sp_backend::bitcoin::{ + block, Address, Amount, Block, BlockHash, Network, OutPoint, Psbt, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, Txid, +}; +use sp_backend::bitcoin::{consensus::deserialize, hashes::hex::FromHex}; // use crossbeam_channel::Receiver; // use parking_lot::Mutex; use serde_json::{json, Value}; @@ -15,6 +22,8 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; +use crate::FAUCET_AMT; + pub struct SensitiveAuth(pub Auth); impl SensitiveAuth { @@ -80,14 +89,22 @@ fn read_cookie(path: &Path) -> Result<(String, String)> { Ok((parts[0].to_owned(), parts[1].to_owned())) } -fn rpc_connect() -> Result { - let rpc_url = "http://127.0.0.1:39332"; +fn rpc_connect(rpcwallet: Option) -> Result { + let mut rpc_url = "http://127.0.0.1:39332".to_owned(); + + match rpcwallet { + Some(rpcwallet) => rpc_url.push_str(&rpcwallet), + None => (), + } + // Allow `wait_for_new_block` to take a bit longer before timing out. // See https://github.com/romanz/electrs/issues/495 for more details. let builder = jsonrpc::simple_http::SimpleHttpTransport::builder() .url(&rpc_url)? .timeout(Duration::from_secs(30)); - let daemon_auth = SensitiveAuth(Auth::CookieFile(PathBuf::from_str("/home/sosthene/.bitcoin/signet/.cookie").unwrap())); + let daemon_auth = SensitiveAuth(Auth::CookieFile( + PathBuf::from_str("/home/sosthene/.bitcoin/signet/.cookie").unwrap(), + )); let builder = match daemon_auth.get_auth() { Auth::None => builder, Auth::UserPass(user, pass) => builder.auth(user, Some(pass)), @@ -108,11 +125,12 @@ pub struct Daemon { impl Daemon { pub(crate) fn connect( + rpcwallet: Option, // config: &Config, // exit_flag: &ExitFlag, // metrics: &Metrics, ) -> Result { - let mut rpc = rpc_connect()?; + let mut rpc = rpc_connect(rpcwallet)?; loop { match rpc_poll(&mut rpc, false) { @@ -167,78 +185,110 @@ impl Daemon { Ok(self .rpc .get_block_count() - .context("failed to get block count")? - ) + .context("failed to get block count")?) } - pub(crate) fn list_unspent_from_to(&self, minconf: Option, maxconf: Option) -> Result> { - Ok(self.rpc - .list_unspent( - minconf, - maxconf, - None, - Some(false), - Some(ListUnspentQueryOptions { - maximum_count: Some(5), - ..Default::default() - }) - )?) + pub(crate) fn get_block(&self, block_hash: BlockHash) -> Result { + Ok(self + .rpc + .get_block(&block_hash) + .context("failed to get block")?) } - pub(crate) fn create_psbt(&self, utxo: ListUnspentResultEntry, spk: ScriptBuf, network: Network) -> Result { - let input = CreateRawTransactionInput { - txid: utxo.txid, - vout: utxo.vout, - sequence: None + pub(crate) fn get_filters(&self, block_height: u32) -> Result<(u32, BlockHash, BlockFilter)> { + let block_hash = self.rpc.get_block_hash(block_height.try_into()?)?; + let filter = self + .rpc + .get_block_filter(&block_hash) + .context("failed to get block filter")? + .into_filter(); + Ok((block_height, block_hash, filter)) + } + + pub(crate) fn list_unspent_from_to( + &self, + minamt: Option, + ) -> Result> { + let minimum_sum_amount = if minamt.is_none() || minamt <= FAUCET_AMT.checked_mul(2) { + FAUCET_AMT.checked_mul(2) + } else { + minamt }; + Ok(self.rpc.list_unspent( + None, + None, + None, + Some(false), + Some(ListUnspentQueryOptions { + minimum_sum_amount, + ..Default::default() + }), + )?) + } + + pub(crate) fn create_psbt( + &self, + unspents: &[ListUnspentResultEntry], + spk: ScriptBuf, + network: Network, + ) -> Result { + let inputs: Vec = unspents + .iter() + .map(|utxo| CreateRawTransactionInput { + txid: utxo.txid, + vout: utxo.vout, + sequence: None, + }) + .collect(); let address = Address::from_script(&spk, network)?; + let total_amt = unspents + .iter() + .fold(Amount::from_sat(0), |acc, x| acc + x.amount); + let mut outputs = HashMap::new(); - outputs.insert(address.to_string(), utxo.amount); - let psbt = self.rpc - .create_psbt( - &vec![input], - &outputs, - None, - None - )?; - Ok(psbt.to_string()) + outputs.insert(address.to_string(), total_amt); + + let options = WalletCreateFundedPsbtOptions { + subtract_fee_from_outputs: vec![0], + ..Default::default() + }; + + let wallet_create_funded_result = + self.rpc + .wallet_create_funded_psbt(&inputs, &outputs, None, Some(options), None)?; + + Ok(wallet_create_funded_result.psbt.to_string()) } pub(crate) fn process_psbt(&self, psbt: String) -> Result { - let processed_psbt = self.rpc - .wallet_process_psbt( - &psbt, - None, - None, - None - )?; + let processed_psbt = self.rpc.wallet_process_psbt(&psbt, None, None, None)?; match processed_psbt.complete { true => Ok(processed_psbt.psbt), - false => Err(Error::msg("Failed to complete the psbt")) + false => Err(Error::msg("Failed to complete the psbt")), } } - pub(crate) fn finalize_psbt(&self, psbt: String) -> Result> { - let final_tx = self.rpc - .finalize_psbt(&psbt, Some(true))?; + pub(crate) fn finalize_psbt(&self, psbt: String) -> Result { + let final_tx = self.rpc.finalize_psbt(&psbt, Some(false))?; match final_tx.complete { - true => Ok(final_tx.hex.expect("We shouldn't have an empty tx for a complete return")), - false => Err(Error::msg("Failed to finalize psbt")) + true => Ok(final_tx + .psbt + .expect("We shouldn't have an empty psbt for a complete return")), + false => Err(Error::msg("Failed to finalize psbt")), } } pub(crate) fn get_network(&self) -> Result { - let blockchain_info = self.rpc - .get_blockchain_info()?; + let blockchain_info = self.rpc.get_blockchain_info()?; Ok(blockchain_info.chain) } pub(crate) fn broadcast(&self, tx: &Transaction) -> Result { - self.rpc - .send_raw_transaction(tx) - .context("failed to broadcast transaction") + let txid = self.rpc.send_raw_transaction(tx)?; + + Ok(txid) } pub(crate) fn get_transaction_info( @@ -260,7 +310,7 @@ impl Daemon { txid: &Txid, blockhash: Option, ) -> Result { - use bitcoin::consensus::serde::{hex::Lower, Hex, With}; + use sp_backend::bitcoin::consensus::serde::{hex::Lower, Hex, With}; let tx = self.get_transaction(txid, blockhash)?; #[derive(serde::Serialize)] diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index 10c33da..0000000 --- a/src/db.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{ - fs::{create_dir_all, remove_file, File}, - io::{Read, Write}, - path::PathBuf, - str::FromStr, - env -}; - -use anyhow::{Error, Result}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -pub struct FileWriter { - path: PathBuf, -} - -impl FileWriter { - pub fn new(file: &str) -> Result { - match env::var("HOME") { - Ok(home_dir) => { - let mut config_path = PathBuf::from(home_dir); - config_path.push(".4nk"); - config_path.push(file); - - // Create the directory if it doesn't exist - match create_dir_all(&config_path) { - Ok(_) => Ok(Self { path: config_path }), - Err(e) => Err(Error::new(e)), - } - }, - Err(e) => Err(Error::new(e)), - } - } - - pub fn write_to_file(&self, data: &T) -> Result<()> { - let json = serde_json::to_string(data)?; - let mut file = File::create(self.path.clone())?; - file.write_all(json.as_bytes())?; - - Ok(()) - } - - pub fn read_from_file Deserialize<'de>>(&self) -> Result { - let mut file = File::open(self.path.clone())?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let data: T = serde_json::from_str(&contents)?; - - Ok(data) - } - - pub fn delete(self) -> Result<()> { - remove_file(self.path).map_err(Error::new) - } -} diff --git a/src/main.rs b/src/main.rs index 72a4eeb..da7fedd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,56 @@ use std::{ - collections::HashMap, env, io::Error as IoError, net::SocketAddr, sync::{Arc, Mutex} + collections::HashMap, + env, + net::SocketAddr, + ops::Deref, + path::PathBuf, + str::FromStr, + sync::{Arc, Mutex}, }; -use bitcoin::{absolute::LockTime, consensus::deserialize, key::TapTweak, secp256k1::PublicKey, transaction::Version, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, XOnlyPublicKey}; -use bitcoin::secp256k1::{Message as Secp256k1Message, ThirtyTwoByteHash}; -use bitcoincore_rpc::json as bitcoin_json; +use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; - use log::{debug, error}; -use silentpayments::sending::SilentPaymentAddress; -use silentpayments::secp256k1::rand::{thread_rng, Rng}; -use spclient::Recipient; +use sp_backend::bitcoin::secp256k1::{ + rand::{thread_rng, Rng}, + Error as Secp256k1Error, Keypair, Message as Secp256k1Message, PublicKey, Scalar, Secp256k1, + SecretKey, ThirtyTwoByteHash, +}; +use sp_backend::bitcoin::{ + absolute::LockTime, + consensus::deserialize, + hex::DisplayHex, + key::TapTweak, + sighash::{Prevouts, SighashCache}, + taproot::Signature, + transaction::Version, + Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, + XOnlyPublicKey, +}; +use sp_backend::spclient::OutputList; + +use sp_backend::db::{JsonFile, Storage}; +use sp_backend::silentpayments::receiving::Label; +use sp_backend::silentpayments::sending::{generate_recipient_pubkeys, SilentPaymentAddress}; +use sp_backend::silentpayments::utils::receiving::{ + calculate_shared_secret, calculate_tweak_data, get_pubkey_from_input, +}; +use sp_backend::silentpayments::utils::sending::calculate_partial_secret; +use sp_backend::spclient::{ + derive_keys_from_seed, OutputSpendStatus, OwnedOutput, Recipient, SpClient, SpendKey, +}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; -use anyhow::{Result, Error}; +use anyhow::{Error, Result}; -mod sp; mod daemon; -mod spclient; -mod constants; -mod db; mod electrumclient; +mod scan; -use crate::daemon::Daemon; -use crate::sp::VinData; -use crate::spclient::{SpClient, SpendKey, OutputSpendStatus}; +use crate::{daemon::Daemon, scan::scan_blocks}; type Tx = UnboundedSender; @@ -35,125 +58,400 @@ type PeerMap = Arc>>; const FAUCET_AMT: Amount = Amount::from_sat(1000); -fn spend_from_core(dest: XOnlyPublicKey, daemon: Arc>) -> Result { - let core = daemon.lock().map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?; - let unspent_list: Vec = core.list_unspent_from_to(Some(101), None)?; // we're (probably) spending coinbase, so let's be extra cautious and not spend before 101 confirmations +enum BroadcastType { + Sender(SocketAddr), + ExcludeSender(SocketAddr), + #[allow(dead_code)] + ToAll, +} - // just take the first of the list - if let Some(unspent) = unspent_list.get(0) { +fn broadcast_message(peers: PeerMap, msg: Message, broadcast: BroadcastType) -> Result<()> { + // log::debug!("Broadcasting message: {}", msg); + match broadcast { + BroadcastType::Sender(addr) => { + peers + .lock() + .map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))? + .iter() + .find(|(peer_addr, _)| peer_addr == &&addr) + .ok_or(Error::msg("Failed to find the sender in the peer_map"))? + .1 + .send(msg.clone())?; + } + BroadcastType::ExcludeSender(addr) => { + peers + .lock() + .map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))? + .iter() + .filter(|(peer_addr, _)| peer_addr != &&addr) + .for_each(|(_, peer_tx)| { + let _ = peer_tx.send(msg.clone()); + }); + } + BroadcastType::ToAll => { + peers + .lock() + .map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))? + .iter() + .for_each(|(_, peer_tx)| { + let _ = peer_tx.send(msg.clone()); + }); + } + } + Ok(()) +} + +fn spend_from_core( + dest: XOnlyPublicKey, + daemon: Arc>, +) -> Result<(Transaction, Amount)> { + let core = daemon + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?; + let unspent_list: Vec = + core.list_unspent_from_to(None)?; + + if !unspent_list.is_empty() { let network = core.get_network()?; let spk = ScriptBuf::new_p2tr_tweaked(dest.dangerous_assume_tweaked()); - let new_psbt = core.create_psbt(unspent.clone(), spk, network)?; + let new_psbt = core.create_psbt(&unspent_list, spk, network)?; let processed_psbt = core.process_psbt(new_psbt)?; - let tx = core.finalize_psbt(processed_psbt)?; - let final_tx = deserialize::(&tx)?; + let finalize_psbt_result = core.finalize_psbt(processed_psbt)?; + let final_psbt = Psbt::from_str(&finalize_psbt_result)?; + let total_fee = final_psbt.fee()?; + let final_tx = final_psbt.extract_tx()?; + let fee_rate = total_fee + .checked_div(final_tx.weight().to_vbytes_ceil()) + .unwrap(); - let _ = core.broadcast(&final_tx)?; - - Ok(final_tx) + Ok((final_tx, fee_rate)) } else { - // we have 0 spendable outputs + // we don't have enough available coins to pay for this faucet request Err(Error::msg("No spendable outputs")) } } -fn faucet_send(sp_address: SilentPaymentAddress, sp_client: Arc>, daemon: Arc>) -> Result { - let wallet = sp_client.lock().map_err(|e| Error::msg(format!("{}", e.to_string())))?; - let final_tx: Transaction; - if let Some(utxo) = wallet.list_outpoints() - .into_iter() - // do we have a sp output available ? - .find(|o| o.spend_status == OutputSpendStatus::Unspent) { - // create a new transaction with an available output - let recipient = Recipient { - address: sp_address.into(), - amount: utxo.amount, - nb_outputs: 1 - }; - let mut new_psbt = wallet.create_new_psbt(vec![utxo], vec![recipient], None)?; - SpClient::set_fees(&mut new_psbt, 1, sp_address.into())?; - wallet.fill_sp_outputs(&mut new_psbt)?; - let mut signed = wallet.sign_psbt(new_psbt)?; - SpClient::finalize_psbt(&mut signed)?; - - final_tx = signed.extract_tx()?; - } else { - drop(wallet); // we don't want to keep locking it - // let's try to spend directly from the mining address - let secp = bitcoin::secp256k1::Secp256k1::signing_only(); - let keypair = bitcoin::secp256k1::Keypair::new(&secp, &mut thread_rng()); - - // we first spend from core to the pubkey we just created - let first_tx = spend_from_core(keypair.x_only_public_key().0, daemon.clone())?; - - // check that the first output of the transaction pays to the key we just created - assert!(first_tx.output[0].script_pubkey == ScriptBuf::new_p2tr_tweaked(keypair.x_only_public_key().0.dangerous_assume_tweaked())); - - // create a new transaction that spends the newly created UTXO to the sp_address - let mut faucet_tx = Transaction { - input: vec![ - TxIn { - previous_output: OutPoint::new(first_tx.txid(), 0), - ..Default::default() +fn find_owned_outputs( + tx: &Transaction, + ours: HashMap, HashMap>, +) -> Result> { + let mut res: HashMap = HashMap::new(); + for (label, map) in ours { + res.extend(tx.output.iter().enumerate().filter_map( + |(i, o)| match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) { + Ok(key) => { + if let Some(scalar) = map.get(&key) { + match SecretKey::from_slice(&scalar.to_be_bytes()) { + Ok(tweak) => { + let outpoint = OutPoint { + txid: tx.txid(), + vout: i as u32, + }; + let label_str: Option; + if let Some(l) = &label { + label_str = + Some(l.as_inner().to_be_bytes().to_lower_hex_string()); + } else { + label_str = None; + } + return Some(( + outpoint, + OwnedOutput { + blockheight: 0, + tweak: tweak.secret_bytes().to_lower_hex_string(), + amount: o.value, + script: o.script_pubkey.as_bytes().to_lower_hex_string(), + label: label_str, + spend_status: OutputSpendStatus::Unspent, + }, + )); + } + Err(_) => { + return None; + } + } } - ], - output: vec![], - version: Version::TWO, - lock_time: LockTime::ZERO - }; + None + } + Err(_) => None, + }, + )); + } + Ok(res) +} - // now do the silent payment operations with the final recipient address - let a_sum = SpClient::get_a_sum_secret_keys(&vec![keypair.secret_key()]); - let prev_outpoint = faucet_tx.input[0].previous_output; - let outpoints_hash = silentpayments::utils::hash_outpoints(&vec![(prev_outpoint.txid.to_string(), prev_outpoint.vout)], keypair.public_key())?; - let partial_secret = silentpayments::utils::sending::sender_calculate_partial_secret(a_sum, outpoints_hash)?; +fn faucet_send( + sp_address: SilentPaymentAddress, + sp_client: Arc>, + sp_outputs: Arc>, + daemon: Arc>, +) -> Result { + let mut first_tx: Option = None; + let final_tx: Transaction; + let mut new_outpoints: HashMap; - let ext_output_key = silentpayments::sending::generate_recipient_pubkey(sp_address.into(), partial_secret)?; - let change_sp_address = sp_client.lock() - .map_err(|e| Error::msg(format!("Failed to lock sp_client: {}", e.to_string())))? - .get_receiving_address(); - let change_output_key = silentpayments::sending::generate_recipient_pubkey(change_sp_address, partial_secret)?; + // do we have a sp output available ? + let available_outpoints = sp_outputs + .lock() + .map_err(|e| Error::msg(e.to_string()))? + .to_spendable_list(); - let ext_spk = ScriptBuf::new_p2tr_tweaked(ext_output_key.dangerous_assume_tweaked()); - let change_spk = ScriptBuf::new_p2tr_tweaked(change_output_key.dangerous_assume_tweaked()); + let available_amt = available_outpoints + .iter() + .fold(Amount::from_sat(0), |acc, (_, x)| acc + x.amount); - faucet_tx.output.push(TxOut { - value: FAUCET_AMT, - script_pubkey: ext_spk - }); - faucet_tx.output.push(TxOut { - value: first_tx.output[0].value - FAUCET_AMT, - script_pubkey: change_spk - }); - - let first_tx_outputs = vec![first_tx.output[0].clone()]; - let prevouts = bitcoin::sighash::Prevouts::All(&first_tx_outputs); - - let hash_ty = bitcoin::TapSighashType::Default; - - let mut cache = bitcoin::sighash::SighashCache::new(&faucet_tx); - - let sighash = cache.taproot_key_spend_signature_hash(0, &prevouts, hash_ty)?; - - let msg = Secp256k1Message::from_digest(sighash.into_32()); - - let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng()); - let final_sig = bitcoin::taproot::Signature{ sig, hash_ty }; - - faucet_tx.input[0].witness.push(final_sig.to_vec()); - - final_tx = faucet_tx; + // If we don't have at least 4 times the amount we need to send, we take some reserves out + if available_amt > FAUCET_AMT.checked_mul(4).unwrap() { + let mut total_amt = Amount::from_sat(0); + let mut inputs = HashMap::new(); + for (outpoint, output) in available_outpoints { + total_amt += output.amount; + inputs.insert(outpoint, output); + if total_amt >= FAUCET_AMT { + break; + } } - daemon.lock() - .map_err(|e| Error::msg(format!("{}", e.to_string())))? - .broadcast(&final_tx)?; + + let recipient = Recipient { + address: sp_address.into(), + amount: FAUCET_AMT, + nb_outputs: 1, + }; + + let fee_estimate = daemon + .lock() + .map_err(|e| Error::msg(format!("{}", e.to_string())))? + .estimate_fee(6)? + .unwrap_or(Amount::from_sat(1000)) + .checked_div(1000) + .unwrap(); + + log::debug!("fee estimate for 6 blocks: {}", fee_estimate); + + let wallet = sp_client.lock().map_err(|e| Error::msg(e.to_string()))?; + + let mut new_psbt = wallet.create_new_psbt(inputs.clone(), vec![recipient], None)?; + log::debug!("Created psbt: {}", new_psbt); + SpClient::set_fees(&mut new_psbt, fee_estimate, sp_address.into())?; + wallet.fill_sp_outputs(&mut new_psbt)?; + log::debug!("Definitive psbt: {}", new_psbt); + let mut aux_rand = [0u8; 32]; + thread_rng().fill(&mut aux_rand); + let mut signed = wallet.sign_psbt(new_psbt, &aux_rand)?; + log::debug!("signed psbt: {}", signed); + SpClient::finalize_psbt(&mut signed)?; + + final_tx = signed.extract_tx()?; + + // take all we need to register the new sp output + let outpoints: Vec<(String, u32)> = final_tx + .input + .iter() + .map(|i| (i.previous_output.txid.to_string(), i.previous_output.vout)) + .collect(); + + let our_sp_address: SilentPaymentAddress = + wallet.sp_receiver.get_receiving_address().try_into()?; + let our_spend_pubkey = our_sp_address.get_spend_key(); + let secp = Secp256k1::verification_only(); + let input_pubkeys: Result, Secp256k1Error> = inputs + .iter() + .map(|(_, o)| { + let tweak = SecretKey::from_str(&o.tweak)?; + our_spend_pubkey.mul_tweak(&secp, &tweak.into()) + }) + .collect(); + let input_pubkeys = input_pubkeys?; + let input_pubkeys: Vec<&PublicKey> = input_pubkeys.iter().collect(); + let partial_tweak = calculate_tweak_data(&input_pubkeys, &outpoints)?; + let ecdh_shared_secret = calculate_shared_secret(partial_tweak, wallet.get_scan_key())?; + + let outputs_to_check: Result, Secp256k1Error> = final_tx + .output + .iter() + .map(|o| XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..])) + .collect(); + + let ours = wallet + .sp_receiver + .scan_transaction(&ecdh_shared_secret, outputs_to_check?)?; + + new_outpoints = find_owned_outputs(&final_tx, ours)?; + } else { + // let's try to spend directly from the mining address + let secp = Secp256k1::signing_only(); + let keypair = Keypair::new(&secp, &mut thread_rng()); + + // we first spend from core to the pubkey we just created + let (core_tx, fee_rate) = spend_from_core(keypair.x_only_public_key().0, daemon.clone())?; + + // check that the first output of the transaction pays to the key we just created + assert!( + core_tx.output[0].script_pubkey + == ScriptBuf::new_p2tr_tweaked( + keypair.x_only_public_key().0.dangerous_assume_tweaked() + ) + ); + + // create a new transaction that spends the newly created UTXO to the sp_address + let mut faucet_tx = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(core_tx.txid(), 0), + ..Default::default() + }], + output: vec![], + version: Version::TWO, + lock_time: LockTime::ZERO, + }; + + // now do the silent payment operations with the final recipient address + let partial_secret = calculate_partial_secret( + &[(keypair.secret_key(), true)], + &[(core_tx.txid().to_string(), 0)], + )?; + + let ext_output_key: XOnlyPublicKey = + generate_recipient_pubkeys(vec![sp_address.into()], partial_secret)? + .into_values() + .flatten() + .collect::>() + .get(0) + .expect("Failed to generate keys") + .to_owned(); + let change_sp_address = sp_client + .lock() + .map_err(|e| Error::msg(format!("Failed to lock sp_client: {}", e.to_string())))? + .get_receiving_address(); + let change_output_key: XOnlyPublicKey = + generate_recipient_pubkeys(vec![change_sp_address], partial_secret)? + .into_values() + .flatten() + .collect::>() + .get(0) + .expect("Failed to generate keys") + .to_owned(); + + let ext_spk = ScriptBuf::new_p2tr_tweaked(ext_output_key.dangerous_assume_tweaked()); + let change_spk = ScriptBuf::new_p2tr_tweaked(change_output_key.dangerous_assume_tweaked()); + + faucet_tx.output.push(TxOut { + value: FAUCET_AMT, + script_pubkey: ext_spk, + }); + faucet_tx.output.push(TxOut { + value: core_tx.output[0].value - FAUCET_AMT, + script_pubkey: change_spk, + }); + + // dummy signature only used for fee estimation + faucet_tx.input[0].witness.push([1; 64].to_vec()); + + let abs_fee = fee_rate + .checked_mul(faucet_tx.weight().to_vbytes_ceil()) + .ok_or_else(|| Error::msg("Fee rate multiplication overflowed"))?; + + // reset the witness to empty + faucet_tx.input[0].witness = Witness::new(); + + faucet_tx.output[1].value -= abs_fee; + + let first_tx_outputs = vec![core_tx.output[0].clone()]; + let prevouts = Prevouts::All(&first_tx_outputs); + + let hash_ty = TapSighashType::Default; + + let mut cache = SighashCache::new(&faucet_tx); + + let sighash = cache.taproot_key_spend_signature_hash(0, &prevouts, hash_ty)?; + + let msg = Secp256k1Message::from_digest(sighash.into_32()); + + let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng()); + let final_sig = Signature { sig, hash_ty }; + + faucet_tx.input[0].witness.push(final_sig.to_vec()); + + // take all we need to register the new sp output + let outpoints: Vec<(String, u32)> = vec![(core_tx.txid().to_string(), 0)]; + + first_tx = Some(core_tx); + + let client = sp_client + .lock() + .map_err(|e| Error::msg(format!("{}", e.to_string())))?; + + let input_pubkey = &keypair.public_key(); + + let input_pub_keys: Vec<&PublicKey> = vec![input_pubkey]; + let partial_tweak = calculate_tweak_data(&input_pub_keys, &outpoints)?; + let ecdh_shared_secret = calculate_shared_secret(partial_tweak, client.get_scan_key())?; + + let p2tr_outs = vec![ext_output_key, change_output_key]; + + let ours = client + .sp_receiver + .scan_transaction(&ecdh_shared_secret, p2tr_outs)?; + + final_tx = faucet_tx; + + new_outpoints = find_owned_outputs(&final_tx, ours)?; + } + + if let Ok(core) = daemon.lock() { + // get current blockheight + let blkheight: u32 = core.get_current_height()?.try_into()?; + // update the new outpoints + for o in new_outpoints.iter_mut() { + o.1.blockheight = blkheight; + } + + // broadcast one or two transactions + if first_tx.is_some() { + core.broadcast(&first_tx.unwrap())?; + } + core.broadcast(&final_tx)?; + } else { + return Err(Error::msg("Failed to lock daemon")); + } + + // update our sp_client with the change output(s) + let mut outputs = sp_outputs + .lock() + .map_err(|e| Error::msg(format!("{}", e.to_string())))?; + + outputs.extend_from(new_outpoints); + + log::debug!("{:?}", outputs.to_outpoints_list()); Ok(final_tx.txid()) } -async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr, sp_client: Arc>, daemon: Arc>) { +fn handle_faucet_request( + msg: &str, + sp_client: Arc>, + sp_outputs: Arc>, + daemon: Arc>, +) -> Result { + if let Ok(sp_address) = SilentPaymentAddress::try_from(&msg["faucet".len()..]) { + // send bootstrap coins to this sp_address + faucet_send(sp_address, sp_client, sp_outputs, daemon) + } else { + Err(Error::msg(format!( + "faucet message with unparsable sp_address" + ))) + } +} + +async fn handle_connection( + peer_map: PeerMap, + raw_stream: TcpStream, + addr: SocketAddr, + sp_client: Arc>, + sp_outputs: Arc>, + daemon: Arc>, +) { debug!("Incoming TCP connection from: {}", addr); let ws_stream = tokio_tungstenite::accept_async(raw_stream) @@ -170,53 +468,46 @@ async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: Socke let broadcast_incoming = incoming.try_for_each({ let peer_map = peer_map.clone(); move |msg| { - match msg.is_text() { - true => { - let msg_str = msg.to_string(); - match msg_str.starts_with("faucet") { - true => { - match SilentPaymentAddress::try_from(&msg_str["faucet".len()..]) { - Ok(sp_address) => { - // send bootstrap coins to this sp_address - match faucet_send(sp_address, sp_client.clone(), daemon.clone()) { - Ok(txid) => { - log::info!("New faucet payment: {}", txid); - }, - Err(e) => { - log::error!("faucet failed with error {}", e); - let peers = peer_map.lock().unwrap(); - - let (_, peer_tx) = peers - .iter() - .find(|(peer_addr, _)| peer_addr == &&addr) - .unwrap(); - - let _ = peer_tx.send(Message::Text(format!("RELAY_ERROR: {}", e))); - } - } - }, - Err(_) => { - log::error!("faucet message with unparsable sp_address received from {}", addr); - } + if msg.is_text() { + if msg.to_string().starts_with("faucet") { + match handle_faucet_request( + &msg.to_string(), + sp_client.clone(), + sp_outputs.clone(), + daemon.clone(), + ) { + Ok(txid) => { + if let Err(e) = broadcast_message( + peer_map.clone(), + Message::Text(format!("faucet{}", txid.to_string())), + BroadcastType::Sender(addr), + ) { + log::error!("Failed to broadcast message: {}", e.to_string()); + } else { + log::debug!("Successfully broadcasted message: {}", txid); + } + } + Err(e) => { + if let Err(e) = broadcast_message( + peer_map.clone(), + Message::Text(e.to_string()), + BroadcastType::Sender(addr), + ) { + log::error!("Failed to broadcast message: {}", e); } - }, - false => { - let peers = peer_map.lock().unwrap(); - - // Broadcast message to other peers - peers - .iter() - .filter(|(peer_addr, _)| peer_addr != &&addr) - .for_each(|(_, peer_tx)| { - let _ = peer_tx.send(msg.clone()); - }); } } - }, - false => { - // we don't care - log::debug!("Received non-text message from peer {}", addr); + } else { + // we just send it `as is` to everyone except sender + if let Err(e) = + broadcast_message(peer_map.clone(), msg, BroadcastType::ExcludeSender(addr)) + { + log::error!("Failed to broadcast message: {}", e); + } } + } else { + // we don't care + log::debug!("Received non-text message {} from peer {}", msg, addr); } future::ok(()) } @@ -248,8 +539,11 @@ fn flatten_msg(parts: &[Vec]) -> Vec { final_vec } -fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc>) -> Result> { - let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec())?; +fn process_raw_tx_message( + core_msg: &bitcoincore_zmq::Message, + daemon: Arc>, +) -> Result> { + let tx: Transaction = deserialize(&core_msg.serialize_data_to_vec())?; if tx.is_coinbase() { return Err(Error::msg("Can't process coinbase transaction")); @@ -258,22 +552,30 @@ fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc = Vec::with_capacity(tx.input.len()); let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); for input in tx.input { - outpoints.push((input.previous_output.txid.to_string(), input.previous_output.vout)); - let prev_tx = daemon.lock() + outpoints.push(( + input.previous_output.txid.to_string(), + input.previous_output.vout, + )); + let prev_tx = daemon + .lock() .map_err(|e| Error::msg(format!("Failed to lock the daemon: {}", e)))? .get_transaction(&input.previous_output.txid, None) - .map_err(|_| Error::msg("Failed to find previous transaction"))?; + .map_err(|e| Error::msg(format!("Failed to find previous transaction: {}", e)))?; if let Some(output) = prev_tx.output.get(input.previous_output.vout as usize) { - let vin_data = VinData { - script_sig: input.script_sig.to_bytes().to_vec(), - txinwitness: input.witness.to_vec(), - script_pub_key: output.script_pubkey.to_bytes() - }; - match sp::get_pubkey_from_input(&vin_data) { + match get_pubkey_from_input( + &input.script_sig.to_bytes(), + &input.witness.to_vec(), + &output.script_pubkey.to_bytes(), + ) { Ok(Some(pubkey)) => pubkeys.push(pubkey), Ok(None) => continue, - Err(e) => return Err(Error::msg(format!("Can't extract pubkey from input: {}", e))), + Err(e) => { + return Err(Error::msg(format!( + "Can't extract pubkey from input: {}", + e + ))) + } } } else { return Err(Error::msg("Transaction with a non-existing input")); @@ -281,13 +583,16 @@ fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc = pubkeys.iter().collect(); - match silentpayments::utils::receiving::recipient_calculate_tweak_data(&input_pub_keys, &outpoints) { + match calculate_tweak_data(&input_pub_keys, &outpoints) { Ok(partial_tweak) => { let mut vecs = core_msg.serialize_to_vecs().to_vec(); vecs.push(partial_tweak.serialize().to_vec()); Ok(flatten_msg(&vecs)) - }, - Err(e) => Err(Error::msg(format!("Failed to compute tweak data: {}", e.to_string()))) + } + Err(e) => Err(Error::msg(format!( + "Failed to compute tweak data: {}", + e.to_string() + ))), } } @@ -303,73 +608,140 @@ async fn handle_zmq(peer_map: PeerMap, daemon: Arc>) { } }; debug!("Received a {} message", core_msg.topic_str()); - let peers = peer_map.lock().unwrap(); let payload: Vec = match core_msg.topic_str() { "rawtx" => { let processed = process_raw_tx_message(&core_msg, daemon.clone()); match processed { Ok(p) => p, - Err(_) => continue + Err(_) => continue, } - }, - _ => { - flatten_msg(&core_msg.serialize_to_vecs()) } + _ => flatten_msg(&core_msg.serialize_to_vecs()), }; - for tx in peers.values() { - let _ = tx.send(Message::Binary(payload.clone())); + if let Err(e) = broadcast_message( + peer_map.clone(), + Message::Binary(payload), + BroadcastType::ToAll, + ) { + log::error!("{}", e.to_string()); } } }); } #[tokio::main(flavor = "multi_thread")] -async fn main() -> Result<(), IoError> { +async fn main() -> Result<()> { env_logger::init(); let addr = env::args() .nth(1) - .unwrap_or_else(|| "127.0.0.1:8080".to_string()); - let wallet_name = env::args() - .nth(2) - .unwrap_or_else(|| "default".to_owned()); + .unwrap_or_else(|| "127.0.0.1:8090".to_string()); + let wallet_name = env::args().nth(2).unwrap_or_else(|| "default".to_owned()); let is_testnet: bool = env::args() .nth(3) .unwrap_or_else(|| "true".to_owned()) .parse() .expect("Please provide either \"true\" or \"false\""); + let core_wallet: Option = env::args().nth(4); let state = PeerMap::new(Mutex::new(HashMap::new())); // Connect the rpc daemon - let daemon = Daemon::connect().unwrap(); + let daemon = Daemon::connect(core_wallet).unwrap(); - let current_tip: u32 = daemon.get_current_height().expect("Failed to make rpc call").try_into().expect("block count is higher than u32::MAX"); - - // load an existing sp_wallet, or create a new one - let sp_client = match spclient::SpClient::try_init_from_disk(wallet_name.clone()) { - Ok(existing) => existing, + let current_tip: u32 = daemon + .get_current_height() + .expect("Failed to make rpc call") + .try_into() + .expect("block count is higher than u32::MAX"); + + let mut config_dir = PathBuf::from_str(&env::var("HOME")?)?; + config_dir.push(".4nk"); + let sp_client_file = JsonFile::new(&config_dir, wallet_name.clone())?; + let sp_outputs_file = JsonFile::new(&config_dir, format!("{}.db", wallet_name))?; + + // load an existing sp_wallet, or create a new one + let (sp_client, sp_outputs) = match >::load(&sp_client_file) { + Ok(existing) => { + if let Ok(our_outputs) = >::load(&sp_outputs_file) { + (existing, our_outputs) + } else { + let our_address = SilentPaymentAddress::try_from(existing.get_receiving_address())?; + let new_outputs = OutputList::new( + our_address.get_scan_key(), + our_address.get_spend_key(), + current_tip, + ); + sp_outputs_file.save(&new_outputs)?; + (existing, new_outputs) + } + } Err(_) => { - let mut seed = [0u8;64]; - thread_rng().fill(&mut seed); - let (scan_sk, spend_sk) = spclient::derive_keys_from_seed(&seed, is_testnet).expect("Couldn't generate a new sp_wallet"); - SpClient::new( - wallet_name, - scan_sk, - SpendKey::Secret(spend_sk), - None, - current_tip, - is_testnet - ).expect("Failed to create a new SpClient") + let mut seed = [0u8; 64]; + thread_rng().fill(&mut seed); + let (scan_sk, spend_sk) = derive_keys_from_seed(&seed, is_testnet) + .expect("Couldn't generate a new sp_wallet"); + let new_client = SpClient::new( + wallet_name, + scan_sk, + SpendKey::Secret(spend_sk), + None, + is_testnet, + ) + .expect("Failed to create a new SpClient"); + + let new_address = SilentPaymentAddress::try_from(new_client.get_receiving_address())?; + + let new_outputs = OutputList::new( + new_address.get_scan_key(), + new_address.get_spend_key(), + current_tip, + ); + + sp_client_file.save(&new_client)?; + sp_outputs_file.save(&new_outputs)?; + + (new_client, new_outputs) } }; - log::info!("Using wallet {} with address {}", sp_client.label, sp_client.get_receiving_address()); + log::info!( + "Using wallet {} with address {}", + sp_client.label, + sp_client.get_receiving_address() + ); + + log::info!( + "Found {} outputs for a total balance of {}", + sp_outputs.to_spendable_list().len(), + sp_outputs.get_balance() + ); + + let last_scan = sp_outputs.get_last_scan(); - let shared_sp_client = Arc::new(Mutex::new(sp_client)); let shared_daemon = Arc::new(Mutex::new(daemon)); + let shared_sp_client = Arc::new(Mutex::new(sp_client)); + let shared_sp_outputs = Arc::new(Mutex::new(sp_outputs)); + + if last_scan < current_tip { + log::info!("Scanning for our outputs"); + match scan_blocks( + shared_sp_client.clone(), + shared_daemon.clone(), + shared_sp_outputs.clone(), + current_tip - last_scan, + ) { + Ok(()) => { + let updated = shared_sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))?; + sp_outputs_file.save(updated.deref())?; + } + Err(e) => return Err(e), + }; + } // Subscribe to Bitcoin Core tokio::spawn(handle_zmq(state.clone(), shared_daemon.clone())); @@ -381,7 +753,14 @@ async fn main() -> Result<(), IoError> { // Let's spawn the handling of each connection in a separate task. while let Ok((stream, addr)) = listener.accept().await { - tokio::spawn(handle_connection(state.clone(), stream, addr, shared_sp_client.clone(), shared_daemon.clone())); + tokio::spawn(handle_connection( + state.clone(), + stream, + addr, + shared_sp_client.clone(), + shared_sp_outputs.clone(), + shared_daemon.clone(), + )); } Ok(()) diff --git a/src/scan.rs b/src/scan.rs new file mode 100644 index 0000000..1b9b7d2 --- /dev/null +++ b/src/scan.rs @@ -0,0 +1,302 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +use anyhow::{Error, Result}; +use electrum_client::ElectrumApi; +use hex::FromHex; +use sp_backend::bitcoin::bip158::BlockFilter; +use sp_backend::bitcoin::hex::DisplayHex; +use sp_backend::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; +use sp_backend::bitcoin::{BlockHash, OutPoint, Transaction, TxOut, XOnlyPublicKey}; +use sp_backend::silentpayments::receiving::Receiver; +use sp_backend::spclient::{OutputList, OutputSpendStatus, OwnedOutput, SpClient}; +use tokio::time::Instant; + +use crate::{electrumclient, Daemon}; + +fn get_script_to_secret_map( + sp_receiver: &Receiver, + tweak_data_vec: Vec, + scan_key_scalar: Scalar, + secp: &Secp256k1, +) -> Result> { + let mut res = HashMap::new(); + let shared_secrets: Result> = tweak_data_vec + .into_iter() + .map(|s| { + let x = PublicKey::from_str(&s).map_err(Error::new)?; + x.mul_tweak(secp, &scan_key_scalar).map_err(Error::new) + }) + .collect(); + let shared_secrets = shared_secrets?; + + for shared_secret in shared_secrets { + let spks = sp_receiver.get_spks_from_shared_secret(&shared_secret)?; + + for spk in spks.into_values() { + res.insert(spk, shared_secret); + } + } + Ok(res) +} + +fn check_block( + blkfilter: BlockFilter, + blkhash: BlockHash, + candidate_spks: Vec<&[u8; 34]>, + owned_spks: Vec>, +) -> Result { + // check output scripts + let mut scripts_to_match: Vec<_> = candidate_spks.into_iter().map(|spk| spk.as_ref()).collect(); + + // check input scripts + scripts_to_match.extend(owned_spks.iter().map(|spk| spk.as_slice())); + + // note: match will always return true for an empty query! + if !scripts_to_match.is_empty() { + Ok(blkfilter.match_any(&blkhash, &mut scripts_to_match.into_iter())?) + } else { + Ok(false) + } +} + +fn scan_block_outputs( + sp_receiver: &Receiver, + txdata: &Vec, + blkheight: u64, + spk2secret: HashMap<[u8; 34], PublicKey>, +) -> Result> { + let mut res: HashMap = HashMap::new(); + + // loop over outputs + for tx in txdata { + let txid = tx.txid(); + + // collect all taproot outputs from transaction + let p2tr_outs: Vec<(usize, &TxOut)> = tx + .output + .iter() + .enumerate() + .filter(|(_, o)| o.script_pubkey.is_p2tr()) + .collect(); + + if p2tr_outs.is_empty() { + continue; + }; // no taproot output + + let mut secret: Option = None; + // Does this transaction contains one of the outputs we already found? + for spk in p2tr_outs.iter().map(|(_, o)| &o.script_pubkey) { + if let Some(s) = spk2secret.get(spk.as_bytes()) { + // we might have at least one output in this transaction + secret = Some(*s); + break; + } + } + + if secret.is_none() { + continue; + }; // we don't have a secret that matches any of the keys + + // Now we can just run sp_receiver on all the p2tr outputs + let xonlykeys: Result> = p2tr_outs + .iter() + .map(|(_, o)| { + XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]).map_err(Error::new) + }) + .collect(); + + let ours = sp_receiver.scan_transaction(&secret.unwrap(), xonlykeys?)?; + for (label, map) in ours { + res.extend(p2tr_outs.iter().filter_map(|(i, o)| { + match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) { + Ok(key) => { + if let Some(scalar) = map.get(&key) { + match SecretKey::from_slice(&scalar.to_be_bytes()) { + Ok(tweak) => { + let outpoint = OutPoint { + txid, + vout: *i as u32, + }; + let label_str: Option; + if let Some(l) = &label { + label_str = + Some(l.as_inner().to_be_bytes().to_lower_hex_string()); + } else { + label_str = None; + } + return Some(( + outpoint, + OwnedOutput { + blockheight: blkheight as u32, + tweak: hex::encode(tweak.secret_bytes()), + amount: o.value, + script: hex::encode(o.script_pubkey.as_bytes()), + label: label_str, + spend_status: OutputSpendStatus::Unspent, + }, + )); + } + Err(_) => { + return None; + } + } + } + None + } + Err(_) => None, + } + })); + } + } + Ok(res) +} + +fn scan_block_inputs( + our_outputs: HashMap, + txdata: Vec, +) -> Result> { + let mut found = vec![]; + + for tx in txdata { + for input in tx.input { + let prevout = input.previous_output; + + if our_outputs.contains_key(&prevout) { + found.push(prevout); + } + } + } + Ok(found) +} + +pub fn scan_blocks( + sp_client: Arc>, + daemon: Arc>, + sp_outputs: Arc>, + mut n_blocks_to_scan: u32, +) -> anyhow::Result<()> { + log::info!("Starting a rescan"); + let electrum_client = electrumclient::create_electrum_client()?; + + let core = daemon + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?; + + let secp = Secp256k1::new(); + let scan_height = sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))? + .get_last_scan(); + let tip_height: u32 = core.get_current_height()?.try_into()?; + + // 0 means scan to tip + if n_blocks_to_scan == 0 { + n_blocks_to_scan = tip_height - scan_height; + } + + let start = scan_height + 1; + let end = if scan_height + n_blocks_to_scan <= tip_height { + scan_height + n_blocks_to_scan + } else { + tip_height + }; + + if start > end { + return Ok(()); + } + + log::info!("start: {} end: {}", start, end); + let mut filters: Vec<(u32, BlockHash, BlockFilter)> = vec![]; + for blkheight in start..=end { + filters.push(core.get_filters(blkheight)?); + } + + let mut tweak_data_map = electrum_client.sp_tweaks(start as usize)?; + + let scan_sk = sp_client + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))? + .get_scan_key(); + + let sp_receiver = sp_client + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))? + .sp_receiver + .clone(); + let start_time = Instant::now(); + + for (blkheight, blkhash, blkfilter) in filters { + let spk2secret = match tweak_data_map.remove(&(&blkheight)) { + Some(tweak_data_vec) => { + get_script_to_secret_map(&sp_receiver, tweak_data_vec, scan_sk.into(), &secp)? + } + None => HashMap::new(), + }; + + // check if new possible outputs are payments to us + let candidate_spks: Vec<&[u8; 34]> = spk2secret.keys().collect(); + + // check if owned inputs are spent + let our_outputs: HashMap = sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))? + .to_outpoints_list(); + + let owned_spks: Result>> = our_outputs + .iter() + .map(|(_, output)| { + let script = Vec::from_hex(&output.script).map_err(|e| Error::new(e)); + script + }) + .collect(); + + let matched = check_block(blkfilter, blkhash, candidate_spks, owned_spks?)?; + + if matched { + let blk = core.get_block(blkhash)?; + + // scan block for new outputs, and add them to our list + let utxo_created_in_block = + scan_block_outputs(&sp_receiver, &blk.txdata, blkheight.into(), spk2secret)?; + if !utxo_created_in_block.is_empty() { + sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))? + .extend_from(utxo_created_in_block); + } + + // update the list of outputs just in case + // utxos may be created and destroyed in the same block + let updated_outputs: HashMap = sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))? + .to_outpoints_list(); + + // search inputs and mark as mined + let utxo_destroyed_in_block = scan_block_inputs(updated_outputs, blk.txdata)?; + if !utxo_destroyed_in_block.is_empty() { + let mut outputs = sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))?; + for outpoint in utxo_destroyed_in_block { + outputs.mark_mined(outpoint, blkhash)?; + } + } + } + } + + // time elapsed for the scan + log::info!( + "Scan complete in {} seconds", + start_time.elapsed().as_secs() + ); + + // update last_scan height + sp_outputs + .lock() + .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))? + .update_last_scan(end); + Ok(()) +} diff --git a/src/sp.rs b/src/sp.rs deleted file mode 100644 index 01eb718..0000000 --- a/src/sp.rs +++ /dev/null @@ -1,155 +0,0 @@ -use bitcoin::secp256k1::{Parity::Even, PublicKey, XOnlyPublicKey}; -use bitcoin::hashes::{hash160, Hash}; - -use anyhow::Error; - -// ** Putting all the pubkey extraction logic in the test utils for now. ** -// NUMS_H (defined in BIP340) -const NUMS_H: [u8; 32] = [ - 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, - 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, -]; - -// Define OP_CODES used in script template matching for readability -const OP_1: u8 = 0x51; -const OP_0: u8 = 0x00; -const OP_PUSHBYTES_20: u8 = 0x14; -const OP_PUSHBYTES_32: u8 = 0x20; -const OP_HASH160: u8 = 0xA9; -const OP_EQUAL: u8 = 0x87; -const OP_DUP: u8 = 0x76; -const OP_EQUALVERIFY: u8 = 0x88; -const OP_CHECKSIG: u8 = 0xAC; - -// Only compressed pubkeys are supported for silent payments -const COMPRESSED_PUBKEY_SIZE: usize = 33; - -pub struct VinData { - pub script_sig: Vec, - pub txinwitness: Vec>, - pub script_pub_key: Vec, -} - -// script templates for inputs allowed in BIP352 shared secret derivation -pub fn is_p2tr(spk: &[u8]) -> bool { - matches!(spk, [OP_1, OP_PUSHBYTES_32, ..] if spk.len() == 34) -} - -fn is_p2wpkh(spk: &[u8]) -> bool { - matches!(spk, [OP_0, OP_PUSHBYTES_20, ..] if spk.len() == 22) -} - -fn is_p2sh(spk: &[u8]) -> bool { - matches!(spk, [OP_HASH160, OP_PUSHBYTES_20, .., OP_EQUAL] if spk.len() == 23) -} - -fn is_p2pkh(spk: &[u8]) -> bool { - matches!(spk, [OP_DUP, OP_HASH160, OP_PUSHBYTES_20, .., OP_EQUALVERIFY, OP_CHECKSIG] if spk.len() == 25) -} - -pub fn get_pubkey_from_input(vin: &VinData) -> Result, Error> { - if is_p2pkh(&vin.script_pub_key) { - match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { - (true, false) => { - let spk_hash = &vin.script_pub_key[3..23]; - for i in (COMPRESSED_PUBKEY_SIZE..=vin.script_sig.len()).rev() { - if let Some(pubkey_bytes) = &vin.script_sig.get(i - COMPRESSED_PUBKEY_SIZE..i) { - let pubkey_hash = hash160::Hash::hash(pubkey_bytes); - if pubkey_hash.to_byte_array() == spk_hash { - return Ok(Some(PublicKey::from_slice(pubkey_bytes)?)); - } - } else { - return Ok(None); - } - } - } - (_, true) => return Err(Error::msg("Empty script_sig for spending a p2pkh")), - (false, _) => return Err(Error::msg("non empty witness for spending a p2pkh")), - } - } else if is_p2sh(&vin.script_pub_key) { - match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { - (false, false) => { - let redeem_script = &vin.script_sig[1..]; - if is_p2wpkh(redeem_script) { - if let Some(value) = vin.txinwitness.last() { - if let Ok(pubkey) = PublicKey::from_slice(value) { - return Ok(Some(pubkey)); - } else { - return Ok(None); - } - } - } - } - (_, true) => { - return Err(Error::msg( - "Empty script_sig for spending a p2sh".to_owned(), - )) - } - (true, false) => { - return Ok(None); - } - } - } else if is_p2wpkh(&vin.script_pub_key) { - match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { - (false, true) => { - if let Some(value) = vin.txinwitness.last() { - if let Ok(pubkey) = PublicKey::from_slice(value) { - return Ok(Some(pubkey)); - } else { - return Ok(None); - } - } else { - return Err(Error::msg("Empty witness".to_owned())); - } - } - (_, false) => { - return Err(Error::msg( - "Non empty script sig for spending a segwit output".to_owned(), - )) - } - (true, _) => { - return Err(Error::msg( - "Empty witness for spending a segwit output".to_owned(), - )) - } - } - } else if is_p2tr(&vin.script_pub_key) { - match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) { - (false, true) => { - // check for the optional annex - let annex = match vin.txinwitness.last().and_then(|value| value.get(0)) { - Some(&0x50) => 1, - Some(_) => 0, - None => return Err(Error::msg("Empty or invalid witness".to_owned())), - }; - - // Check for script path - let stack_size = vin.txinwitness.len(); - if stack_size > annex && vin.txinwitness[stack_size - annex - 1][1..33] == NUMS_H { - return Ok(None); - } - - // Return the pubkey from the script pubkey - return XOnlyPublicKey::from_slice(&vin.script_pub_key[2..34]) - .map_err(|e| Error::new(e)) - .map(|x_only_public_key| { - Some(PublicKey::from_x_only_public_key(x_only_public_key, Even)) - }); - } - (_, false) => { - return Err(Error::msg( - "Non empty script sig for spending a segwit output".to_owned(), - )) - } - (true, _) => { - return Err(Error::msg( - "Empty witness for spending a segwit output".to_owned(), - )) - } - } - } else { - // We don't support this kind of output - return Err(Error::msg("Unsupported script pubkey type")); - } - Ok(None) -} diff --git a/src/spclient.rs b/src/spclient.rs deleted file mode 100644 index 0c2eb8b..0000000 --- a/src/spclient.rs +++ /dev/null @@ -1,787 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap}, - str::FromStr, -}; - -use bitcoin::psbt::{raw, Input, Output}; -use bitcoin::{ - bip32::{DerivationPath, Xpriv}, - consensus::{deserialize, serialize}, - hashes::hex::FromHex, - key::TapTweak, - psbt::PsbtSighashType, - secp256k1::{ - constants::SECRET_KEY_SIZE, Keypair, Message, PublicKey, Scalar, Secp256k1, SecretKey, - ThirtyTwoByteHash, - }, - sighash::{Prevouts, SighashCache}, - taproot::Signature, - Address, Amount, BlockHash, Network, OutPoint, ScriptBuf, TapLeafHash, Transaction, Txid, TxIn, TxOut, Witness, -}; -use log::info; - -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use serde_with::DisplayFromStr; - -use silentpayments::sending::SilentPaymentAddress; -use silentpayments::utils as sp_utils; -use silentpayments::{receiving::Receiver, utils::LabelHash}; -use silentpayments::secp256k1::rand::{thread_rng, prelude::SliceRandom}; - -use anyhow::{Error, Result}; - -use crate::db::FileWriter; -use crate::constants::{NUMS, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, PSBT_SP_TWEAK_KEY}; - -pub use bitcoin::psbt::Psbt; - -pub struct ScanProgress { - pub start: u32, - pub current: u32, - pub end: u32, -} - -type SpendingTxId = String; -type MinedInBlock = String; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub enum OutputSpendStatus { - Unspent, - Spent(SpendingTxId), - Mined(MinedInBlock), -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct OwnedOutput { - pub txoutpoint: String, - pub blockheight: u32, - pub tweak: String, - pub amount: u64, - pub script: String, - pub label: Option, - pub spend_status: OutputSpendStatus, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct Recipient { - pub address: String, // either old school or silent payment - pub amount: u64, - pub nb_outputs: u32, // if address is not SP, only 1 is valid -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -pub enum SpendKey { - Secret(SecretKey), - Public(PublicKey), -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -pub struct SpClient { - pub label: String, - scan_sk: SecretKey, - spend_key: SpendKey, - pub mnemonic: Option, - pub sp_receiver: Receiver, - pub birthday: u32, - pub last_scan: u32, - #[serde_as(as = "HashMap")] - owned: HashMap, - writer: FileWriter, -} - -impl SpClient { - pub fn new( - label: String, - scan_sk: SecretKey, - spend_key: SpendKey, - mnemonic: Option, - birthday: u32, - is_testnet: bool, - ) -> Result { - let secp = Secp256k1::signing_only(); - let scan_pubkey = scan_sk.public_key(&secp); - let sp_receiver: Receiver; - let change_label = LabelHash::from_b_scan_and_m(scan_sk, 0).to_scalar(); - match spend_key { - SpendKey::Public(key) => { - sp_receiver = Receiver::new(0, scan_pubkey, key, change_label.into(), is_testnet)?; - } - SpendKey::Secret(key) => { - let spend_pubkey = key.public_key(&secp); - sp_receiver = Receiver::new( - 0, - scan_pubkey, - spend_pubkey, - change_label.into(), - is_testnet, - )?; - } - } - let writer = FileWriter::new(&label)?; - - Ok(Self { - label, - scan_sk, - spend_key, - mnemonic, - sp_receiver, - birthday, - last_scan: if birthday == 0 { 0 } else { birthday - 1 }, - owned: HashMap::new(), - writer, - }) - } - - pub fn try_init_from_disk(label: String) -> Result { - let empty = SpClient::new( - label, - SecretKey::from_slice(&[1u8; SECRET_KEY_SIZE]).unwrap(), - SpendKey::Secret(SecretKey::from_slice(&[1u8; SECRET_KEY_SIZE]).unwrap()), - None, - 0, - false, - )?; - - empty.retrieve_from_disk() - } - - pub fn update_last_scan(&mut self, scan_height: u32) { - self.last_scan = scan_height; - } - - pub fn get_spendable_amt(&self) -> u64 { - self.owned - .values() - .filter(|x| x.spend_status == OutputSpendStatus::Unspent) - .fold(0, |acc, x| acc + x.amount) - } - - #[allow(dead_code)] - pub fn get_unconfirmed_amt(&self) -> u64 { - self.owned - .values() - .filter(|x| match x.spend_status { - OutputSpendStatus::Spent(_) => true, - _ => false, - }) - .fold(0, |acc, x| acc + x.amount) - } - - pub fn extend_owned(&mut self, owned: Vec<(OutPoint, OwnedOutput)>) { - self.owned.extend(owned); - } - - pub fn check_outpoint_owned(&self, outpoint: OutPoint) -> bool { - self.owned.contains_key(&outpoint) - } - - // pub fn mark_transaction_inputs_as_spent( - // &mut self, - // tx: nakamoto::chain::Transaction, - // ) -> Result<()> { - // let txid = tx.txid(); - - // // note: this currently fails for collaborative transactions - // for input in tx.input { - // self.mark_outpoint_spent(input.previous_output, txid)?; - // } - - // send_amount_update(self.get_spendable_amt()); - - // self.save_to_disk() - // } - - pub fn mark_outpoint_spent(&mut self, outpoint: OutPoint, txid: Txid) -> Result<()> { - if let Some(owned) = self.owned.get_mut(&outpoint) { - match owned.spend_status { - OutputSpendStatus::Unspent => { - info!("marking {} as spent by tx {}", owned.txoutpoint, txid); - owned.spend_status = OutputSpendStatus::Spent(txid.to_string()); - } - _ => return Err(Error::msg("owned outpoint is already spent")), - } - Ok(()) - } else { - Err(anyhow::anyhow!("owned outpoint not found")) - } - } - - pub fn mark_outpoint_mined(&mut self, outpoint: OutPoint, blkhash: BlockHash) -> Result<()> { - if let Some(owned) = self.owned.get_mut(&outpoint) { - match owned.spend_status { - OutputSpendStatus::Mined(_) => { - return Err(Error::msg("owned outpoint is already mined")) - } - _ => { - info!("marking {} as mined in block {}", owned.txoutpoint, blkhash); - owned.spend_status = OutputSpendStatus::Mined(blkhash.to_string()); - } - } - Ok(()) - } else { - Err(anyhow::anyhow!("owned outpoint not found")) - } - } - - pub fn list_outpoints(&self) -> Vec { - self.owned.values().cloned().collect() - } - - pub fn reset_from_blockheight(self, blockheight: u32) -> Self { - let mut new = self.clone(); - new.owned = HashMap::new(); - new.owned = self - .owned - .into_iter() - .filter(|o| o.1.blockheight <= blockheight) - .collect(); - new.last_scan = blockheight; - - new - } - - pub fn save_to_disk(&self) -> Result<()> { - self.writer.write_to_file(self) - } - - pub fn retrieve_from_disk(self) -> Result { - self.writer.read_from_file() - } - - pub fn delete_from_disk(self) -> Result<()> { - self.writer.delete() - } - - pub fn get_receiving_address(&self) -> String { - self.sp_receiver.get_receiving_address() - } - - pub fn get_scan_key(&self) -> SecretKey { - self.scan_sk - } - - pub fn fill_sp_outputs(&self, psbt: &mut Psbt) -> Result<()> { - let b_spend = match self.spend_key { - SpendKey::Secret(key) => key, - SpendKey::Public(_) => return Err(Error::msg("Watch-only wallet, can't spend")), - }; - - let mut input_privkeys: Vec = vec![]; - for (i, input) in psbt.inputs.iter().enumerate() { - if let Some(tweak) = input.proprietary.get(&raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(), - }) { - let mut buffer = [0u8; 32]; - if tweak.len() != 32 { - return Err(Error::msg(format!("Invalid tweak at input {}", i))); - } - buffer.copy_from_slice(tweak.as_slice()); - let scalar = Scalar::from_be_bytes(buffer)?; - input_privkeys.push(b_spend.add_tweak(&scalar)?); - } else { - // For now all inputs belong to us - return Err(Error::msg(format!("Missing tweak at input {}", i))); - } - } - - let a_sum = Self::get_a_sum_secret_keys(&input_privkeys); - let outpoints: Vec<(String, u32)> = psbt - .unsigned_tx - .input - .iter() - .map(|i| { - let prev_out = i.previous_output; - (prev_out.txid.to_string(), prev_out.vout) - }) - .collect(); - let outpoints_hash: Scalar = - sp_utils::hash_outpoints(&outpoints, a_sum.public_key(&Secp256k1::signing_only()))?; - let partial_secret = - sp_utils::sending::sender_calculate_partial_secret(a_sum, outpoints_hash)?; - - // get all the silent addresses - let mut sp_addresses: Vec = Vec::with_capacity(psbt.outputs.len()); - for output in psbt.outputs.iter() { - // get the sp address from psbt - if let Some(value) = output.proprietary.get(&raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), - }) { - let sp_address = SilentPaymentAddress::try_from(deserialize::(value)?)?; - sp_addresses.push(sp_address.into()); - } else { - // Not a sp output - continue; - } - } - - let mut sp_address2xonlypubkeys = - silentpayments::sending::generate_multiple_recipient_pubkeys( - sp_addresses, - partial_secret, - )?; - for (i, output) in psbt.unsigned_tx.output.iter_mut().enumerate() { - // get the sp address from psbt - let output_data = &psbt.outputs[i]; - if let Some(value) = output_data.proprietary.get(&raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), - }) { - let sp_address = SilentPaymentAddress::try_from(deserialize::(value)?)?; - if let Some(xonlypubkeys) = sp_address2xonlypubkeys.get_mut(&sp_address.to_string()) - { - if !xonlypubkeys.is_empty() { - let output_key = xonlypubkeys.remove(0); // actually we could randomize it - // update the script pubkey - output.script_pubkey = - ScriptBuf::new_p2tr_tweaked(output_key.dangerous_assume_tweaked()); - } else { - return Err(Error::msg(format!( - "We're missing a key for address {}", - sp_address - ))); - } - } else { - return Err(Error::msg(format!("Can't find address {}", sp_address))); - } - } else { - // Not a sp output - continue; - } - } - Ok(()) - } - - pub fn set_fees(psbt: &mut Psbt, fee_rate: u32, payer: String) -> Result<()> { - let payer_vouts: Vec = match SilentPaymentAddress::try_from(payer.clone()) { - Ok(sp_address) => psbt - .outputs - .iter() - .enumerate() - .filter_map(|(i, o)| { - if let Some(value) = o.proprietary.get(&raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), - }) { - let candidate = - SilentPaymentAddress::try_from(deserialize::(value).unwrap()) - .unwrap(); - if sp_address == candidate { - Some(i as u32) - } else { - None - } - } else { - None - } - }) - .collect(), - Err(_) => { - let address = Address::from_str(&payer)?; - let spk = address.assume_checked().script_pubkey(); - psbt.unsigned_tx - .output - .iter() - .enumerate() - .filter_map(|(i, o)| { - if o.script_pubkey == spk { - Some(i as u32) - } else { - None - } - }) - .collect() // Actually we should have only one output for normal address - } - }; - - if payer_vouts.is_empty() { - return Err(Error::msg("Payer is not part of this transaction")); - } - - // check against the total amt in inputs - let total_input_amt: u64 = psbt - .iter_funding_utxos() - .try_fold(0u64, |sum, utxo_result| { - utxo_result.map(|utxo| sum + utxo.value.to_sat()) - })?; - - // total amt in outputs should be equal - let total_output_amt: u64 = psbt - .unsigned_tx - .output - .iter() - .fold(0, |sum, add| sum + add.value.to_sat()); - - let dust = total_input_amt - total_output_amt; - - // now compute the size of the tx - let fake = Self::sign_psbt_fake(psbt); - let vsize = fake.vsize(); - - // absolut amount of fees - let fee_amt: u64 = (fee_rate * vsize as u32).into(); - - // now deduce the fees from one of the payer outputs - // TODO deduce fee from the change address - if fee_amt > dust { - let mut rng = thread_rng(); - if let Some(deduce_from) = payer_vouts.choose(&mut rng) { - let output = &mut psbt.unsigned_tx.output[*deduce_from as usize]; - let old_value = output.value; - output.value = old_value - Amount::from_sat(fee_amt - dust); // account for eventual dust - } else { - return Err(Error::msg("no payer vout")); - } - } - - Ok(()) - } - - pub fn create_new_psbt( - &self, - inputs: Vec, - mut recipients: Vec, - payload: Option<&[u8]> - ) -> Result { - let mut tx_in: Vec = vec![]; - let mut inputs_data: Vec<(ScriptBuf, u64, Scalar)> = vec![]; - let mut total_input_amount = 0u64; - let mut total_output_amount = 0u64; - - for i in inputs { - tx_in.push(TxIn { - previous_output: bitcoin::OutPoint::from_str(&i.txoutpoint)?, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::new(), - }); - - let scalar = Scalar::from_be_bytes(FromHex::from_hex(&i.tweak)?)?; - - total_input_amount += i.amount; - - inputs_data.push((ScriptBuf::from_hex(&i.script)?, i.amount, scalar)); - } - - // We could compute the outputs key right away, - // but keeping things separated may be interesting, - // for example creating transactions in a watch-only wallet - // and using another signer - let placeholder_spk = ScriptBuf::new_p2tr_tweaked( - bitcoin::XOnlyPublicKey::from_str(NUMS)?.dangerous_assume_tweaked(), - ); - - let _outputs: Result> = recipients - .iter() - .map(|o| { - let script_pubkey: ScriptBuf; - - match SilentPaymentAddress::try_from(o.address.as_str()) { - Ok(sp_address) => { - if self.sp_receiver.is_testnet != sp_address.is_testnet() { - return Err(Error::msg(format!( - "Wrong network for address {}", - sp_address - ))); - } - - script_pubkey = placeholder_spk.clone(); - } - Err(_) => { - let unchecked_address = Address::from_str(&o.address)?; // TODO: handle better garbage string - - let address_is_testnet = match *unchecked_address.network() { - Network::Bitcoin => false, - _ => true, - }; - - if self.sp_receiver.is_testnet != address_is_testnet { - return Err(Error::msg(format!( - "Wrong network for address {}", - unchecked_address.assume_checked() - ))); - } - - script_pubkey = ScriptBuf::from_bytes( - unchecked_address - .assume_checked() - .script_pubkey() - .to_bytes(), - ); - } - } - - total_output_amount += o.amount; - - Ok(TxOut { - value: Amount::from_sat(o.amount), - script_pubkey, - }) - }) - .collect(); - - let mut outputs = _outputs?; - - let change_amt = total_input_amount - total_output_amount; - - // Add change output - let change_address = self.sp_receiver.get_change_address(); - - outputs.push(TxOut { - value: Amount::from_sat(change_amt), - script_pubkey: placeholder_spk, - }); - - recipients.push(Recipient { - address: change_address, - amount: change_amt, - nb_outputs: 1, - }); - - if let Some(data) = payload { - if data.len() > 40 { - return Err(Error::msg("Payload must be max 40B")); - } - let mut op_return = bitcoin::script::PushBytesBuf::new(); - op_return.extend_from_slice(data); - outputs.push(TxOut { - value: Amount::from_sat(0), - script_pubkey: ScriptBuf::new_op_return(op_return), - }); - } - - let tx = bitcoin::Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: tx_in, - output: outputs, - }; - - let mut psbt = Psbt::from_unsigned_tx(tx)?; - - // Add the witness utxo to the input in psbt - for (i, input_data) in inputs_data.iter().enumerate() { - let (script_pubkey, value, tweak) = input_data; - let witness_txout = TxOut { - value: Amount::from_sat(*value), - script_pubkey: script_pubkey.clone(), - }; - let mut psbt_input = Input { - witness_utxo: Some(witness_txout), - ..Default::default() - }; - psbt_input.proprietary.insert( - raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(), - }, - tweak.to_be_bytes().to_vec(), - ); - psbt.inputs[i] = psbt_input; - } - - for (i, recipient) in recipients.iter().enumerate() { - if let Ok(sp_address) = SilentPaymentAddress::try_from(recipient.address.as_str()) { - // Add silentpayment address to the output - let mut psbt_output = Output { - ..Default::default() - }; - psbt_output.proprietary.insert( - raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), - }, - serialize(&sp_address.to_string()), - ); - psbt.outputs[i] = psbt_output; - } else { - // Regular address, we don't need to add more data - continue; - } - } - - Ok(psbt) - } - - pub fn get_a_sum_secret_keys(input: &[SecretKey]) -> SecretKey { - let secp = Secp256k1::new(); - - let mut negated_keys: Vec = vec![]; - - for key in input { - let (_, parity) = key.x_only_public_key(&secp); - - if parity == bitcoin::secp256k1::Parity::Odd { - negated_keys.push(key.negate()); - } else { - negated_keys.push(*key); - } - } - - let (head, tail) = negated_keys.split_first().unwrap(); - - let result: SecretKey = tail - .iter() - .fold(*head, |acc, &item| acc.add_tweak(&item.into()).unwrap()); - - result - } - - pub(crate) fn taproot_sighash< - T: std::ops::Deref + std::borrow::Borrow, - >( - input: &Input, - prevouts: &Vec<&TxOut>, - input_index: usize, - cache: &mut SighashCache, - tapleaf_hash: Option, - ) -> Result<(Message, PsbtSighashType), Error> { - let prevouts = Prevouts::All(prevouts); - - let hash_ty = input - .sighash_type - .map(|ty| ty.taproot_hash_ty()) - .unwrap_or(Ok(bitcoin::TapSighashType::Default))?; - - let sighash = match tapleaf_hash { - Some(leaf_hash) => cache.taproot_script_spend_signature_hash( - input_index, - &prevouts, - leaf_hash, - hash_ty, - )?, - None => cache.taproot_key_spend_signature_hash(input_index, &prevouts, hash_ty)?, - }; - let msg = Message::from_digest(sighash.into_32()); - Ok((msg, hash_ty.into())) - } - - // Sign a transaction with garbage, used for easier fee estimation - fn sign_psbt_fake(psbt: &Psbt) -> Transaction { - let mut fake_psbt = psbt.clone(); - - let fake_sig = [1u8; 64]; - - for i in fake_psbt.inputs.iter_mut() { - i.tap_key_sig = Some(Signature::from_slice(&fake_sig).unwrap()); - } - - Self::finalize_psbt(&mut fake_psbt).unwrap(); - - fake_psbt.extract_tx().expect("Invalid fake tx") - } - - pub fn sign_psbt(&self, psbt: Psbt) -> Result { - let b_spend = match self.spend_key { - SpendKey::Secret(key) => key, - SpendKey::Public(_) => return Err(Error::msg("Watch-only wallet, can't spend")), - }; - - let mut cache = SighashCache::new(&psbt.unsigned_tx); - - let mut prevouts: Vec<&TxOut> = vec![]; - - for input in &psbt.inputs { - if let Some(witness_utxo) = &input.witness_utxo { - prevouts.push(witness_utxo); - } - } - - let mut signed_psbt = psbt.clone(); - - let secp = Secp256k1::signing_only(); - - for (i, input) in psbt.inputs.iter().enumerate() { - let tap_leaf_hash: Option = None; - - let (msg, sighash_ty) = - Self::taproot_sighash(input, &prevouts, i, &mut cache, tap_leaf_hash)?; - - // Construct the signing key - let tweak = input.proprietary.get(&raw::ProprietaryKey { - prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), - subtype: PSBT_SP_SUBTYPE, - key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(), - }); - - if tweak.is_none() { - panic!("Missing tweak") - }; - - let tweak = SecretKey::from_slice(tweak.unwrap().as_slice()).unwrap(); - - let sk = b_spend.add_tweak(&tweak.into())?; - - let keypair = Keypair::from_secret_key(&secp, &sk); - - let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng()); - - signed_psbt.inputs[i].tap_key_sig = Some(Signature { - sig, - hash_ty: sighash_ty.taproot_hash_ty()?, - }); - } - - Ok(signed_psbt) - } - - pub(crate) fn finalize_psbt(psbt: &mut Psbt) -> Result<()> { - psbt.inputs.iter_mut().for_each(|i| { - let mut script_witness = Witness::new(); - if let Some(sig) = i.tap_key_sig { - script_witness.push(sig.to_vec()); - } else { - panic!("Missing signature"); - } - - i.final_script_witness = Some(script_witness); - - // Clear all the data fields as per the spec. - i.tap_key_sig = None; - i.partial_sigs = BTreeMap::new(); - i.sighash_type = None; - i.redeem_script = None; - i.witness_script = None; - i.bip32_derivation = BTreeMap::new(); - }); - Ok(()) - } -} - -pub fn derive_keys_from_seed(seed: &[u8], is_testnet: bool) -> Result<(SecretKey, SecretKey)> { - let network = if is_testnet { - Network::Testnet - } else { - Network::Bitcoin - }; - - let xprv = Xpriv::new_master(network, seed)?; - - let (scan_privkey, spend_privkey) = derive_keys_from_xprv(xprv)?; - - Ok((scan_privkey, spend_privkey)) -} - -fn derive_keys_from_xprv(xprv: Xpriv) -> Result<(SecretKey, SecretKey)> { - let (scan_path, spend_path) = match xprv.network { - bitcoin::Network::Bitcoin => ("m/352h/0h/0h/1h/0", "m/352h/0h/0h/0h/0"), - _ => ("m/352h/1h/0h/1h/0", "m/352h/1h/0h/0h/0"), - }; - - let secp = Secp256k1::signing_only(); - let scan_path = DerivationPath::from_str(scan_path)?; - let spend_path = DerivationPath::from_str(spend_path)?; - let scan_privkey = xprv.derive_priv(&secp, &scan_path)?.private_key; - let spend_privkey = xprv.derive_priv(&secp, &spend_path)?.private_key; - - Ok((scan_privkey, spend_privkey)) -}