From eb6699baca3d34261171ba905d3b70631bc6e133 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 21 Feb 2024 17:11:13 +0100 Subject: [PATCH 01/27] base_server --- Cargo.lock | 1170 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 + src/main.rs | 138 ++++++ 3 files changed, 1323 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..22432d7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1170 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + +[[package]] +name = "bitcoin" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" +dependencies = [ + "bech32", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + +[[package]] +name = "bitcoincore-zmq" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1e21acea23b5ec33f912350f18adee9a08bd513dca9f66f0e2cfe9d756ef46" +dependencies = [ + "bitcoin", + "zmq", + "zmq-sys", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dircpy" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29259db751c34980bfc44100875890c507f585323453b91936960ab1104272ca" +dependencies = [ + "jwalk", + "log", + "walkdir", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" + +[[package]] +name = "hex-conservative" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "jwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" +dependencies = [ + "crossbeam", + "rayon", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.6", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sdk_relay" +version = "0.1.0" +dependencies = [ + "bitcoincore-zmq", + "env_logger", + "futures-util", + "log", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-tungstenite", +] + +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d90f4e0f530c4c69f62b80d839e9ef3855edc9cba471a160c4d692deed62b401" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeromq-src" +version = "0.2.6+4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc120b771270365d5ed0dfb4baf1005f2243ae1ae83703265cb3504070f4160b" +dependencies = [ + "cc", + "dircpy", +] + +[[package]] +name = "zmq" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd3091dd571fb84a9b3e5e5c6a807d186c411c812c8618786c3c30e5349234e7" +dependencies = [ + "bitflags", + "libc", + "zmq-sys", +] + +[[package]] +name = "zmq-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8351dc72494b4d7f5652a681c33634063bbad58046c1689e75270908fdc864" +dependencies = [ + "libc", + "system-deps", + "zeromq-src", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3b564a7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sdk_relay" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitcoincore-zmq = "1.4.0" +env_logger = "0.9" +futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] } +log = "0.4.20" +serde = { version = "1.0.193", features = ["derive"]} +serde_json = "1.0" +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/main.rs b/src/main.rs new file mode 100644 index 0000000..0303cb1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,138 @@ +use std::{ + collections::HashMap, + env, + io::Error as IoError, + net::SocketAddr, + sync::{Arc, Mutex}, +}; + +use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; + +use log::{debug, error}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tokio_tungstenite::tungstenite::Message; + +type Tx = UnboundedSender; + +type PeerMap = Arc>>; + +// #[derive(Deserialize, Serialize, Debug)] +// struct AnkMessage { +// dest: SilentPaymentId, +// payload: String +// } + +// impl TryFrom for AnkMessage { +// type Error = serde_json::Error; + +// fn try_from(value: Message) -> Result { +// match value { +// Message::Text(s) => serde_json::from_str(&s), +// _ => Err(serde_json::Error::custom("Unsupported message type")), +// } +// } +// } + +async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr) { + debug!("Incoming TCP connection from: {}", addr); + + let ws_stream = tokio_tungstenite::accept_async(raw_stream) + .await + .expect("Error during the websocket handshake occurred"); + debug!("WebSocket connection established"); + + // Insert the write part of this peer to the peer map. + let (tx, rx) = unbounded_channel(); + peer_map.lock().unwrap().insert(addr, tx); + + let (outgoing, incoming) = ws_stream.split(); + + let broadcast_incoming = incoming.try_for_each({ + let peer_map = peer_map.clone(); + move |msg| { + 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()); + }); + + future::ok(()) + } + }); + + let receive_from_others = UnboundedReceiverStream::new(rx) + .map(Ok) + .forward(outgoing) + .map(|result| { + if let Err(e) = result { + debug!("Error sending message: {}", e); + } + }); + + pin_mut!(broadcast_incoming, receive_from_others); + future::select(broadcast_incoming, receive_from_others).await; + + debug!("{} disconnected", &addr); + peer_map.lock().unwrap().remove(&addr); +} + +async fn handle_zmq(peer_map: PeerMap) { + tokio::task::spawn_blocking(move || { + debug!("Starting listening on Core"); + for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { + match msg { + Ok(core_msg) => { + debug!("Received a {} message", core_msg.topic_str()); + let peers = peer_map.lock().unwrap(); + + let vectors = core_msg.serialize_to_vecs(); + + let mut payload: Vec = Vec::with_capacity(vectors[0].len() + vectors[1].len() + vectors[2].len()); + for v in vectors { + payload.extend(v); + } + + for tx in peers.values() { + let _ = tx.send(Message::Binary(payload.clone())); + } + } + Err(e) => { + error!("Error receiving ZMQ message: {}", e); + continue; + } + } + } + }); +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<(), IoError> { + env_logger::init(); + + let addr = env::args() + .nth(1) + .unwrap_or_else(|| "127.0.0.1:8080".to_string()); + + let state = PeerMap::new(Mutex::new(HashMap::new())); + + // Subscribe to Bitcoin Core + tokio::spawn(handle_zmq(state.clone())); + + // Create the event loop and TCP listener we'll accept connections on. + let try_socket = TcpListener::bind(&addr).await; + let listener = try_socket.expect("Failed to bind"); + debug!("Listening on: {}", addr); + + // 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)); + } + + Ok(()) +} From 2d044ec2c2bb7cf638bf4e2326b4546f514cd6fa Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 7 Mar 2024 09:36:59 +0100 Subject: [PATCH 02/27] Analyze rawtx msg and add sp_tweak --- Cargo.lock | 92 ++++++++++++++- Cargo.toml | 4 + src/daemon.rs | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 98 ++++++++++++---- src/sp.rs | 155 +++++++++++++++++++++++++ 5 files changed, 629 insertions(+), 24 deletions(-) create mode 100644 src/daemon.rs create mode 100644 src/sp.rs diff --git a/Cargo.lock b/Cargo.lock index 22432d7..91c17d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" + [[package]] name = "atty" version = "0.2.14" @@ -58,24 +64,43 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.10.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bitcoin" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" dependencies = [ - "bech32", + "bech32 0.10.0-beta", "bitcoin-internals", "bitcoin_hashes", "hex-conservative", "hex_lit", "secp256k1", + "serde", ] [[package]] @@ -83,6 +108,9 @@ name = "bitcoin-internals" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +dependencies = [ + "serde", +] [[package]] name = "bitcoin_hashes" @@ -92,6 +120,31 @@ checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ "bitcoin-internals", "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb70725a621848c83b3809913d5314c0d20ca84877d99dd909504b564edab00" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662" +dependencies = [ + "bitcoin", + "serde", + "serde_json", ] [[package]] @@ -386,6 +439,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-conservative" version = "0.1.1" @@ -456,6 +515,17 @@ dependencies = [ "libc", ] +[[package]] +name = "jsonrpc" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" +dependencies = [ + "base64", + "serde", + "serde_json", +] + [[package]] name = "jwalk" version = "0.8.1" @@ -675,12 +745,16 @@ dependencies = [ name = "sdk_relay" version = "0.1.0" dependencies = [ + "anyhow", + "bitcoin", + "bitcoincore-rpc", "bitcoincore-zmq", "env_logger", "futures-util", "log", "serde", "serde_json", + "silentpayments", "tokio", "tokio-stream", "tokio-tungstenite", @@ -693,7 +767,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "bitcoin_hashes", + "rand", "secp256k1-sys", + "serde", ] [[package]] @@ -756,6 +832,20 @@ dependencies = [ "digest", ] +[[package]] +name = "silentpayments" +version = "0.1.0" +source = "git+https://github.com/cygnet3/rust-silentpayments?branch=master#e915f5f8daef4b39ea32902963c143a5c0ed1746" +dependencies = [ + "bech32 0.9.1", + "bimap", + "bitcoin_hashes", + "hex", + "secp256k1", + "serde", + "serde_json", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 3b564a7..bdf1d79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0" +bitcoin = { version = "0.31.1", features = ["serde"] } +bitcoincore-rpc = { version = "0.18" } bitcoincore-zmq = "1.4.0" env_logger = "0.9" futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] } log = "0.4.20" serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" +silentpayments = { git = "https://github.com/cygnet3/rust-silentpayments", branch = "master", features = ['utils'] } 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/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..fde0250 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,304 @@ +use anyhow::{Context, Result}; + +use bitcoin::{consensus::deserialize, hashes::hex::FromHex}; +use bitcoin::{Amount, BlockHash, Transaction, Txid}; +use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; +// use crossbeam_channel::Receiver; +// use parking_lot::Mutex; +use serde_json::{json, Value}; + +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::time::Duration; + +pub struct SensitiveAuth(pub Auth); + +impl SensitiveAuth { + pub(crate) fn get_auth(&self) -> Auth { + self.0.clone() + } +} + +enum PollResult { + Done(Result<()>), + Retry, +} + +fn rpc_poll(client: &mut Client, skip_block_download_wait: bool) -> PollResult { + match client.get_blockchain_info() { + Ok(info) => { + if skip_block_download_wait { + // bitcoind RPC is available, don't wait for block download to finish + return PollResult::Done(Ok(())); + } + let left_blocks = info.headers - info.blocks; + if info.initial_block_download || left_blocks > 0 { + log::info!( + "waiting for {} blocks to download{}", + left_blocks, + if info.initial_block_download { + " (IBD)" + } else { + "" + } + ); + return PollResult::Retry; + } + PollResult::Done(Ok(())) + } + Err(err) => { + if let Some(e) = extract_bitcoind_error(&err) { + if e.code == -28 { + log::debug!("waiting for RPC warmup: {}", e.message); + return PollResult::Retry; + } + } + PollResult::Done(Err(err).context("daemon not available")) + } + } +} + +fn read_cookie(path: &Path) -> Result<(String, String)> { + // Load username and password from bitcoind cookie file: + // * https://github.com/bitcoin/bitcoin/pull/6388/commits/71cbeaad9a929ba6a7b62d9b37a09b214ae00c1a + // * https://bitcoin.stackexchange.com/questions/46782/rpc-cookie-authentication + let mut file = File::open(path) + .with_context(|| format!("failed to open bitcoind cookie file: {}", path.display()))?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .with_context(|| format!("failed to read bitcoind cookie from {}", path.display()))?; + + let parts: Vec<&str> = contents.splitn(2, ':').collect(); + anyhow::ensure!( + parts.len() == 2, + "failed to parse bitcoind cookie - missing ':' separator" + ); + Ok((parts[0].to_owned(), parts[1].to_owned())) +} + +fn rpc_connect() -> Result { + let rpc_url = "http://127.0.0.1:39332"; + // 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 builder = match daemon_auth.get_auth() { + Auth::None => builder, + Auth::UserPass(user, pass) => builder.auth(user, Some(pass)), + Auth::CookieFile(path) => { + let (user, pass) = read_cookie(&path)?; + builder.auth(user, Some(pass)) + } + }; + Ok(Client::from_jsonrpc(jsonrpc::Client::with_transport( + builder.build(), + ))) +} + +pub struct Daemon { + // p2p: Mutex, + rpc: Client, +} + +impl Daemon { + pub(crate) fn connect( + // config: &Config, + // exit_flag: &ExitFlag, + // metrics: &Metrics, + ) -> Result { + let mut rpc = rpc_connect()?; + + loop { + match rpc_poll(&mut rpc, true) { + PollResult::Done(result) => { + result.context("bitcoind RPC polling failed")?; + break; // on success, finish polling + } + PollResult::Retry => { + std::thread::sleep(std::time::Duration::from_secs(1)); // wait a bit before polling + } + } + } + + let network_info = rpc.get_network_info()?; + // if network_info.version < 21_00_00 { + // bail!("electrs requires bitcoind 0.21+"); + // } + if !network_info.network_active { + anyhow::bail!("electrs requires active bitcoind p2p network"); + } + let info = rpc.get_blockchain_info()?; + if info.pruned { + anyhow::bail!("electrs requires non-pruned bitcoind node"); + } + + // let p2p = tokio::sync::Mutex::new(Connection::connect( + // config.network, + // config.daemon_p2p_addr, + // metrics, + // config.signet_magic, + // )?); + Ok(Self { rpc }) + } + + pub(crate) fn estimate_fee(&self, nblocks: u16) -> Result> { + Ok(self + .rpc + .estimate_smart_fee(nblocks, None) + .context("failed to estimate fee")? + .fee_rate) + } + + pub(crate) fn get_relay_fee(&self) -> Result { + Ok(self + .rpc + .get_network_info() + .context("failed to get relay fee")? + .relay_fee) + } + + pub(crate) fn broadcast(&self, tx: &Transaction) -> Result { + self.rpc + .send_raw_transaction(tx) + .context("failed to broadcast transaction") + } + + pub(crate) fn get_transaction_info( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result { + // No need to parse the resulting JSON, just return it as-is to the client. + self.rpc + .call( + "getrawtransaction", + &[json!(txid), json!(true), json!(blockhash)], + ) + .context("failed to get transaction info") + } + + pub(crate) fn get_transaction_hex( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result { + use bitcoin::consensus::serde::{hex::Lower, Hex, With}; + + let tx = self.get_transaction(txid, blockhash)?; + #[derive(serde::Serialize)] + #[serde(transparent)] + struct TxAsHex(#[serde(with = "With::>")] Transaction); + serde_json::to_value(TxAsHex(tx)).map_err(Into::into) + } + + pub(crate) fn get_transaction( + &self, + txid: &Txid, + blockhash: Option, + ) -> Result { + self.rpc + .get_raw_transaction(txid, blockhash.as_ref()) + .context("failed to get transaction") + } + + pub(crate) fn get_block_txids(&self, blockhash: BlockHash) -> Result> { + Ok(self + .rpc + .get_block_info(&blockhash) + .context("failed to get block txids")? + .tx) + } + + pub(crate) fn get_mempool_txids(&self) -> Result> { + self.rpc + .get_raw_mempool() + .context("failed to get mempool txids") + } + + pub(crate) fn get_mempool_entries( + &self, + txids: &[Txid], + ) -> Result>> { + let client = self.rpc.get_jsonrpc_client(); + log::debug!("getting {} mempool entries", txids.len()); + let args: Vec<_> = txids + .iter() + .map(|txid| vec![serde_json::value::to_raw_value(txid).unwrap()]) + .collect(); + let reqs: Vec<_> = args + .iter() + .map(|a| client.build_request("getmempoolentry", a)) + .collect(); + let res = client.send_batch(&reqs).context("batch request failed")?; + log::debug!("got {} mempool entries", res.len()); + Ok(res + .into_iter() + .map(|r| { + r.context("missing response")? + .result::() + .context("invalid response") + }) + .collect()) + } + + pub(crate) fn get_mempool_transactions( + &self, + txids: &[Txid], + ) -> Result>> { + let client = self.rpc.get_jsonrpc_client(); + log::debug!("getting {} transactions", txids.len()); + let args: Vec<_> = txids + .iter() + .map(|txid| vec![serde_json::value::to_raw_value(txid).unwrap()]) + .collect(); + let reqs: Vec<_> = args + .iter() + .map(|a| client.build_request("getrawtransaction", a)) + .collect(); + let res = client.send_batch(&reqs).context("batch request failed")?; + log::debug!("got {} mempool transactions", res.len()); + Ok(res + .into_iter() + .map(|r| -> Result { + let tx_hex = r + .context("missing response")? + .result::() + .context("invalid response")?; + let tx_bytes = Vec::from_hex(&tx_hex).context("non-hex transaction")?; + deserialize(&tx_bytes).context("invalid transaction") + }) + .collect()) + } + + // pub(crate) fn get_new_headers(&self, chain: &Chain) -> Result> { + // self.p2p.lock().get_new_headers(chain) + // } + + // pub(crate) fn for_blocks(&self, blockhashes: B, func: F) -> Result<()> + // where + // B: IntoIterator, + // F: FnMut(BlockHash, SerBlock), + // { + // self.p2p.lock().for_blocks(blockhashes, func) + // } + + // pub(crate) fn new_block_notification(&self) -> Receiver<()> { + // self.p2p.lock().new_block_notification() + // } +} + +pub(crate) type RpcError = bitcoincore_rpc::jsonrpc::error::RpcError; + +pub(crate) fn extract_bitcoind_error(err: &bitcoincore_rpc::Error) -> Option<&RpcError> { + use bitcoincore_rpc::{ + jsonrpc::error::Error::Rpc as ServerError, Error::JsonRpc as JsonRpcError, + }; + match err { + JsonRpcError(ServerError(e)) => Some(e), + _ => None, + } +} diff --git a/src/main.rs b/src/main.rs index 0303cb1..7c34968 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::{ sync::{Arc, Mutex}, }; +use bitcoin::{consensus::deserialize, secp256k1::PublicKey}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; @@ -14,27 +15,16 @@ use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; +mod sp; +mod daemon; + +use crate::daemon::Daemon; +use crate::sp::VinData; + type Tx = UnboundedSender; type PeerMap = Arc>>; -// #[derive(Deserialize, Serialize, Debug)] -// struct AnkMessage { -// dest: SilentPaymentId, -// payload: String -// } - -// impl TryFrom for AnkMessage { -// type Error = serde_json::Error; - -// fn try_from(value: Message) -> Result { -// match value { -// Message::Text(s) => serde_json::from_str(&s), -// _ => Err(serde_json::Error::custom("Unsupported message type")), -// } -// } -// } - async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr) { debug!("Incoming TCP connection from: {}", addr); @@ -82,7 +72,17 @@ async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: Socke peer_map.lock().unwrap().remove(&addr); } -async fn handle_zmq(peer_map: PeerMap) { +fn flatten_msg(parts: &[Vec]) -> Vec { + let total_len = parts.iter().fold(0, |acc, v| acc + v.len()); + let mut final_vec = Vec::with_capacity(total_len); + for p in parts { + final_vec.extend(p); + } + + final_vec +} + +async fn handle_zmq(peer_map: PeerMap, daemon: Daemon) { tokio::task::spawn_blocking(move || { debug!("Starting listening on Core"); for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { @@ -91,11 +91,60 @@ async fn handle_zmq(peer_map: PeerMap) { debug!("Received a {} message", core_msg.topic_str()); let peers = peer_map.lock().unwrap(); - let vectors = core_msg.serialize_to_vecs(); + let payload: Vec; + match core_msg.topic_str() { + "rawtx" => { + let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec()).unwrap(); - let mut payload: Vec = Vec::with_capacity(vectors[0].len() + vectors[1].len() + vectors[2].len()); - for v in vectors { - payload.extend(v); + if tx.is_coinbase() { + continue; + } + + let mut outpoints: Vec<(String, u32)> = 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.get_transaction(&input.previous_output.txid, None).unwrap(); + 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) { + Ok(res) => { + if let Some(pubkey) = res { + pubkeys.push(pubkey); + } else { + continue; + } + }, + Err(e) => { + log::error!("Can't extract pubkey from input: {}", e.to_string()); + continue; + } + } + } else { + log::error!("Transaction with a non existing input"); + continue; + } + } + let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); + match silentpayments::utils::receiving::recipient_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()); + payload = flatten_msg(&vecs); + }, + Err(e) => { + log::error!("Failed to compute tweak data: {}", e.to_string()); + continue; + } + } + }, + _ => { + payload = flatten_msg(&core_msg.serialize_to_vecs()); + } } for tx in peers.values() { @@ -121,8 +170,11 @@ async fn main() -> Result<(), IoError> { let state = PeerMap::new(Mutex::new(HashMap::new())); + // Connect the rpc daemon + let daemon = Daemon::connect().unwrap(); + // Subscribe to Bitcoin Core - tokio::spawn(handle_zmq(state.clone())); + tokio::spawn(handle_zmq(state.clone(), daemon)); // Create the event loop and TCP listener we'll accept connections on. let try_socket = TcpListener::bind(&addr).await; diff --git a/src/sp.rs b/src/sp.rs new file mode 100644 index 0000000..01eb718 --- /dev/null +++ b/src/sp.rs @@ -0,0 +1,155 @@ +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) +} From 5f4efa5aa349b03b27e329c3eb6becda101b6ba7 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Fri, 8 Mar 2024 15:31:23 +0100 Subject: [PATCH 03/27] Add the sp wallet + daemon logic, some refactoring --- Cargo.lock | 384 ++++++++++++++++++++++- Cargo.toml | 1 + src/constants.rs | 24 ++ src/daemon.rs | 80 ++++- src/db.rs | 55 ++++ src/main.rs | 184 ++++++----- src/spclient.rs | 787 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1430 insertions(+), 85 deletions(-) create mode 100644 src/constants.rs create mode 100644 src/db.rs create mode 100644 src/spclient.rs diff --git a/Cargo.lock b/Cargo.lock index 91c17d2..a591bca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.80" @@ -70,6 +85,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bech32" version = "0.9.1" @@ -173,6 +194,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + [[package]] name = "byteorder" version = "1.5.0" @@ -211,6 +238,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.4", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -286,12 +332,57 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -412,6 +503,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -480,6 +577,35 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -490,6 +616,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.3" @@ -497,7 +634,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", + "serde", ] [[package]] @@ -515,13 +653,22 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "jsonrpc" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" dependencies = [ - "base64", + "base64 0.13.1", "serde", "serde_json", ] @@ -574,6 +721,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -593,6 +755,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -617,6 +785,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -754,6 +928,7 @@ dependencies = [ "log", "serde", "serde_json", + "serde_with", "silentpayments", "tokio", "tokio-stream", @@ -821,6 +996,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -871,6 +1076,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "2.0.49" @@ -930,6 +1141,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1023,7 +1265,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap", + "indexmap 2.2.3", "serde", "serde_spanned", "toml_datetime", @@ -1121,6 +1363,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -1152,13 +1448,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1167,13 +1472,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -1182,42 +1502,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winnow" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index bdf1d79..0a17f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ futures-util = { version = "0.3.28", default-features = false, features = ["sink 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'] } tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", "sync"] } tokio-stream = "0.1" diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..c49a64b --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +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 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 fde0250..c955cc9 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,12 +1,14 @@ -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result, Error}; use bitcoin::{consensus::deserialize, hashes::hex::FromHex}; -use bitcoin::{Amount, BlockHash, Transaction, Txid}; +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, jsonrpc, Auth, Client, RpcApi}; // use crossbeam_channel::Receiver; // use parking_lot::Mutex; use serde_json::{json, Value}; +use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; @@ -113,7 +115,7 @@ impl Daemon { let mut rpc = rpc_connect()?; loop { - match rpc_poll(&mut rpc, true) { + match rpc_poll(&mut rpc, false) { PollResult::Done(result) => { result.context("bitcoind RPC polling failed")?; break; // on success, finish polling @@ -161,6 +163,78 @@ impl Daemon { .relay_fee) } + pub(crate) fn get_current_height(&self) -> Result { + Ok(self + .rpc + .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 create_psbt(&self, utxo: ListUnspentResultEntry, spk: ScriptBuf, network: Network) -> Result { + let input = CreateRawTransactionInput { + txid: utxo.txid, + vout: utxo.vout, + sequence: None + }; + let address = Address::from_script(&spk, network)?; + 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()) + } + + pub(crate) fn process_psbt(&self, psbt: String) -> Result { + 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")) + } + } + + pub(crate) fn finalize_psbt(&self, psbt: String) -> Result> { + let final_tx = self.rpc + .finalize_psbt(&psbt, Some(true))?; + + 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")) + } + } + + pub(crate) fn get_network(&self) -> Result { + 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) diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..10c33da --- /dev/null +++ b/src/db.rs @@ -0,0 +1,55 @@ +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 7c34968..5cdb014 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,23 +3,33 @@ use std::{ env, io::Error as IoError, net::SocketAddr, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, MutexGuard}, }; -use bitcoin::{consensus::deserialize, secp256k1::PublicKey}; +use bitcoin::{consensus::deserialize, key::TapTweak, secp256k1::PublicKey, OutPoint, ScriptBuf, XOnlyPublicKey}; +use bitcoincore_rpc::json 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 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}; + mod sp; mod daemon; +mod spclient; +mod constants; +mod db; use crate::daemon::Daemon; use crate::sp::VinData; +use crate::spclient::{SpClient, SpendKey, OutputSpendStatus}; type Tx = UnboundedSender; @@ -80,81 +90,80 @@ fn flatten_msg(parts: &[Vec]) -> Vec { } final_vec -} +} -async fn handle_zmq(peer_map: PeerMap, daemon: Daemon) { +fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc>) -> Result> { + let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec())?; + + if tx.is_coinbase() { + return Err(Error::msg("Can't process coinbase transaction")); + } + + let mut outpoints: Vec<(String, u32)> = 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() + .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"))?; + + 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) { + Ok(Some(pubkey)) => pubkeys.push(pubkey), + Ok(None) => continue, + 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")); + } + } + + let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); + match silentpayments::utils::receiving::recipient_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()))) + } +} + +async fn handle_zmq(peer_map: PeerMap, daemon: Arc>) { tokio::task::spawn_blocking(move || { debug!("Starting listening on Core"); for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { - match msg { - Ok(core_msg) => { - debug!("Received a {} message", core_msg.topic_str()); - let peers = peer_map.lock().unwrap(); - - let payload: Vec; - match core_msg.topic_str() { - "rawtx" => { - let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec()).unwrap(); - - if tx.is_coinbase() { - continue; - } - - let mut outpoints: Vec<(String, u32)> = 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.get_transaction(&input.previous_output.txid, None).unwrap(); - 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) { - Ok(res) => { - if let Some(pubkey) = res { - pubkeys.push(pubkey); - } else { - continue; - } - }, - Err(e) => { - log::error!("Can't extract pubkey from input: {}", e.to_string()); - continue; - } - } - } else { - log::error!("Transaction with a non existing input"); - continue; - } - } - let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); - match silentpayments::utils::receiving::recipient_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()); - payload = flatten_msg(&vecs); - }, - Err(e) => { - log::error!("Failed to compute tweak data: {}", e.to_string()); - continue; - } - } - }, - _ => { - payload = flatten_msg(&core_msg.serialize_to_vecs()); - } - } - - for tx in peers.values() { - let _ = tx.send(Message::Binary(payload.clone())); - } - } + let core_msg = match msg { + Ok(core_msg) => core_msg, Err(e) => { error!("Error receiving ZMQ message: {}", e); continue; } + }; + 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 + } + }, + _ => { + flatten_msg(&core_msg.serialize_to_vecs()) + } + }; + + for tx in peers.values() { + let _ = tx.send(Message::Binary(payload.clone())); } } }); @@ -167,14 +176,47 @@ async fn main() -> Result<(), IoError> { 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()); + let is_testnet: bool = env::args() + .nth(3) + .unwrap_or_else(|| "true".to_owned()) + .parse() + .expect("Please provide either \"true\" or \"false\""); let state = PeerMap::new(Mutex::new(HashMap::new())); // Connect the rpc daemon let daemon = Daemon::connect().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, + 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") + } + }; + + log::info!("Using wallet {} with address {}", sp_client.label, sp_client.get_receiving_address()); + + let shared_sp_client = Arc::new(Mutex::new(sp_client)); + let shared_daemon = Arc::new(Mutex::new(daemon)); + // Subscribe to Bitcoin Core - tokio::spawn(handle_zmq(state.clone(), daemon)); + tokio::spawn(handle_zmq(state.clone(), shared_daemon.clone())); // Create the event loop and TCP listener we'll accept connections on. let try_socket = TcpListener::bind(&addr).await; @@ -183,7 +225,7 @@ 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)); + tokio::spawn(handle_connection(state.clone(), stream, addr, shared_sp_client.clone(), shared_daemon.clone())); } Ok(()) diff --git a/src/spclient.rs b/src/spclient.rs new file mode 100644 index 0000000..7a3893d --- /dev/null +++ b/src/spclient.rs @@ -0,0 +1,787 @@ +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: &Vec) -> 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 + } + + 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)) +} From 7774207e014e23c89d5d7d2115d1a61231db8fa2 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Fri, 8 Mar 2024 20:40:25 +0100 Subject: [PATCH 04/27] Faucet spending --- src/main.rs | 185 ++++++++++++++++++++++++++++++++++++++++++++---- src/spclient.rs | 4 +- 2 files changed, 172 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5cdb014..74a0ebe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,9 @@ use std::{ - collections::HashMap, - env, - io::Error as IoError, - net::SocketAddr, - sync::{Arc, Mutex, MutexGuard}, + collections::HashMap, env, io::Error as IoError, net::SocketAddr, sync::{Arc, Mutex} }; -use bitcoin::{consensus::deserialize, key::TapTweak, secp256k1::PublicKey, OutPoint, ScriptBuf, XOnlyPublicKey}; +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 futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; @@ -35,7 +32,127 @@ type Tx = UnboundedSender; type PeerMap = Arc>>; -async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr) { +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 + + // just take the first of the list + if let Some(unspent) = unspent_list.get(0) { + 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 processed_psbt = core.process_psbt(new_psbt)?; + let tx = core.finalize_psbt(processed_psbt)?; + let final_tx = deserialize::(&tx)?; + + let _ = core.broadcast(&final_tx)?; + + Ok(final_tx) + } else { + // we have 0 spendable outputs + 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() + } + ], + output: vec![], + version: Version::TWO, + lock_time: LockTime::ZERO + }; + + // 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)?; + + 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)?; + + 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: 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; + } + daemon.lock() + .map_err(|e| Error::msg(format!("{}", e.to_string())))? + .broadcast(&final_tx)?; + + Ok(final_tx.txid()) +} + +async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr, sp_client: Arc>, daemon: Arc>) { debug!("Incoming TCP connection from: {}", addr); let ws_stream = tokio_tungstenite::accept_async(raw_stream) @@ -52,16 +169,54 @@ 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| { - let peers = peer_map.lock().unwrap(); + 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(); - // Broadcast message to other peers - peers - .iter() - .filter(|(peer_addr, _)| peer_addr != &&addr) - .for_each(|(_, peer_tx)| { - let _ = peer_tx.send(msg.clone()); - }); + 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); + } + } + }, + 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); + } + } future::ok(()) } }); diff --git a/src/spclient.rs b/src/spclient.rs index 7a3893d..0c2eb8b 100644 --- a/src/spclient.rs +++ b/src/spclient.rs @@ -612,7 +612,7 @@ impl SpClient { Ok(psbt) } - pub fn get_a_sum_secret_keys(input: &Vec) -> SecretKey { + pub fn get_a_sum_secret_keys(input: &[SecretKey]) -> SecretKey { let secp = Secp256k1::new(); let mut negated_keys: Vec = vec![]; @@ -636,7 +636,7 @@ impl SpClient { result } - fn taproot_sighash< + pub(crate) fn taproot_sighash< T: std::ops::Deref + std::borrow::Borrow, >( input: &Input, From ed37accb673d7b8c513d5dcb931ec60a30edbae3 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Fri, 15 Mar 2024 12:39:04 +0100 Subject: [PATCH 05/27] Add electrum_client --- Cargo.toml | 1 + src/electrumclient.rs | 15 +++++++++++++++ src/main.rs | 1 + 3 files changed, 17 insertions(+) create mode 100644 src/electrumclient.rs diff --git a/Cargo.toml b/Cargo.toml index 0a17f3b..350c35f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ 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"] } log = "0.4.20" diff --git a/src/electrumclient.rs b/src/electrumclient.rs new file mode 100644 index 0000000..897d533 --- /dev/null +++ b/src/electrumclient.rs @@ -0,0 +1,15 @@ +use electrum_client::{Client, ConfigBuilder}; +use log::info; + +const ELECTRS_URI: &str = "ssl://silentpayments.dev:51002"; +const VALIDATE_DOMAIN: bool = false; // self-signed cert, so we don't validate + +pub fn create_electrum_client() -> anyhow::Result { + let config = ConfigBuilder::new() + .validate_domain(VALIDATE_DOMAIN) + .build(); + let electrum_client = Client::from_config(ELECTRS_URI, config)?; + info!("ssl client {}", ELECTRS_URI); + + Ok(electrum_client) +} diff --git a/src/main.rs b/src/main.rs index 74a0ebe..72a4eeb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod daemon; mod spclient; mod constants; mod db; +mod electrumclient; use crate::daemon::Daemon; use crate::sp::VinData; From 3ebc319a26e1c7bdebdd15b9b33f26fb9170662e Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Fri, 15 Mar 2024 12:39:17 +0100 Subject: [PATCH 06/27] Add DUST constant --- src/constants.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/constants.rs b/src/constants.rs index c49a64b..94721a8 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use bitcoin::Amount; type SecretKeyString = String; type PublicKeyString = String; @@ -10,6 +10,8 @@ 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, 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 07/27] 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)) -} From a28f40fa0cdb6dd59dcb4c082eb9d1e3ea009c36 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 21 Mar 2024 18:05:46 +0100 Subject: [PATCH 08/27] derive Debug for Daemon --- src/daemon.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/daemon.rs b/src/daemon.rs index 5c97fa7..6720a0a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -118,6 +118,7 @@ fn rpc_connect(rpcwallet: Option) -> Result { ))) } +#[derive(Debug)] pub struct Daemon { // p2p: Mutex, rpc: Client, From d33c3e9735fbbfb47953cfe154b8297613efc452 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 21 Mar 2024 18:06:17 +0100 Subject: [PATCH 09/27] Add MutexExt trait --- src/main.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main.rs b/src/main.rs index da7fedd..a55d23f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,17 @@ type PeerMap = Arc>>; const FAUCET_AMT: Amount = Amount::from_sat(1000); +pub(crate) trait MutexExt { + fn lock_anyhow(&self) -> Result, Error>; +} + +impl MutexExt for Mutex { + fn lock_anyhow(&self) -> Result, Error> { + self.lock() + .map_err(|e| Error::msg(format!("Failed to lock: {}", e))) + } +} + enum BroadcastType { Sender(SocketAddr), ExcludeSender(SocketAddr), From 6db81ee769b3275140b439e7dbc45daa0a6ab938 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 21 Mar 2024 18:06:39 +0100 Subject: [PATCH 10/27] Add SilentPaymentWallet --- src/main.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main.rs b/src/main.rs index a55d23f..4e0c9b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,27 @@ impl MutexExt for Mutex { } } +#[derive(Debug)] +pub(crate) struct SilentPaymentWallet { + sp_client: Mutex, + sp_outputs: Mutex, + storage: Mutex, +} + +impl SilentPaymentWallet { + pub fn get_client(&self) -> Result> { + self.sp_client.lock_anyhow() + } + + pub fn get_outputs(&self) -> Result> { + self.sp_outputs.lock_anyhow() + } + + pub fn get_storage(&self) -> Result> { + self.storage.lock_anyhow() + } +} + enum BroadcastType { Sender(SocketAddr), ExcludeSender(SocketAddr), From 306949e9f0a58bb3941912a9cf76980feca46303 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 21 Mar 2024 18:07:22 +0100 Subject: [PATCH 11/27] Refactoring --- Cargo.lock | 2 +- src/main.rs | 179 ++++++++++++++++++++++++++++++---------------------- src/scan.rs | 64 +++++++------------ 3 files changed, 127 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0749449..193a55b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,7 +1197,7 @@ dependencies = [ [[package]] name = "sp_backend" version = "0.1.0" -source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#0213188a95921081f5c74e5099ac46e6737a07d0" +source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#32967c214df9a25daef551a372b89c400f2369f8" dependencies = [ "anyhow", "bitcoin 0.31.1", diff --git a/src/main.rs b/src/main.rs index 4e0c9b0..4730f30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ use std::{ collections::HashMap, env, + fmt::Debug, net::SocketAddr, ops::Deref, path::PathBuf, str::FromStr, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, MutexGuard}, }; use bitcoincore_rpc::json::{self as bitcoin_json}; @@ -56,6 +57,8 @@ type Tx = UnboundedSender; type PeerMap = Arc>>; +type SharedDaemon = Arc>; + const FAUCET_AMT: Amount = Amount::from_sat(1000); pub(crate) trait MutexExt { @@ -216,19 +219,15 @@ fn find_owned_outputs( fn faucet_send( sp_address: SilentPaymentAddress, - sp_client: Arc>, - sp_outputs: Arc>, - daemon: Arc>, + sp_wallet: Arc, + shared_daemon: SharedDaemon, ) -> Result { let mut first_tx: Option = None; let final_tx: Transaction; let mut new_outpoints: HashMap; // 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 available_outpoints = sp_wallet.get_outputs()?.to_spendable_list(); let available_amt = available_outpoints .iter() @@ -252,9 +251,8 @@ fn faucet_send( nb_outputs: 1, }; - let fee_estimate = daemon - .lock() - .map_err(|e| Error::msg(format!("{}", e.to_string())))? + let fee_estimate = shared_daemon + .lock_anyhow()? .estimate_fee(6)? .unwrap_or(Amount::from_sat(1000)) .checked_div(1000) @@ -262,7 +260,7 @@ fn faucet_send( log::debug!("fee estimate for 6 blocks: {}", fee_estimate); - let wallet = sp_client.lock().map_err(|e| Error::msg(e.to_string()))?; + let wallet = sp_wallet.get_client()?; let mut new_psbt = wallet.create_new_psbt(inputs.clone(), vec![recipient], None)?; log::debug!("Created psbt: {}", new_psbt); @@ -317,7 +315,8 @@ fn faucet_send( 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())?; + let (core_tx, fee_rate) = + spend_from_core(keypair.x_only_public_key().0, shared_daemon.clone())?; // check that the first output of the transaction pays to the key we just created assert!( @@ -352,10 +351,7 @@ fn faucet_send( .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_sp_address = sp_wallet.get_client()?.get_receiving_address(); let change_output_key: XOnlyPublicKey = generate_recipient_pubkeys(vec![change_sp_address], partial_secret)? .into_values() @@ -410,9 +406,7 @@ fn faucet_send( first_tx = Some(core_tx); - let client = sp_client - .lock() - .map_err(|e| Error::msg(format!("{}", e.to_string())))?; + let client = sp_wallet.get_client()?; let input_pubkey = &keypair.public_key(); @@ -431,9 +425,9 @@ fn faucet_send( new_outpoints = find_owned_outputs(&final_tx, ours)?; } - if let Ok(core) = daemon.lock() { + if let Ok(daemon) = shared_daemon.lock() { // get current blockheight - let blkheight: u32 = core.get_current_height()?.try_into()?; + let blkheight: u32 = daemon.get_current_height()?.try_into()?; // update the new outpoints for o in new_outpoints.iter_mut() { o.1.blockheight = blkheight; @@ -441,34 +435,32 @@ fn faucet_send( // broadcast one or two transactions if first_tx.is_some() { - core.broadcast(&first_tx.unwrap())?; + daemon.broadcast(&first_tx.unwrap())?; } - core.broadcast(&final_tx)?; + daemon.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())))?; + let mut outputs = sp_wallet.get_outputs()?; outputs.extend_from(new_outpoints); - log::debug!("{:?}", outputs.to_outpoints_list()); + // save to disk + sp_wallet.get_storage()?.save(outputs.deref())?; Ok(final_tx.txid()) } fn handle_faucet_request( msg: &str, - sp_client: Arc>, - sp_outputs: Arc>, - daemon: Arc>, + sp_wallet: Arc, + shared_daemon: SharedDaemon, ) -> 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) + faucet_send(sp_address, sp_wallet, shared_daemon) } else { Err(Error::msg(format!( "faucet message with unparsable sp_address" @@ -477,12 +469,11 @@ fn handle_faucet_request( } async fn handle_connection( - peer_map: PeerMap, + peers: PeerMap, + shared_daemon: SharedDaemon, + sp_wallet: Arc, raw_stream: TcpStream, addr: SocketAddr, - sp_client: Arc>, - sp_outputs: Arc>, - daemon: Arc>, ) { debug!("Incoming TCP connection from: {}", addr); @@ -493,24 +484,29 @@ async fn handle_connection( // Insert the write part of this peer to the peer map. let (tx, rx) = unbounded_channel(); - peer_map.lock().unwrap().insert(addr, tx); + match peers.lock_anyhow() { + Ok(mut peer_map) => peer_map.insert(addr, tx), + Err(e) => { + log::error!("{}", e); + panic!(); + } + }; let (outgoing, incoming) = ws_stream.split(); let broadcast_incoming = incoming.try_for_each({ - let peer_map = peer_map.clone(); + let peers = peers.clone(); move |msg| { 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(), + sp_wallet.clone(), + shared_daemon.clone(), ) { Ok(txid) => { if let Err(e) = broadcast_message( - peer_map.clone(), + peers.clone(), Message::Text(format!("faucet{}", txid.to_string())), BroadcastType::Sender(addr), ) { @@ -521,7 +517,7 @@ async fn handle_connection( } Err(e) => { if let Err(e) = broadcast_message( - peer_map.clone(), + peers.clone(), Message::Text(e.to_string()), BroadcastType::Sender(addr), ) { @@ -532,7 +528,7 @@ async fn handle_connection( } else { // we just send it `as is` to everyone except sender if let Err(e) = - broadcast_message(peer_map.clone(), msg, BroadcastType::ExcludeSender(addr)) + broadcast_message(peers.clone(), msg, BroadcastType::ExcludeSender(addr)) { log::error!("Failed to broadcast message: {}", e); } @@ -558,7 +554,7 @@ async fn handle_connection( future::select(broadcast_incoming, receive_from_others).await; debug!("{} disconnected", &addr); - peer_map.lock().unwrap().remove(&addr); + peers.lock().unwrap().remove(&addr); } fn flatten_msg(parts: &[Vec]) -> Vec { @@ -628,7 +624,11 @@ fn process_raw_tx_message( } } -async fn handle_zmq(peer_map: PeerMap, daemon: Arc>) { +async fn handle_zmq( + peers: PeerMap, + shared_daemon: SharedDaemon, + sp_wallet: Arc, +) { tokio::task::spawn_blocking(move || { debug!("Starting listening on Core"); for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { @@ -643,22 +643,50 @@ async fn handle_zmq(peer_map: PeerMap, daemon: Arc>) { let payload: Vec = match core_msg.topic_str() { "rawtx" => { - let processed = process_raw_tx_message(&core_msg, daemon.clone()); + let processed = process_raw_tx_message(&core_msg, shared_daemon.clone()); match processed { Ok(p) => p, Err(_) => continue, } - } + }, + "rawblock" => { + // scan the block for our outputs + match scan_blocks(shared_daemon.clone(), sp_wallet.clone(), 1) { + Ok(()) => { + let updated = match sp_wallet.get_outputs() { + Ok(sp_outputs) => sp_outputs, + Err(e) => { + log::error!("{}", e); + continue; + } + }; + match sp_wallet.get_storage() { + Ok(storage) => { + if let Err(e) = storage.save(updated.deref()) { + log::error!("{}", e); + } + } + Err(e) => { + log::error!("{}", e); + } + } + } + Err(e) => log::error!("{}", e), + }; + + flatten_msg(&core_msg.serialize_to_vecs()) + }, _ => flatten_msg(&core_msg.serialize_to_vecs()), }; if let Err(e) = broadcast_message( - peer_map.clone(), + peers.clone(), Message::Binary(payload), BroadcastType::ToAll, ) { log::error!("{}", e.to_string()); } + } }); } @@ -678,16 +706,15 @@ async fn main() -> Result<()> { .expect("Please provide either \"true\" or \"false\""); let core_wallet: Option = env::args().nth(4); - let state = PeerMap::new(Mutex::new(HashMap::new())); + let peers = PeerMap::new(Mutex::new(HashMap::new())); // Connect the rpc daemon - let daemon = Daemon::connect(core_wallet).unwrap(); + let shared_daemon = Arc::new(Mutex::new(Daemon::connect(core_wallet)?)); - 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 current_tip: u32 = shared_daemon + .lock_anyhow()? + .get_current_height()? + .try_into()?; let mut config_dir = PathBuf::from_str(&env::var("HOME")?)?; config_dir.push(".4nk"); @@ -753,30 +780,31 @@ async fn main() -> Result<()> { let last_scan = sp_outputs.get_last_scan(); - 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)); + let shared_sp_client = Mutex::new(sp_client); + let shared_sp_outputs = Mutex::new(sp_outputs); + let shared_outputs_storage = Mutex::new(sp_outputs_file); + + let sp_wallet = Arc::new(SilentPaymentWallet { + sp_client: shared_sp_client, + sp_outputs: shared_sp_outputs, + storage: shared_outputs_storage, + }); if last_scan < current_tip { log::info!("Scanning for our outputs"); - match scan_blocks( - shared_sp_client.clone(), + scan_blocks( shared_daemon.clone(), - shared_sp_outputs.clone(), + sp_wallet.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())); + tokio::spawn(handle_zmq( + peers.clone(), + shared_daemon.clone(), + sp_wallet.clone(), + )); // Create the event loop and TCP listener we'll accept connections on. let try_socket = TcpListener::bind(&addr).await; @@ -786,12 +814,11 @@ async fn main() -> Result<()> { // 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(), + peers.clone(), + shared_daemon.clone(), + sp_wallet.clone(), stream, addr, - shared_sp_client.clone(), - shared_sp_outputs.clone(), - shared_daemon.clone(), )); } diff --git a/src/scan.rs b/src/scan.rs index 1b9b7d2..7e25403 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; +use std::ops::Deref; use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use anyhow::{Error, Result}; use electrum_client::ElectrumApi; @@ -9,11 +10,12 @@ 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::db::Storage; use sp_backend::silentpayments::receiving::Receiver; -use sp_backend::spclient::{OutputList, OutputSpendStatus, OwnedOutput, SpClient}; +use sp_backend::spclient::{OutputSpendStatus, OwnedOutput}; use tokio::time::Instant; -use crate::{electrumclient, Daemon}; +use crate::{electrumclient, MutexExt, SharedDaemon, SilentPaymentWallet}; fn get_script_to_secret_map( sp_receiver: &Receiver, @@ -172,23 +174,17 @@ fn scan_block_inputs( } pub fn scan_blocks( - sp_client: Arc>, - daemon: Arc>, - sp_outputs: Arc>, + shared_daemon: SharedDaemon, + sp_wallet: 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 core = shared_daemon.lock_anyhow()?; 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 scan_height = sp_wallet.get_outputs()?.get_last_scan(); let tip_height: u32 = core.get_current_height()?.try_into()?; // 0 means scan to tip @@ -215,16 +211,9 @@ pub fn scan_blocks( 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 scan_sk = sp_wallet.get_client()?.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 sp_receiver = sp_wallet.get_client()?.sp_receiver.clone(); let start_time = Instant::now(); for (blkheight, blkhash, blkfilter) in filters { @@ -239,10 +228,8 @@ pub fn scan_blocks( 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 our_outputs: HashMap = + sp_wallet.get_outputs()?.to_outpoints_list(); let owned_spks: Result>> = our_outputs .iter() @@ -261,25 +248,18 @@ pub fn scan_blocks( 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); + sp_wallet.get_outputs()?.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(); + let updated_outputs: HashMap = + sp_wallet.get_outputs()?.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)))?; + let mut outputs = sp_wallet.get_outputs()?; for outpoint in utxo_destroyed_in_block { outputs.mark_mined(outpoint, blkhash)?; } @@ -294,9 +274,11 @@ pub fn scan_blocks( ); // update last_scan height - sp_outputs - .lock() - .map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))? - .update_last_scan(end); + let mut updated = sp_wallet.get_outputs()?; + + updated.update_last_scan(end); + + sp_wallet.get_storage()?.save(updated.deref())?; + Ok(()) } From 7dba477f33e9eb72c2b54056872605551ad436bc Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Mon, 8 Apr 2024 18:15:26 +0200 Subject: [PATCH 12/27] Refactoring --- Cargo.lock | 84 +++++++++++++-------------- src/main.rs | 164 +++++++++++++++++++++------------------------------- src/scan.rs | 30 +++++----- 3 files changed, 125 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 193a55b..437e338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,15 +60,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" +checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" dependencies = [ "base64 0.21.7", "bech32 0.10.0-beta", @@ -194,7 +194,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662" dependencies = [ - "bitcoin 0.31.1", + "bitcoin 0.31.2", "serde", "serde_json", ] @@ -205,7 +205,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff1e21acea23b5ec33f912350f18adee9a08bd513dca9f66f0e2cfe9d756ef46" dependencies = [ - "bitcoin 0.31.1", + "bitcoin 0.31.2", "zmq", "zmq-sys", ] @@ -239,15 +239,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "1fd97381a8cc6493395a5afc4c691c1084b3768db713b73aa215217aa245d153" dependencies = [ "jobserver", "libc", @@ -271,9 +271,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -537,9 +537,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -689,9 +689,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" @@ -746,9 +746,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "miniz_oxide" @@ -818,9 +818,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -896,9 +896,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -916,9 +916,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -939,9 +939,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "ring" @@ -1095,9 +1095,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1123,7 +1123,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -1180,9 +1180,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -1197,10 +1197,10 @@ dependencies = [ [[package]] name = "sp_backend" version = "0.1.0" -source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#32967c214df9a25daef551a372b89c400f2369f8" +source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#d09c92ac7718c34c9ce3ff55298140ebda0b2e1a" dependencies = [ "anyhow", - "bitcoin 0.31.1", + "bitcoin 0.31.2", "serde", "serde_json", "silentpayments", @@ -1220,9 +1220,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.53" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -1325,9 +1325,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -1401,7 +1401,7 @@ version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", diff --git a/src/main.rs b/src/main.rs index 4730f30..b55db89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ use std::{ fmt::Debug, net::SocketAddr, ops::Deref, - path::PathBuf, str::FromStr, sync::{Arc, Mutex, MutexGuard}, }; @@ -12,11 +11,6 @@ use std::{ use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; -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, @@ -29,6 +23,14 @@ use sp_backend::bitcoin::{ XOnlyPublicKey, }; use sp_backend::spclient::OutputList; +use sp_backend::{ + bitcoin::secp256k1::{ + rand::{thread_rng, Rng}, + Error as Secp256k1Error, Keypair, Message as Secp256k1Message, PublicKey, Scalar, + Secp256k1, SecretKey, ThirtyTwoByteHash, + }, + spclient::SpWallet, +}; use sp_backend::db::{JsonFile, Storage}; use sp_backend::silentpayments::receiving::Label; @@ -73,23 +75,18 @@ impl MutexExt for Mutex { } #[derive(Debug)] -pub(crate) struct SilentPaymentWallet { - sp_client: Mutex, - sp_outputs: Mutex, +struct SilentPaymentWallet { + sp_wallet: Mutex, storage: Mutex, } impl SilentPaymentWallet { - pub fn get_client(&self) -> Result> { - self.sp_client.lock_anyhow() + pub fn get_wallet(&self) -> Result> { + self.sp_wallet.lock_anyhow() } - pub fn get_outputs(&self) -> Result> { - self.sp_outputs.lock_anyhow() - } - - pub fn get_storage(&self) -> Result> { - self.storage.lock_anyhow() + pub fn save(&self) -> Result<()> { + self.storage.lock_anyhow()?.save(&self.sp_wallet) } } @@ -227,7 +224,7 @@ fn faucet_send( let mut new_outpoints: HashMap; // do we have a sp output available ? - let available_outpoints = sp_wallet.get_outputs()?.to_spendable_list(); + let available_outpoints = sp_wallet.get_wallet()?.get_outputs().to_spendable_list(); let available_amt = available_outpoints .iter() @@ -260,16 +257,19 @@ fn faucet_send( log::debug!("fee estimate for 6 blocks: {}", fee_estimate); - let wallet = sp_wallet.get_client()?; + let wallet = sp_wallet.get_wallet()?; - let mut new_psbt = wallet.create_new_psbt(inputs.clone(), vec![recipient], None)?; + let mut new_psbt = + wallet + .get_client() + .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)?; + wallet.get_client().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)?; + let mut signed = wallet.get_client().sign_psbt(new_psbt, &aux_rand)?; log::debug!("signed psbt: {}", signed); SpClient::finalize_psbt(&mut signed)?; @@ -282,8 +282,11 @@ fn faucet_send( .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_sp_address: SilentPaymentAddress = wallet + .get_client() + .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 @@ -296,7 +299,8 @@ fn faucet_send( 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 ecdh_shared_secret = + calculate_shared_secret(partial_tweak, wallet.get_client().get_scan_key())?; let outputs_to_check: Result, Secp256k1Error> = final_tx .output @@ -305,6 +309,7 @@ fn faucet_send( .collect(); let ours = wallet + .get_client() .sp_receiver .scan_transaction(&ecdh_shared_secret, outputs_to_check?)?; @@ -351,7 +356,7 @@ fn faucet_send( .get(0) .expect("Failed to generate keys") .to_owned(); - let change_sp_address = sp_wallet.get_client()?.get_receiving_address(); + let change_sp_address = sp_wallet.get_wallet()?.get_client().get_receiving_address(); let change_output_key: XOnlyPublicKey = generate_recipient_pubkeys(vec![change_sp_address], partial_secret)? .into_values() @@ -406,17 +411,19 @@ fn faucet_send( first_tx = Some(core_tx); - let client = sp_wallet.get_client()?; + let client = sp_wallet.get_wallet()?; 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 ecdh_shared_secret = + calculate_shared_secret(partial_tweak, client.get_client().get_scan_key())?; let p2tr_outs = vec![ext_output_key, change_output_key]; let ours = client + .get_client() .sp_receiver .scan_transaction(&ecdh_shared_secret, p2tr_outs)?; @@ -443,12 +450,13 @@ fn faucet_send( } // update our sp_client with the change output(s) - let mut outputs = sp_wallet.get_outputs()?; - - outputs.extend_from(new_outpoints); + sp_wallet + .get_wallet()? + .get_mut_outputs() + .extend_from(new_outpoints); // save to disk - sp_wallet.get_storage()?.save(outputs.deref())?; + sp_wallet.save()?; Ok(final_tx.txid()) } @@ -648,34 +656,15 @@ async fn handle_zmq( Ok(p) => p, Err(_) => continue, } - }, + } "rawblock" => { // scan the block for our outputs - match scan_blocks(shared_daemon.clone(), sp_wallet.clone(), 1) { - Ok(()) => { - let updated = match sp_wallet.get_outputs() { - Ok(sp_outputs) => sp_outputs, - Err(e) => { - log::error!("{}", e); - continue; - } - }; - match sp_wallet.get_storage() { - Ok(storage) => { - if let Err(e) = storage.save(updated.deref()) { - log::error!("{}", e); - } - } - Err(e) => { - log::error!("{}", e); - } - } - } - Err(e) => log::error!("{}", e), - }; + if let Err(e) = scan_blocks(shared_daemon.clone(), sp_wallet.clone(), 1) { + log::error!("{}", e); + } flatten_msg(&core_msg.serialize_to_vecs()) - }, + } _ => flatten_msg(&core_msg.serialize_to_vecs()), }; @@ -686,7 +675,6 @@ async fn handle_zmq( ) { log::error!("{}", e.to_string()); } - } }); } @@ -716,27 +704,12 @@ async fn main() -> Result<()> { .get_current_height()? .try_into()?; - 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))?; + let mut config_dir = env::var("HOME")?; + config_dir.push_str("/.4nk"); + let sp_wallet_file = JsonFile::new(&config_dir, &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) - } - } + let sp_wallet = match >::load(&sp_wallet_file) { Err(_) => { let mut seed = [0u8; 64]; thread_rng().fill(&mut seed); @@ -751,45 +724,42 @@ async fn main() -> Result<()> { ) .expect("Failed to create a new SpClient"); - let new_address = SilentPaymentAddress::try_from(new_client.get_receiving_address())?; + let mut wallet = SpWallet::new(new_client, None)?; - let new_outputs = OutputList::new( - new_address.get_scan_key(), - new_address.get_spend_key(), - current_tip, - ); + // set birthday to avoid unnecessary scanning + let outputs = wallet.get_mut_outputs(); + outputs.set_birthday(current_tip); + outputs.update_last_scan(current_tip); - sp_client_file.save(&new_client)?; - sp_outputs_file.save(&new_outputs)?; - - (new_client, new_outputs) + wallet } + Ok(wallet) => wallet, }; log::info!( "Using wallet {} with address {}", - sp_client.label, - sp_client.get_receiving_address() + sp_wallet.get_client().label, + sp_wallet.get_client().get_receiving_address() ); log::info!( "Found {} outputs for a total balance of {}", - sp_outputs.to_spendable_list().len(), - sp_outputs.get_balance() + sp_wallet.get_outputs().to_spendable_list().len(), + sp_wallet.get_outputs().get_balance() ); - let last_scan = sp_outputs.get_last_scan(); + let last_scan = sp_wallet.get_outputs().get_last_scan(); - let shared_sp_client = Mutex::new(sp_client); - let shared_sp_outputs = Mutex::new(sp_outputs); - let shared_outputs_storage = Mutex::new(sp_outputs_file); + let shared_sp_wallet = Mutex::new(sp_wallet); + let shared_wallet_storage = Mutex::new(sp_wallet_file); let sp_wallet = Arc::new(SilentPaymentWallet { - sp_client: shared_sp_client, - sp_outputs: shared_sp_outputs, - storage: shared_outputs_storage, + sp_wallet: shared_sp_wallet, + storage: shared_wallet_storage, }); + sp_wallet.save()?; + if last_scan < current_tip { log::info!("Scanning for our outputs"); scan_blocks( diff --git a/src/scan.rs b/src/scan.rs index 7e25403..fecb05b 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -10,7 +9,6 @@ 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::db::Storage; use sp_backend::silentpayments::receiving::Receiver; use sp_backend::spclient::{OutputSpendStatus, OwnedOutput}; use tokio::time::Instant; @@ -184,7 +182,7 @@ pub fn scan_blocks( let core = shared_daemon.lock_anyhow()?; let secp = Secp256k1::new(); - let scan_height = sp_wallet.get_outputs()?.get_last_scan(); + let scan_height = sp_wallet.get_wallet()?.get_outputs().get_last_scan(); let tip_height: u32 = core.get_current_height()?.try_into()?; // 0 means scan to tip @@ -211,9 +209,9 @@ pub fn scan_blocks( let mut tweak_data_map = electrum_client.sp_tweaks(start as usize)?; - let scan_sk = sp_wallet.get_client()?.get_scan_key(); + let scan_sk = sp_wallet.get_wallet()?.get_client().get_scan_key(); - let sp_receiver = sp_wallet.get_client()?.sp_receiver.clone(); + let sp_receiver = sp_wallet.get_wallet()?.get_client().sp_receiver.clone(); let start_time = Instant::now(); for (blkheight, blkhash, blkfilter) in filters { @@ -229,7 +227,7 @@ pub fn scan_blocks( // check if owned inputs are spent let our_outputs: HashMap = - sp_wallet.get_outputs()?.to_outpoints_list(); + sp_wallet.get_wallet()?.get_outputs().to_outpoints_list(); let owned_spks: Result>> = our_outputs .iter() @@ -248,18 +246,22 @@ pub fn scan_blocks( let utxo_created_in_block = scan_block_outputs(&sp_receiver, &blk.txdata, blkheight.into(), spk2secret)?; if !utxo_created_in_block.is_empty() { - sp_wallet.get_outputs()?.extend_from(utxo_created_in_block); + sp_wallet + .get_wallet()? + .get_mut_outputs() + .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_wallet.get_outputs()?.to_outpoints_list(); + sp_wallet.get_wallet()?.get_outputs().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_wallet.get_outputs()?; + let mut wallet = sp_wallet.get_wallet()?; + let outputs = wallet.get_mut_outputs(); for outpoint in utxo_destroyed_in_block { outputs.mark_mined(outpoint, blkhash)?; } @@ -274,11 +276,11 @@ pub fn scan_blocks( ); // update last_scan height - let mut updated = sp_wallet.get_outputs()?; - - updated.update_last_scan(end); - - sp_wallet.get_storage()?.save(updated.deref())?; + sp_wallet + .get_wallet()? + .get_mut_outputs() + .update_last_scan(end); + sp_wallet.save()?; Ok(()) } From c1d1c0a4b52906220ec32e7c1c7d5f1319415428 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Tue, 9 Apr 2024 14:18:28 +0200 Subject: [PATCH 13/27] Add sdk_common dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index d43db65..5dbb70d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ env_logger = "0.9" futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] } hex = "0.4.3" log = "0.4.20" +sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo" } serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" serde_with = "3.6.0" From 459756815f9060691fef980270e89854929bf2ed Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:18:33 +0200 Subject: [PATCH 14/27] abort if core doesn't have enough funds --- src/daemon.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/daemon.rs b/src/daemon.rs index 6720a0a..0e7d7a5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -246,6 +246,10 @@ impl Daemon { .iter() .fold(Amount::from_sat(0), |acc, x| acc + x.amount); + if total_amt < FAUCET_AMT { + return Err(Error::msg("Not enought funds")); + } + let mut outputs = HashMap::new(); outputs.insert(address.to_string(), total_amt); From 4d3dc8123a5048362452ef96207b5e942f428319 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:24:12 +0200 Subject: [PATCH 15/27] Simplify zmq & one track for all messaging --- Cargo.lock | 653 ++++++++++++++++++++++++++++++---------------------- Cargo.toml | 2 +- src/main.rs | 244 ++++++++++---------- 3 files changed, 505 insertions(+), 394 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 437e338..d71f2f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -43,9 +78,33 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] [[package]] name = "atty" @@ -199,17 +258,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "bitcoincore-zmq" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1e21acea23b5ec33f912350f18adee9a08bd513dca9f66f0e2cfe9d756ef46" -dependencies = [ - "bitcoin 0.31.2", - "zmq", - "zmq-sys", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -227,9 +275,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -245,23 +293,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.91" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd97381a8cc6493395a5afc4c691c1084b3768db713b73aa215217aa245d153" -dependencies = [ - "jobserver", - "libc", -] - -[[package]] -name = "cfg-expr" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" -dependencies = [ - "smallvec", - "target-lexicon", -] +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" [[package]] name = "cfg-if" @@ -271,15 +305,25 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.4", + "windows-targets 0.52.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", ] [[package]] @@ -297,47 +341,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -360,9 +363,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.8" @@ -398,6 +411,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -424,23 +450,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dircpy" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29259db751c34980bfc44100875890c507f585323453b91936960ab1104272ca" -dependencies = [ - "jwalk", - "log", - "walkdir", -] - -[[package]] -name = "either" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" - [[package]] name = "electrum-client" version = "0.18.0" @@ -493,12 +502,39 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -518,8 +554,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", + "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -546,12 +585,35 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -564,12 +626,6 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -687,21 +743,21 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "jobserver" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.69" @@ -722,22 +778,22 @@ dependencies = [ "serde_json", ] -[[package]] -name = "jwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" -dependencies = [ - "crossbeam", - "rayon", -] - [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" @@ -810,6 +866,35 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -829,10 +914,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.30" +name = "polyval" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] [[package]] name = "powerfmt" @@ -848,18 +939,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -895,23 +986,12 @@ dependencies = [ ] [[package]] -name = "rayon" -version = "1.10.0" +name = "redox_syscall" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "bitflags", ] [[package]] @@ -993,13 +1073,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] -name = "same-file" -version = "1.0.6" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -1011,18 +1088,33 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdk_common" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "rand", + "serde", + "serde_json", + "sp_backend", + "tsify", + "uuid", + "wasm-bindgen", +] + [[package]] name = "sdk_relay" version = "0.1.0" dependencies = [ "anyhow", "bitcoincore-rpc", - "bitcoincore-zmq", "electrum-client", "env_logger", "futures-util", "hex", "log", + "sdk_common", "serde", "serde_json", "serde_with", @@ -1030,6 +1122,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "zeromq", ] [[package]] @@ -1094,22 +1187,24 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.115" +name = "serde_derive_internals" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ - "itoa", - "ryu", - "serde", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "serde_spanned" -version = "0.6.5" +name = "serde_json" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "itoa", + "ryu", "serde", ] @@ -1154,6 +1249,15 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "silentpayments" version = "0.1.1" @@ -1219,35 +1323,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "syn" -version = "2.0.58" +name = "subtle" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" - [[package]] name = "termcolor" version = "1.4.1" @@ -1279,9 +1370,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -1300,9 +1391,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -1334,7 +1425,9 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -1375,37 +1468,40 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.12" +name = "tokio-util" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] -name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +name = "tsify" +version = "0.4.5" +source = "git+https://github.com/Sosthene00/tsify?branch=next#8a5a550d2ab41612cef88a3a3de2a94639b0d3fc" dependencies = [ + "gloo-utils", "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", ] [[package]] -name = "toml_edit" -version = "0.22.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +name = "tsify-macros" +version = "0.4.5" +source = "git+https://github.com/Sosthene00/tsify?branch=next#8a5a550d2ab41612cef88a3a3de2a94639b0d3fc" dependencies = [ - "indexmap 2.2.6", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", ] [[package]] @@ -1454,6 +1550,16 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -1478,10 +1584,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "version-compare" -version = "0.2.0" +name = "uuid" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "serde", +] [[package]] name = "version_check" @@ -1489,16 +1599,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1559,6 +1659,16 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki" version = "0.22.4" @@ -1615,7 +1725,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1633,7 +1743,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1653,17 +1763,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -1674,9 +1785,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -1686,9 +1797,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -1698,9 +1809,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -1710,9 +1827,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -1722,9 +1839,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -1734,9 +1851,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -1746,47 +1863,33 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] -name = "winnow" -version = "0.6.5" +name = "zeromq" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "9ad3ffd65d6ae06a9eece312a64c3dfa2151a70a5c99051e2080828653cbda45" dependencies = [ - "memchr", -] - -[[package]] -name = "zeromq-src" -version = "0.2.6+4.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc120b771270365d5ed0dfb4baf1005f2243ae1ae83703265cb3504070f4160b" -dependencies = [ - "cc", - "dircpy", -] - -[[package]] -name = "zmq" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd3091dd571fb84a9b3e5e5c6a807d186c411c812c8618786c3c30e5349234e7" -dependencies = [ - "bitflags", - "libc", - "zmq-sys", -] - -[[package]] -name = "zmq-sys" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8351dc72494b4d7f5652a681c33634063bbad58046c1689e75270908fdc864" -dependencies = [ - "libc", - "system-deps", - "zeromq-src", + "async-trait", + "asynchronous-codec", + "bytes", + "crossbeam-queue", + "dashmap", + "futures-channel", + "futures-io", + "futures-task", + "futures-util", + "log", + "num-traits", + "once_cell", + "parking_lot", + "rand", + "regex", + "thiserror", + "tokio", + "tokio-util", + "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 5dbb70d..b81c47f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [dependencies] anyhow = "1.0" 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"] } @@ -20,3 +19,4 @@ sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "sp_cl tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", "sync"] } tokio-stream = "0.1" tokio-tungstenite = "0.21.0" +zeromq = "0.3.5" diff --git a/src/main.rs b/src/main.rs index b55db89..2b6b464 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,22 @@ use std::{ - collections::HashMap, - env, - fmt::Debug, - net::SocketAddr, - ops::Deref, - str::FromStr, - sync::{Arc, Mutex, MutexGuard}, + any::Any, collections::HashMap, env, fmt::Debug, net::SocketAddr, str::FromStr, sync::{Arc, Mutex, MutexGuard} }; use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; +use sdk_common::network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}; use sp_backend::bitcoin::{ absolute::LockTime, - consensus::deserialize, + consensus::{deserialize, serialize}, hex::DisplayHex, key::TapTweak, sighash::{Prevouts, SighashCache}, taproot::Signature, transaction::Version, - Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, + Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey, }; -use sp_backend::spclient::OutputList; use sp_backend::{ bitcoin::secp256k1::{ rand::{thread_rng, Rng}, @@ -48,6 +42,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; use anyhow::{Error, Result}; +use zeromq::{Socket, SocketRecv}; mod daemon; mod electrumclient; @@ -97,8 +92,18 @@ enum BroadcastType { ToAll, } -fn broadcast_message(peers: PeerMap, msg: Message, broadcast: BroadcastType) -> Result<()> { +fn broadcast_message( + peers: PeerMap, + flag: AnkFlag, + payload: String, + broadcast: BroadcastType, +) -> Result<()> { // log::debug!("Broadcasting message: {}", msg); + let ank_msg = AnkNetworkMsg { + flag, + content: payload, + }; + let msg = Message::Text(serde_json::to_string(&ank_msg)?); match broadcast { BroadcastType::Sender(addr) => { peers @@ -505,41 +510,55 @@ async fn handle_connection( let broadcast_incoming = incoming.try_for_each({ let peers = peers.clone(); move |msg| { - if msg.is_text() { - if msg.to_string().starts_with("faucet") { - match handle_faucet_request( - &msg.to_string(), - sp_wallet.clone(), - shared_daemon.clone(), - ) { - Ok(txid) => { - if let Err(e) = broadcast_message( - peers.clone(), - Message::Text(format!("faucet{}", txid.to_string())), - BroadcastType::Sender(addr), - ) { - log::error!("Failed to broadcast message: {}", e.to_string()); + if let Ok(raw_msg) = msg.to_text() { + debug!("Received msg: {}", raw_msg); + let parsed = serde_json::from_str::(raw_msg); + match parsed { + Ok(ank_msg) => match ank_msg.flag { + AnkFlag::Faucet => { + debug!("Received a faucet message"); + if let Ok(content) = + serde_json::from_str::(&ank_msg.content) + { + match handle_faucet_request( + &content.sp_address, + sp_wallet.clone(), + shared_daemon.clone(), + ) { + Ok(new_tx_msg) => { + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::NewTx, + serde_json::to_string(&new_tx_msg) + .expect("This should not fail"), + BroadcastType::Sender(addr), + ) { + log::error!( + "Failed to broadcast message: {}", + e.to_string() + ); + } + } + Err(e) => { + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::Error, + e.to_string(), + BroadcastType::Sender(addr), + ) { + log::error!("Failed to broadcast message: {}", e); + } + } + } } else { - log::debug!("Successfully broadcasted message: {}", txid); + log::error!("Invalid content for faucet message"); } } - Err(e) => { - if let Err(e) = broadcast_message( - peers.clone(), - Message::Text(e.to_string()), - BroadcastType::Sender(addr), - ) { - log::error!("Failed to broadcast message: {}", e); - } - } - } - } else { - // we just send it `as is` to everyone except sender - if let Err(e) = - broadcast_message(peers.clone(), msg, BroadcastType::ExcludeSender(addr)) - { - log::error!("Failed to broadcast message: {}", e); - } + AnkFlag::NewTx => unimplemented!(), + AnkFlag::Error => unimplemented!(), + AnkFlag::Unknown => unimplemented!(), + }, + Err(_) => log::error!("Failed to parse network message"), } } else { // we don't care @@ -565,26 +584,10 @@ async fn handle_connection( peers.lock().unwrap().remove(&addr); } -fn flatten_msg(parts: &[Vec]) -> Vec { - let total_len = parts.iter().fold(0, |acc, v| acc + v.len()); - let mut final_vec = Vec::with_capacity(total_len); - for p in parts { - final_vec.extend(p); - } - - final_vec -} - -fn process_raw_tx_message( - core_msg: &bitcoincore_zmq::Message, +fn compute_partial_tweak_to_transaction( + tx: Transaction, 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")); - } - +) -> Result { let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len()); let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); for input in tx.input { @@ -619,64 +622,73 @@ fn process_raw_tx_message( } let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); - 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() - ))), - } + let partial_tweak = calculate_tweak_data(&input_pub_keys, &outpoints)?; + Ok(partial_tweak) } -async fn handle_zmq( - peers: PeerMap, - shared_daemon: SharedDaemon, - sp_wallet: Arc, -) { - tokio::task::spawn_blocking(move || { - debug!("Starting listening on Core"); - for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { - let core_msg = match msg { - Ok(core_msg) => core_msg, - Err(e) => { - error!("Error receiving ZMQ message: {}", e); - continue; - } - }; - debug!("Received a {} message", core_msg.topic_str()); +fn create_new_tx_message(transaction: Vec, daemon: Arc>) -> Result { + let tx: Transaction = deserialize(&transaction)?; - let payload: Vec = match core_msg.topic_str() { - "rawtx" => { - let processed = process_raw_tx_message(&core_msg, shared_daemon.clone()); - match processed { - Ok(p) => p, - Err(_) => continue, - } - } - "rawblock" => { - // scan the block for our outputs - if let Err(e) = scan_blocks(shared_daemon.clone(), sp_wallet.clone(), 1) { - log::error!("{}", e); - } + if tx.is_coinbase() { + return Err(Error::msg("Can't process coinbase transaction")); + } - flatten_msg(&core_msg.serialize_to_vecs()) - } - _ => flatten_msg(&core_msg.serialize_to_vecs()), - }; + let partial_tweak = compute_partial_tweak_to_transaction(tx, daemon)?; + Ok(NewTxMessage::new( + transaction.to_lower_hex_string(), + Some(partial_tweak.to_string()), + )) +} - if let Err(e) = broadcast_message( - peers.clone(), - Message::Binary(payload), - BroadcastType::ToAll, - ) { - log::error!("{}", e.to_string()); +async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) { + debug!("Starting listening on Core"); + let mut socket = zeromq::SubSocket::new(); + socket.connect("tcp://127.0.0.1:29100").await.unwrap(); + socket.subscribe("rawtx").await.unwrap(); + // socket.subscribe("hashblock"); + debug!("{:?}", socket.type_id()); + loop { + let core_msg = match socket.recv().await { + Ok(m) => m, + Err(e) => { + error!("Zmq error: {}", e); + continue; } + }; + debug!("Received a message"); + + let payload: String = + if let (Some(topic), Some(data)) = (core_msg.get(0), core_msg.get(1)) { + match std::str::from_utf8(&topic) { + Ok("rawtx") => { + match create_new_tx_message(data.to_vec(), shared_daemon.clone()) { + Ok(m) => serde_json::to_string(&m).expect("This shouldn't fail"), + Err(e) => { + error!("{}", e); + continue; + } + } + } + Ok("hashblock") => todo!(), + _ => { + error!("Unexpected message in zmq"); + continue; + } + } + } else { + error!("Empty message"); + continue; + }; + + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::NewTx, + payload, + BroadcastType::ToAll, + ) { + log::error!("{}", e.to_string()); } - }); + } } #[tokio::main(flavor = "multi_thread")] @@ -770,11 +782,7 @@ async fn main() -> Result<()> { } // Subscribe to Bitcoin Core - tokio::spawn(handle_zmq( - peers.clone(), - shared_daemon.clone(), - sp_wallet.clone(), - )); + tokio::spawn(handle_zmq(peers.clone(), shared_daemon.clone())); // Create the event loop and TCP listener we'll accept connections on. let try_socket = TcpListener::bind(&addr).await; From 8bbee83aac28d9ad18c9a7a3c278e8b2c652a5e5 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:25:06 +0200 Subject: [PATCH 16/27] Return whole tx for faucet --- src/main.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2b6b464..39a5a72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -223,7 +223,7 @@ fn faucet_send( sp_address: SilentPaymentAddress, sp_wallet: Arc, shared_daemon: SharedDaemon, -) -> Result { +) -> Result { let mut first_tx: Option = None; let final_tx: Transaction; let mut new_outpoints: HashMap; @@ -460,20 +460,29 @@ fn faucet_send( .get_mut_outputs() .extend_from(new_outpoints); + debug!("{:?}", sp_wallet); + // save to disk sp_wallet.save()?; - Ok(final_tx.txid()) + Ok(final_tx) } fn handle_faucet_request( msg: &str, sp_wallet: Arc, shared_daemon: SharedDaemon, -) -> Result { - if let Ok(sp_address) = SilentPaymentAddress::try_from(&msg["faucet".len()..]) { +) -> Result { + if let Ok(sp_address) = SilentPaymentAddress::try_from(msg) { + debug!("Sending bootstrap coins to {}", sp_address); // send bootstrap coins to this sp_address - faucet_send(sp_address, sp_wallet, shared_daemon) + let tx = faucet_send(sp_address, sp_wallet, shared_daemon.clone())?; + let partial_tweak = + compute_partial_tweak_to_transaction(tx.clone(), shared_daemon.clone())?; + Ok(NewTxMessage::new( + serialize(&tx).to_lower_hex_string(), + Some(partial_tweak.to_string()), + )) } else { Err(Error::msg(format!( "faucet message with unparsable sp_address" From a287db7cf89887fbd53e05f6f124914525f0fc0a Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 08:25:52 +0200 Subject: [PATCH 17/27] [bug fix] set floor fees when no estimation from core --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 39a5a72..0996a45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -255,8 +255,9 @@ fn faucet_send( let fee_estimate = shared_daemon .lock_anyhow()? - .estimate_fee(6)? - .unwrap_or(Amount::from_sat(1000)) + .estimate_fee(6) + .unwrap_or(Some(Amount::from_sat(1000))) + .unwrap() .checked_div(1000) .unwrap(); From ee5fcb49328ebefc1a5b9c04970a550700e55618 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 17 Apr 2024 21:44:30 +0200 Subject: [PATCH 18/27] Update sp_backend to sp_client --- Cargo.toml | 2 +- src/daemon.rs | 8 ++++---- src/main.rs | 19 +++++++++---------- src/scan.rs | 12 ++++++------ 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b81c47f..7513f6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" serde_with = "3.6.0" -sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "sp_client" } +sp_client = { git = "https://github.com/Sosthene00/sp-client", 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/daemon.rs b/src/daemon.rs index 0e7d7a5..4e702e0 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -5,12 +5,12 @@ use bitcoincore_rpc::json::{ WalletCreateFundedPsbtOptions, }; use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; -use sp_backend::bitcoin::bip158::BlockFilter; -use sp_backend::bitcoin::{ +use sp_client::bitcoin::bip158::BlockFilter; +use sp_client::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 sp_client::bitcoin::{consensus::deserialize, hashes::hex::FromHex}; // use crossbeam_channel::Receiver; // use parking_lot::Mutex; use serde_json::{json, Value}; @@ -315,7 +315,7 @@ impl Daemon { txid: &Txid, blockhash: Option, ) -> Result { - use sp_backend::bitcoin::consensus::serde::{hex::Lower, Hex, With}; + use sp_client::bitcoin::consensus::serde::{hex::Lower, Hex, With}; let tx = self.get_transaction(txid, blockhash)?; #[derive(serde::Serialize)] diff --git a/src/main.rs b/src/main.rs index 0996a45..266cdb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; use sdk_common::network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}; -use sp_backend::bitcoin::{ +use sp_client::bitcoin::{ absolute::LockTime, consensus::{deserialize, serialize}, hex::DisplayHex, @@ -17,7 +17,7 @@ use sp_backend::bitcoin::{ Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey, }; -use sp_backend::{ +use sp_client::{ bitcoin::secp256k1::{ rand::{thread_rng, Rng}, Error as Secp256k1Error, Keypair, Message as Secp256k1Message, PublicKey, Scalar, @@ -26,15 +26,14 @@ use sp_backend::{ spclient::SpWallet, }; -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_client::db::{JsonFile, Storage}; +use sp_client::silentpayments::sending::{generate_recipient_pubkeys, SilentPaymentAddress}; +use sp_client::silentpayments::utils::receiving::{ + 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 sp_client::silentpayments::utils::sending::calculate_partial_secret; +use sp_client::spclient::{ + derive_keys_from_seed, Recipient, SpClient, SpendKey, }; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; diff --git a/src/scan.rs b/src/scan.rs index fecb05b..93f8f30 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -5,12 +5,12 @@ use std::sync::Arc; 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::{OutputSpendStatus, OwnedOutput}; +use sp_client::bitcoin::bip158::BlockFilter; +use sp_client::bitcoin::hex::DisplayHex; +use sp_client::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; +use sp_client::bitcoin::{BlockHash, OutPoint, Transaction, TxOut, XOnlyPublicKey}; +use sp_client::silentpayments::receiving::Receiver; +use sp_client::spclient::{OutputSpendStatus, OwnedOutput}; use tokio::time::Instant; use crate::{electrumclient, MutexExt, SharedDaemon, SilentPaymentWallet}; From 94b96320d7de883349e44c811a8bd7612d5ccb1c Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Thu, 18 Apr 2024 00:34:10 +0200 Subject: [PATCH 19/27] spend refactoring --- Cargo.lock | 36 +++++--- src/main.rs | 231 +++++++++++++++------------------------------------- 2 files changed, 88 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d71f2f5..d5b92e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -939,9 +939,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -1097,7 +1097,7 @@ dependencies = [ "rand", "serde", "serde_json", - "sp_backend", + "sp_client 0.1.0 (git+https://github.com/Sosthene00/sp-backend?branch=sp_client)", "tsify", "uuid", "wasm-bindgen", @@ -1118,7 +1118,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "sp_backend", + "sp_client 0.1.0 (git+https://github.com/Sosthene00/sp-client?branch=sp_client)", "tokio", "tokio-stream", "tokio-tungstenite", @@ -1168,18 +1168,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -1299,9 +1299,21 @@ dependencies = [ ] [[package]] -name = "sp_backend" +name = "sp_client" version = "0.1.0" -source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#d09c92ac7718c34c9ce3ff55298140ebda0b2e1a" +source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#4eaa51ed306fc939f487c0e14208bf36da8c8a26" +dependencies = [ + "anyhow", + "bitcoin 0.31.2", + "serde", + "serde_json", + "silentpayments", +] + +[[package]] +name = "sp_client" +version = "0.1.0" +source = "git+https://github.com/Sosthene00/sp-client?branch=sp_client#4eaa51ed306fc939f487c0e14208bf36da8c8a26" dependencies = [ "anyhow", "bitcoin 0.31.2", @@ -1330,9 +1342,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", diff --git a/src/main.rs b/src/main.rs index 266cdb2..e115aaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,11 @@ use std::{ - any::Any, collections::HashMap, env, fmt::Debug, net::SocketAddr, str::FromStr, sync::{Arc, Mutex, MutexGuard} + any::Any, + collections::HashMap, + env, + fmt::Debug, + net::SocketAddr, + str::FromStr, + sync::{Arc, Mutex, MutexGuard}, }; use bitcoincore_rpc::json::{self as bitcoin_json}; @@ -20,8 +26,8 @@ use sp_client::bitcoin::{ use sp_client::{ bitcoin::secp256k1::{ rand::{thread_rng, Rng}, - Error as Secp256k1Error, Keypair, Message as Secp256k1Message, PublicKey, Scalar, - Secp256k1, SecretKey, ThirtyTwoByteHash, + Keypair, Message as Secp256k1Message, PublicKey, + Secp256k1, ThirtyTwoByteHash, }, spclient::SpWallet, }; @@ -86,6 +92,7 @@ impl SilentPaymentWallet { enum BroadcastType { Sender(SocketAddr), + #[allow(dead_code)] ExcludeSender(SocketAddr), #[allow(dead_code)] ToAll, @@ -169,55 +176,6 @@ fn spend_from_core( } } -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; - } - } - } - None - } - Err(_) => None, - }, - )); - } - Ok(res) -} - fn faucet_send( sp_address: SilentPaymentAddress, sp_wallet: Arc, @@ -225,7 +183,6 @@ fn faucet_send( ) -> Result { let mut first_tx: Option = None; let final_tx: Transaction; - let mut new_outpoints: HashMap; // do we have a sp output available ? let available_outpoints = sp_wallet.get_wallet()?.get_outputs().to_spendable_list(); @@ -270,7 +227,8 @@ fn faucet_send( .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.get_client().fill_sp_outputs(&mut new_psbt)?; + let partial_secret = wallet.get_client().get_partial_secret_from_psbt(&new_psbt)?; + wallet.get_client().fill_sp_outputs(&mut new_psbt, partial_secret)?; log::debug!("Definitive psbt: {}", new_psbt); let mut aux_rand = [0u8; 32]; thread_rng().fill(&mut aux_rand); @@ -279,46 +237,6 @@ fn faucet_send( 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 - .get_client() - .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_client().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 - .get_client() - .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(); @@ -329,7 +247,7 @@ fn faucet_send( spend_from_core(keypair.x_only_public_key().0, shared_daemon.clone())?; // check that the first output of the transaction pays to the key we just created - assert!( + debug_assert!( core_tx.output[0].script_pubkey == ScriptBuf::new_p2tr_tweaked( keypair.x_only_public_key().0.dangerous_assume_tweaked() @@ -411,40 +329,12 @@ fn faucet_send( 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_wallet.get_wallet()?; - - 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_client().get_scan_key())?; - - let p2tr_outs = vec![ext_output_key, change_output_key]; - - let ours = client - .get_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(daemon) = shared_daemon.lock() { - // get current blockheight - let blkheight: u32 = daemon.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() { daemon.broadcast(&first_tx.unwrap())?; @@ -454,17 +344,6 @@ fn faucet_send( return Err(Error::msg("Failed to lock daemon")); } - // update our sp_client with the change output(s) - sp_wallet - .get_wallet()? - .get_mut_outputs() - .extend_from(new_outpoints); - - debug!("{:?}", sp_wallet); - - // save to disk - sp_wallet.save()?; - Ok(final_tx) } @@ -476,9 +355,33 @@ fn handle_faucet_request( if let Ok(sp_address) = SilentPaymentAddress::try_from(msg) { debug!("Sending bootstrap coins to {}", sp_address); // send bootstrap coins to this sp_address - let tx = faucet_send(sp_address, sp_wallet, shared_daemon.clone())?; - let partial_tweak = - compute_partial_tweak_to_transaction(tx.clone(), shared_daemon.clone())?; + let tx = faucet_send(sp_address, sp_wallet.clone(), shared_daemon.clone())?; + + // get the tweak + let partial_tweak = compute_partial_tweak_to_transaction(&tx, shared_daemon.clone())?; + + debug!("Got the tweak"); + + // get current blockheight + let blkheight: u32 = shared_daemon + .lock_anyhow()? + .get_current_height()? + .try_into()?; + + // update our sp_client with the change output(s) + sp_wallet.get_wallet()?.update_wallet_with_transaction( + &tx, + blkheight, + partial_tweak, + )?; + + debug!("{:?}", sp_wallet.get_wallet()?.get_outputs()); + + debug!("updated the wallet"); + // save to disk + sp_wallet.save()?; + + debug!("saved the wallet"); Ok(NewTxMessage::new( serialize(&tx).to_lower_hex_string(), Some(partial_tweak.to_string()), @@ -594,19 +497,18 @@ async fn handle_connection( } fn compute_partial_tweak_to_transaction( - tx: Transaction, + tx: &Transaction, daemon: Arc>, ) -> Result { let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len()); let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); - for input in tx.input { + for input in tx.input.iter() { 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)))? + .lock_anyhow()? .get_transaction(&input.previous_output.txid, None) .map_err(|e| Error::msg(format!("Failed to find previous transaction: {}", e)))?; @@ -642,7 +544,7 @@ fn create_new_tx_message(transaction: Vec, daemon: Arc>) -> Re return Err(Error::msg("Can't process coinbase transaction")); } - let partial_tweak = compute_partial_tweak_to_transaction(tx, daemon)?; + let partial_tweak = compute_partial_tweak_to_transaction(&tx, daemon)?; Ok(NewTxMessage::new( transaction.to_lower_hex_string(), Some(partial_tweak.to_string()), @@ -666,35 +568,30 @@ async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) { }; debug!("Received a message"); - let payload: String = - if let (Some(topic), Some(data)) = (core_msg.get(0), core_msg.get(1)) { - match std::str::from_utf8(&topic) { - Ok("rawtx") => { - match create_new_tx_message(data.to_vec(), shared_daemon.clone()) { - Ok(m) => serde_json::to_string(&m).expect("This shouldn't fail"), - Err(e) => { - error!("{}", e); - continue; - } - } - } - Ok("hashblock") => todo!(), - _ => { - error!("Unexpected message in zmq"); + let payload: String = if let (Some(topic), Some(data)) = (core_msg.get(0), core_msg.get(1)) + { + match std::str::from_utf8(&topic) { + Ok("rawtx") => match create_new_tx_message(data.to_vec(), shared_daemon.clone()) { + Ok(m) => serde_json::to_string(&m).expect("This shouldn't fail"), + Err(e) => { + error!("{}", e); continue; } + }, + Ok("hashblock") => todo!(), + _ => { + error!("Unexpected message in zmq"); + continue; } - } else { - error!("Empty message"); - continue; - }; + } + } else { + error!("Empty message"); + continue; + }; - if let Err(e) = broadcast_message( - peers.clone(), - AnkFlag::NewTx, - payload, - BroadcastType::ToAll, - ) { + if let Err(e) = + broadcast_message(peers.clone(), AnkFlag::NewTx, payload, BroadcastType::ToAll) + { log::error!("{}", e.to_string()); } } From ec8e4ebef87411ad826d5ebc65e77f9425e506e7 Mon Sep 17 00:00:00 2001 From: Sosthene00 Date: Mon, 29 Apr 2024 16:03:09 +0200 Subject: [PATCH 20/27] refactoring --- src/main.rs | 233 +++++++++++++++++++++++++++++----------------------- 1 file changed, 131 insertions(+), 102 deletions(-) diff --git a/src/main.rs b/src/main.rs index e115aaa..59a2515 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use sdk_common::network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}; use sp_client::bitcoin::{ absolute::LockTime, consensus::{deserialize, serialize}, - hex::DisplayHex, + hex::{DisplayHex, FromHex}, key::TapTweak, sighash::{Prevouts, SighashCache}, taproot::Signature, @@ -26,21 +26,16 @@ use sp_client::bitcoin::{ use sp_client::{ bitcoin::secp256k1::{ rand::{thread_rng, Rng}, - Keypair, Message as Secp256k1Message, PublicKey, - Secp256k1, ThirtyTwoByteHash, + Keypair, Message as Secp256k1Message, PublicKey, Secp256k1, ThirtyTwoByteHash, }, spclient::SpWallet, }; use sp_client::db::{JsonFile, Storage}; use sp_client::silentpayments::sending::{generate_recipient_pubkeys, SilentPaymentAddress}; -use sp_client::silentpayments::utils::receiving::{ - calculate_tweak_data, get_pubkey_from_input, -}; +use sp_client::silentpayments::utils::receiving::{calculate_tweak_data, get_pubkey_from_input}; use sp_client::silentpayments::utils::sending::calculate_partial_secret; -use sp_client::spclient::{ - derive_keys_from_seed, Recipient, SpClient, SpendKey, -}; +use sp_client::spclient::{derive_keys_from_seed, Recipient, SpClient, SpendKey}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -61,7 +56,7 @@ type PeerMap = Arc>>; type SharedDaemon = Arc>; -const FAUCET_AMT: Amount = Amount::from_sat(1000); +const FAUCET_AMT: Amount = Amount::from_sat(100_000); pub(crate) trait MutexExt { fn lock_anyhow(&self) -> Result, Error>; @@ -227,8 +222,12 @@ fn faucet_send( .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())?; - let partial_secret = wallet.get_client().get_partial_secret_from_psbt(&new_psbt)?; - wallet.get_client().fill_sp_outputs(&mut new_psbt, partial_secret)?; + let partial_secret = wallet + .get_client() + .get_partial_secret_from_psbt(&new_psbt)?; + wallet + .get_client() + .fill_sp_outputs(&mut new_psbt, partial_secret)?; log::debug!("Definitive psbt: {}", new_psbt); let mut aux_rand = [0u8; 32]; thread_rng().fill(&mut aux_rand); @@ -292,12 +291,19 @@ fn faucet_send( 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()); + // Take some margin to pay for the fees + if core_tx.output[0].value < FAUCET_AMT * 4 { + return Err(Error::msg("Not enough funds")); + } + + let change_amt = core_tx.output[0].value.checked_sub(FAUCET_AMT).unwrap(); + 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, + value: change_amt, script_pubkey: change_spk, }); @@ -352,45 +358,51 @@ fn handle_faucet_request( sp_wallet: Arc, shared_daemon: SharedDaemon, ) -> Result { - if let Ok(sp_address) = SilentPaymentAddress::try_from(msg) { - debug!("Sending bootstrap coins to {}", sp_address); - // send bootstrap coins to this sp_address - let tx = faucet_send(sp_address, sp_wallet.clone(), shared_daemon.clone())?; + let sp_address = SilentPaymentAddress::try_from(msg)?; + debug!("Sending bootstrap coins to {}", sp_address); + // send bootstrap coins to this sp_address + let tx = faucet_send(sp_address, sp_wallet.clone(), shared_daemon.clone())?; - // get the tweak + // get the tweak + let partial_tweak = compute_partial_tweak_to_transaction(&tx, shared_daemon.clone())?; + + // get current blockheight + let blkheight: u32 = shared_daemon + .lock_anyhow()? + .get_current_height()? + .try_into()?; + + // update our sp_client with the change output(s) + sp_wallet + .get_wallet()? + .update_wallet_with_transaction(&tx, blkheight, partial_tweak)?; + + debug!("{:?}", sp_wallet); + + debug!("updated the wallet"); + // save to disk + sp_wallet.save()?; + + debug!("saved the wallet"); + Ok(NewTxMessage::new( + serialize(&tx).to_lower_hex_string(), + Some(partial_tweak.to_string()), + )) +} + +fn handle_new_tx_request(msg: &str, shared_daemon: SharedDaemon) -> Result { + let mut new_tx_msg = serde_json::from_str::(msg)?; + let tx = deserialize::(&Vec::from_hex(&new_tx_msg.transaction)?)?; + if new_tx_msg.tweak_data.is_none() { + // we add the tweak_data let partial_tweak = compute_partial_tweak_to_transaction(&tx, shared_daemon.clone())?; - - debug!("Got the tweak"); - - // get current blockheight - let blkheight: u32 = shared_daemon - .lock_anyhow()? - .get_current_height()? - .try_into()?; - - // update our sp_client with the change output(s) - sp_wallet.get_wallet()?.update_wallet_with_transaction( - &tx, - blkheight, - partial_tweak, - )?; - - debug!("{:?}", sp_wallet.get_wallet()?.get_outputs()); - - debug!("updated the wallet"); - // save to disk - sp_wallet.save()?; - - debug!("saved the wallet"); - Ok(NewTxMessage::new( - serialize(&tx).to_lower_hex_string(), - Some(partial_tweak.to_string()), - )) - } else { - Err(Error::msg(format!( - "faucet message with unparsable sp_address" - ))) + new_tx_msg.tweak_data = Some(partial_tweak.to_string()); } + + // we try to broadcast it + shared_daemon.lock_anyhow()?.broadcast(&tx)?; + + Ok(new_tx_msg) } async fn handle_connection( @@ -419,65 +431,83 @@ async fn handle_connection( let (outgoing, incoming) = ws_stream.split(); - let broadcast_incoming = incoming.try_for_each({ + let broadcast_incoming = incoming.try_for_each(|msg| { let peers = peers.clone(); - move |msg| { - if let Ok(raw_msg) = msg.to_text() { - debug!("Received msg: {}", raw_msg); - let parsed = serde_json::from_str::(raw_msg); - match parsed { - Ok(ank_msg) => match ank_msg.flag { - AnkFlag::Faucet => { - debug!("Received a faucet message"); - if let Ok(content) = - serde_json::from_str::(&ank_msg.content) - { - match handle_faucet_request( - &content.sp_address, - sp_wallet.clone(), - shared_daemon.clone(), - ) { - Ok(new_tx_msg) => { - if let Err(e) = broadcast_message( - peers.clone(), - AnkFlag::NewTx, - serde_json::to_string(&new_tx_msg) - .expect("This should not fail"), - BroadcastType::Sender(addr), - ) { - log::error!( - "Failed to broadcast message: {}", - e.to_string() - ); - } - } - Err(e) => { - if let Err(e) = broadcast_message( - peers.clone(), - AnkFlag::Error, - e.to_string(), - BroadcastType::Sender(addr), - ) { - log::error!("Failed to broadcast message: {}", e); - } + if let Ok(raw_msg) = msg.to_text() { + debug!("Received msg: {}", raw_msg); + let parsed = serde_json::from_str::(raw_msg); + match parsed { + Ok(ank_msg) => match ank_msg.flag { + AnkFlag::Faucet => { + debug!("Received a faucet message"); + if let Ok(content) = serde_json::from_str::(&ank_msg.content) + { + match handle_faucet_request( + &content.sp_address, + sp_wallet.clone(), + shared_daemon.clone(), + ) { + Ok(new_tx_msg) => { + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::NewTx, + serde_json::to_string(&new_tx_msg) + .expect("This should not fail"), + BroadcastType::Sender(addr), + ) { + log::error!( + "Failed to broadcast message: {}", + e.to_string() + ); } } - } else { - log::error!("Invalid content for faucet message"); + Err(e) => { + log::error!("Failed to send faucet tx: {}", e); + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::Error, + e.to_string(), + BroadcastType::Sender(addr), + ) { + log::error!("Failed to broadcast message: {}", e); + } + } + } + } else { + log::error!("Invalid content for faucet message"); + } + } + AnkFlag::NewTx => { + debug!("Received a new tx message"); + match handle_new_tx_request(&ank_msg.content, shared_daemon.clone()) { + Ok(new_tx_msg) => { + // Repeat the msg to all except sender + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::NewTx, + serde_json::to_string(&new_tx_msg) + .expect("This should not fail"), + BroadcastType::ExcludeSender(addr), + ) { + log::error!("Failed to send message with error: {}", e); + } + } + Err(e) => { + log::error!("handle_new_tx_request returned error: {}", e); } } - AnkFlag::NewTx => unimplemented!(), - AnkFlag::Error => unimplemented!(), - AnkFlag::Unknown => unimplemented!(), - }, - Err(_) => log::error!("Failed to parse network message"), - } - } else { - // we don't care - log::debug!("Received non-text message {} from peer {}", msg, addr); + } + AnkFlag::Error => unimplemented!(), + AnkFlag::Unknown => unimplemented!(), + AnkFlag::Prd => unimplemented!(), + }, + Err(_) => log::error!("Failed to parse network message"), } - future::ok(()) + } else { + // we don't care + log::debug!("Received non-text message {} from peer {}", msg, addr); } + future::ok(()) }); let receive_from_others = UnboundedReceiverStream::new(rx) @@ -557,7 +587,6 @@ async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) { socket.connect("tcp://127.0.0.1:29100").await.unwrap(); socket.subscribe("rawtx").await.unwrap(); // socket.subscribe("hashblock"); - debug!("{:?}", socket.type_id()); loop { let core_msg = match socket.recv().await { Ok(m) => m, From 4455c76663a7909187743a085d9676972d94fdda Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 6 May 2024 10:48:56 +0200 Subject: [PATCH 21/27] Process Unknown message --- src/main.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 59a2515..2a61286 100644 --- a/src/main.rs +++ b/src/main.rs @@ -498,7 +498,18 @@ async fn handle_connection( } } AnkFlag::Error => unimplemented!(), - AnkFlag::Unknown => unimplemented!(), + AnkFlag::Unknown => { + debug!("Received an unknown message"); + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::Unknown, + serde_json::to_string(&ank_msg.content) + .expect("This should not fail"), + BroadcastType::ExcludeSender(addr), + ) { + log::error!("Failed to send message with error: {}", e); + } + }, AnkFlag::Prd => unimplemented!(), }, Err(_) => log::error!("Failed to parse network message"), From 083843a94a083faa4b5cf2a914a26d09e2e426be Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 18 May 2024 16:33:25 +0200 Subject: [PATCH 22/27] [bug] estimate_fee --- src/daemon.rs | 12 ++++++++---- src/main.rs | 3 +-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 4e702e0..d3beb46 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -166,12 +166,16 @@ impl Daemon { Ok(Self { rpc }) } - pub(crate) fn estimate_fee(&self, nblocks: u16) -> Result> { - Ok(self + pub(crate) fn estimate_fee(&self, nblocks: u16) -> Result { + let res = self .rpc .estimate_smart_fee(nblocks, None) - .context("failed to estimate fee")? - .fee_rate) + .context("failed to estimate fee")?; + if res.errors.is_some() { + Err(Error::msg(serde_json::to_string(&res.errors.unwrap())?)) + } else { + Ok(res.fee_rate.unwrap()) + } } pub(crate) fn get_relay_fee(&self) -> Result { diff --git a/src/main.rs b/src/main.rs index 2a61286..42dfcd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -207,8 +207,7 @@ fn faucet_send( let fee_estimate = shared_daemon .lock_anyhow()? .estimate_fee(6) - .unwrap_or(Some(Amount::from_sat(1000))) - .unwrap() + .unwrap_or(Amount::from_sat(1000)) .checked_div(1000) .unwrap(); From de84c8a1bff39d29f39b378fc698353f43e863f8 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 22 May 2024 10:18:02 +0200 Subject: [PATCH 23/27] Faucet refactoring --- src/main.rs | 100 ++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/src/main.rs b/src/main.rs index 42dfcd0..160a5fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::{ - any::Any, collections::HashMap, env, fmt::Debug, @@ -11,17 +10,12 @@ use std::{ use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; -use sdk_common::network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}; +use sdk_common::{ + network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}, + silentpayments::create_transaction_for_address_with_shared_secret, +}; use sp_client::bitcoin::{ - absolute::LockTime, - consensus::{deserialize, serialize}, - hex::{DisplayHex, FromHex}, - key::TapTweak, - sighash::{Prevouts, SighashCache}, - taproot::Signature, - transaction::Version, - Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, - XOnlyPublicKey, + absolute::LockTime, consensus::{deserialize, serialize}, hex::{DisplayHex, FromHex}, key::TapTweak, script::PushBytesBuf, sighash::{Prevouts, SighashCache}, taproot::Signature, transaction::Version, Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey }; use sp_client::{ bitcoin::secp256k1::{ @@ -99,12 +93,12 @@ fn broadcast_message( payload: String, broadcast: BroadcastType, ) -> Result<()> { - // log::debug!("Broadcasting message: {}", msg); let ank_msg = AnkNetworkMsg { flag, content: payload, }; let msg = Message::Text(serde_json::to_string(&ank_msg)?); + log::debug!("Broadcasting message: {}", msg); match broadcast { BroadcastType::Sender(addr) => { peers @@ -114,7 +108,7 @@ fn broadcast_message( .find(|(peer_addr, _)| peer_addr == &&addr) .ok_or(Error::msg("Failed to find the sender in the peer_map"))? .1 - .send(msg.clone())?; + .send(msg)?; } BroadcastType::ExcludeSender(addr) => { peers @@ -173,6 +167,7 @@ fn spend_from_core( fn faucet_send( sp_address: SilentPaymentAddress, + commitment: &str, sp_wallet: Arc, shared_daemon: SharedDaemon, ) -> Result { @@ -215,26 +210,13 @@ fn faucet_send( let wallet = sp_wallet.get_wallet()?; - let mut new_psbt = - wallet - .get_client() - .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())?; - let partial_secret = wallet - .get_client() - .get_partial_secret_from_psbt(&new_psbt)?; - wallet - .get_client() - .fill_sp_outputs(&mut new_psbt, partial_secret)?; - log::debug!("Definitive psbt: {}", new_psbt); - let mut aux_rand = [0u8; 32]; - thread_rng().fill(&mut aux_rand); - let mut signed = wallet.get_client().sign_psbt(new_psbt, &aux_rand)?; - log::debug!("signed psbt: {}", signed); - SpClient::finalize_psbt(&mut signed)?; + let signed_psbt = create_transaction_for_address_with_shared_secret( + recipient, &wallet, Some(commitment), fee_estimate, + )?; - final_tx = signed.extract_tx()?; + let psbt = Psbt::from_str(&signed_psbt)?; + + final_tx = psbt.extract_tx()?; } else { // let's try to spend directly from the mining address let secp = Secp256k1::signing_only(); @@ -252,6 +234,7 @@ fn faucet_send( ) ); + // This is ugly and can be streamlined // create a new transaction that spends the newly created UTXO to the sp_address let mut faucet_tx = Transaction { input: vec![TxIn { @@ -289,6 +272,10 @@ fn faucet_send( 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 mut op_return = PushBytesBuf::new(); + op_return.extend_from_slice(&Vec::from_hex(commitment)?)?; + let data_spk = ScriptBuf::new_op_return(op_return); // Take some margin to pay for the fees if core_tx.output[0].value < FAUCET_AMT * 4 { @@ -305,6 +292,10 @@ fn faucet_send( value: change_amt, script_pubkey: change_spk, }); + faucet_tx.output.push(TxOut { + value: Amount::from_sat(0), + script_pubkey: data_spk, + }); // dummy signature only used for fee estimation faucet_tx.input[0].witness.push([1; 64].to_vec()); @@ -344,7 +335,8 @@ fn faucet_send( if first_tx.is_some() { daemon.broadcast(&first_tx.unwrap())?; } - daemon.broadcast(&final_tx)?; + let txid = daemon.broadcast(&final_tx)?; + debug!("Sent tx {}", txid); } else { return Err(Error::msg("Failed to lock daemon")); } @@ -353,14 +345,19 @@ fn faucet_send( } fn handle_faucet_request( - msg: &str, + msg: &FaucetMessage, sp_wallet: Arc, shared_daemon: SharedDaemon, ) -> Result { - let sp_address = SilentPaymentAddress::try_from(msg)?; + let sp_address = SilentPaymentAddress::try_from(msg.sp_address.as_str())?; debug!("Sending bootstrap coins to {}", sp_address); // send bootstrap coins to this sp_address - let tx = faucet_send(sp_address, sp_wallet.clone(), shared_daemon.clone())?; + let tx = faucet_send( + sp_address, + &msg.commitment, + sp_wallet.clone(), + shared_daemon.clone(), + )?; // get the tweak let partial_tweak = compute_partial_tweak_to_transaction(&tx, shared_daemon.clone())?; @@ -442,23 +439,15 @@ async fn handle_connection( if let Ok(content) = serde_json::from_str::(&ank_msg.content) { match handle_faucet_request( - &content.sp_address, + &content, sp_wallet.clone(), shared_daemon.clone(), ) { Ok(new_tx_msg) => { - if let Err(e) = broadcast_message( - peers.clone(), - AnkFlag::NewTx, - serde_json::to_string(&new_tx_msg) - .expect("This should not fail"), - BroadcastType::Sender(addr), - ) { - log::error!( - "Failed to broadcast message: {}", - e.to_string() - ); - } + debug!( + "Obtained new_tx_msg: {}", + serde_json::to_string(&new_tx_msg).unwrap() + ); } Err(e) => { log::error!("Failed to send faucet tx: {}", e); @@ -502,13 +491,12 @@ async fn handle_connection( if let Err(e) = broadcast_message( peers.clone(), AnkFlag::Unknown, - serde_json::to_string(&ank_msg.content) - .expect("This should not fail"), + serde_json::to_string(&ank_msg.content).expect("This should not fail"), BroadcastType::ExcludeSender(addr), ) { log::error!("Failed to send message with error: {}", e); } - }, + } AnkFlag::Prd => unimplemented!(), }, Err(_) => log::error!("Failed to parse network message"), @@ -578,6 +566,7 @@ fn compute_partial_tweak_to_transaction( } fn create_new_tx_message(transaction: Vec, daemon: Arc>) -> Result { + // debug!("Creating tx message"); let tx: Transaction = deserialize(&transaction)?; if tx.is_coinbase() { @@ -609,9 +598,13 @@ async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) { let payload: String = if let (Some(topic), Some(data)) = (core_msg.get(0), core_msg.get(1)) { + // debug!("topic: {}", std::str::from_utf8(&topic).unwrap()); match std::str::from_utf8(&topic) { Ok("rawtx") => match create_new_tx_message(data.to_vec(), shared_daemon.clone()) { - Ok(m) => serde_json::to_string(&m).expect("This shouldn't fail"), + Ok(m) => { + debug!("Created message"); + serde_json::to_string(&m).expect("This shouldn't fail") + } Err(e) => { error!("{}", e); continue; @@ -628,6 +621,7 @@ async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) { continue; }; + debug!("Broadcasting message {}", payload); if let Err(e) = broadcast_message(peers.clone(), AnkFlag::NewTx, payload, BroadcastType::ToAll) { From 539670d24805bb30c754a0d24484dd016a794f4e Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 12:10:39 +0200 Subject: [PATCH 24/27] Handle new message types and better error management --- src/daemon.rs | 6 ++++ src/main.rs | 83 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index d3beb46..661bc5f 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -294,6 +294,12 @@ impl Daemon { Ok(blockchain_info.chain) } + pub(crate) fn test_mempool_accept(&self, tx: &Transaction) -> Result { + let res = self.rpc.test_mempool_accept(&vec![tx])?; + + Ok(res.get(0).unwrap().clone()) + } + pub(crate) fn broadcast(&self, tx: &Transaction) -> Result { let txid = self.rpc.send_raw_transaction(tx)?; diff --git a/src/main.rs b/src/main.rs index 160a5fa..6d724b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,7 @@ use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; use sdk_common::{ - network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}, - silentpayments::create_transaction_for_address_with_shared_secret, + error::AnkError, network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}, silentpayments::create_transaction_for_address_with_shared_secret }; use sp_client::bitcoin::{ absolute::LockTime, consensus::{deserialize, serialize}, hex::{DisplayHex, FromHex}, key::TapTweak, script::PushBytesBuf, sighash::{Prevouts, SighashCache}, taproot::Signature, transaction::Version, Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey @@ -386,9 +385,12 @@ fn handle_faucet_request( )) } -fn handle_new_tx_request(msg: &str, shared_daemon: SharedDaemon) -> Result { - let mut new_tx_msg = serde_json::from_str::(msg)?; +fn handle_new_tx_request(new_tx_msg: &mut NewTxMessage, shared_daemon: SharedDaemon) -> Result<()> { let tx = deserialize::(&Vec::from_hex(&new_tx_msg.transaction)?)?; + let mempool_accept = shared_daemon.lock_anyhow()?.test_mempool_accept(&tx)?; + if !mempool_accept.allowed { + return Err(AnkError::NewTxError(mempool_accept.reject_reason.unwrap()))?; + } if new_tx_msg.tweak_data.is_none() { // we add the tweak_data let partial_tweak = compute_partial_tweak_to_transaction(&tx, shared_daemon.clone())?; @@ -398,7 +400,7 @@ fn handle_new_tx_request(msg: &str, shared_daemon: SharedDaemon) -> Result match ank_msg.flag { AnkFlag::Faucet => { debug!("Received a faucet message"); - if let Ok(content) = serde_json::from_str::(&ank_msg.content) + if let Ok(mut content) = serde_json::from_str::(&ank_msg.content) { match handle_faucet_request( &content, @@ -451,10 +453,12 @@ async fn handle_connection( } Err(e) => { log::error!("Failed to send faucet tx: {}", e); + content.error = Some(e.into()); + let payload = serde_json::to_string(&content).expect("Message type shouldn't fail"); if let Err(e) = broadcast_message( peers.clone(), - AnkFlag::Error, - e.to_string(), + AnkFlag::Faucet, + payload, BroadcastType::Sender(addr), ) { log::error!("Failed to broadcast message: {}", e); @@ -467,37 +471,60 @@ async fn handle_connection( } AnkFlag::NewTx => { debug!("Received a new tx message"); - match handle_new_tx_request(&ank_msg.content, shared_daemon.clone()) { - Ok(new_tx_msg) => { - // Repeat the msg to all except sender - if let Err(e) = broadcast_message( - peers.clone(), - AnkFlag::NewTx, - serde_json::to_string(&new_tx_msg) - .expect("This should not fail"), - BroadcastType::ExcludeSender(addr), - ) { - log::error!("Failed to send message with error: {}", e); + if let Ok(mut new_tx_msg) = serde_json::from_str::(&ank_msg.content) { + match handle_new_tx_request(&mut new_tx_msg, shared_daemon.clone()) { + Ok(new_tx_msg) => { + // Repeat the msg to all except sender + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::NewTx, + serde_json::to_string(&new_tx_msg) + .expect("This should not fail"), + BroadcastType::ExcludeSender(addr), + ) { + log::error!("Failed to send message with error: {}", e); + } + } + Err(e) => { + log::error!("handle_new_tx_request returned error: {}", e); + new_tx_msg.error = Some(e.into()); + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::NewTx, + serde_json::to_string(&new_tx_msg).expect("This shouldn't fail"), + BroadcastType::Sender(addr), + ) { + log::error!("Failed to broadcast message: {}", e); + } } } - Err(e) => { - log::error!("handle_new_tx_request returned error: {}", e); - } + } else { + log::error!("Invalid content for new_tx message"); } } - AnkFlag::Error => unimplemented!(), - AnkFlag::Unknown => { - debug!("Received an unknown message"); + AnkFlag::Cipher => { + // For now we just send it to everyone + debug!("Received a cipher message"); if let Err(e) = broadcast_message( peers.clone(), - AnkFlag::Unknown, - serde_json::to_string(&ank_msg.content).expect("This should not fail"), + AnkFlag::Cipher, + ank_msg.content, + BroadcastType::ExcludeSender(addr), + ) { + log::error!("Failed to send message with error: {}", e); + } + } + AnkFlag::Unknown => { + debug!("Received an unknown message"); + if let Err(e) = broadcast_message( + peers.clone(), + AnkFlag::Unknown, + ank_msg.content, BroadcastType::ExcludeSender(addr), ) { log::error!("Failed to send message with error: {}", e); } } - AnkFlag::Prd => unimplemented!(), }, Err(_) => log::error!("Failed to parse network message"), } From ad026a783e3bdcc0e0dd3a8e0e5fe1a69601d233 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 27 May 2024 12:14:18 +0200 Subject: [PATCH 25/27] Add a message cache --- src/daemon.rs | 5 ++- src/main.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 661bc5f..a3d8c80 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -294,7 +294,10 @@ impl Daemon { Ok(blockchain_info.chain) } - pub(crate) fn test_mempool_accept(&self, tx: &Transaction) -> Result { + pub(crate) fn test_mempool_accept( + &self, + tx: &Transaction, + ) -> Result { let res = self.rpc.test_mempool_accept(&vec![tx])?; Ok(res.get(0).unwrap().clone()) diff --git a/src/main.rs b/src/main.rs index 6d724b5..5b6cdea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,8 @@ use std::{ fmt::Debug, net::SocketAddr, str::FromStr, - sync::{Arc, Mutex, MutexGuard}, + sync::{Arc, Mutex, MutexGuard, OnceLock}, + time::{Duration, Instant}, }; use bitcoincore_rpc::json::{self as bitcoin_json}; @@ -29,7 +30,7 @@ use sp_client::silentpayments::sending::{generate_recipient_pubkeys, SilentPayme use sp_client::silentpayments::utils::receiving::{calculate_tweak_data, get_pubkey_from_input}; use sp_client::silentpayments::utils::sending::calculate_partial_secret; use sp_client::spclient::{derive_keys_from_seed, Recipient, SpClient, SpendKey}; -use tokio::net::{TcpListener, TcpStream}; +use tokio::{net::{TcpListener, TcpStream}, time}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; @@ -49,6 +50,66 @@ type PeerMap = Arc>>; type SharedDaemon = Arc>; +static MESSAGECACHE: OnceLock = OnceLock::new(); + +const MESSAGECACHEDURATION: Duration = Duration::from_secs(10); +const MESSAGECACHEINTERVAL: Duration = Duration::from_secs(2); + +#[derive(Debug)] +struct MessageCache { + store: Mutex>, +} + +impl MessageCache { + fn new() -> Self { + Self { + store: Mutex::new(HashMap::new()), + } + } + + fn insert(&self, key: String) { + let mut store = self.store.lock().unwrap(); + store.insert(key.clone(), Instant::now()); + } + + fn contains(&self, key: &str) -> bool { + let store = self.store.lock().unwrap(); + store.contains_key(key) + } +} + +async fn clean_up() { + let cache = MESSAGECACHE.get().unwrap(); + + let mut interval = time::interval(MESSAGECACHEINTERVAL); + + loop { + interval.tick().await; + + let mut store = cache.store.lock().unwrap(); + + let now = Instant::now(); + let to_rm: Vec = store + .iter() + .filter_map(|(entry, entrytime)| { + if let Some(duration) = now.checked_duration_since(*entrytime) { + if duration > MESSAGECACHEDURATION { + Some(entry.clone()) + } else { + None + } + } else { + None + } + }) + .collect(); + + for key in to_rm { + store.remove(&key); + } + } +} + const FAUCET_AMT: Amount = Amount::from_sat(100_000); pub(crate) trait MutexExt { @@ -433,12 +494,20 @@ async fn handle_connection( let peers = peers.clone(); if let Ok(raw_msg) = msg.to_text() { debug!("Received msg: {}", raw_msg); + let cache = MESSAGECACHE.get().expect("Cache should be initialized"); + if cache.contains(raw_msg) { + debug!("Message already processed, dropping"); + return future::ok(()); + } else { + cache.insert(raw_msg.to_owned()); + } let parsed = serde_json::from_str::(raw_msg); match parsed { Ok(ank_msg) => match ank_msg.flag { AnkFlag::Faucet => { debug!("Received a faucet message"); - if let Ok(mut content) = serde_json::from_str::(&ank_msg.content) + if let Ok(mut content) = + serde_json::from_str::(&ank_msg.content) { match handle_faucet_request( &content, @@ -454,7 +523,8 @@ async fn handle_connection( Err(e) => { log::error!("Failed to send faucet tx: {}", e); content.error = Some(e.into()); - let payload = serde_json::to_string(&content).expect("Message type shouldn't fail"); + let payload = serde_json::to_string(&content) + .expect("Message type shouldn't fail"); if let Err(e) = broadcast_message( peers.clone(), AnkFlag::Faucet, @@ -471,7 +541,9 @@ async fn handle_connection( } AnkFlag::NewTx => { debug!("Received a new tx message"); - if let Ok(mut new_tx_msg) = serde_json::from_str::(&ank_msg.content) { + if let Ok(mut new_tx_msg) = + serde_json::from_str::(&ank_msg.content) + { match handle_new_tx_request(&mut new_tx_msg, shared_daemon.clone()) { Ok(new_tx_msg) => { // Repeat the msg to all except sender @@ -491,7 +563,8 @@ async fn handle_connection( if let Err(e) = broadcast_message( peers.clone(), AnkFlag::NewTx, - serde_json::to_string(&new_tx_msg).expect("This shouldn't fail"), + serde_json::to_string(&new_tx_msg) + .expect("This shouldn't fail"), BroadcastType::Sender(addr), ) { log::error!("Failed to broadcast message: {}", e); @@ -672,6 +745,10 @@ async fn main() -> Result<()> { .expect("Please provide either \"true\" or \"false\""); let core_wallet: Option = env::args().nth(4); + MESSAGECACHE.set(MessageCache::new()).expect("Message Cache initialization failed"); + + tokio::spawn(clean_up()); + let peers = PeerMap::new(Mutex::new(HashMap::new())); // Connect the rpc daemon From 0d5149eb968c73df1b009ca8f78a2090918db547 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 28 May 2024 11:10:48 +0200 Subject: [PATCH 26/27] import sp_client through sdk_common --- Cargo.toml | 1 - src/daemon.rs | 8 ++++---- src/main.rs | 43 ++++++++++++++++++++++++++++++------------- src/scan.rs | 12 ++++++------ 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7513f6f..fccc035 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "demo serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" serde_with = "3.6.0" -sp_client = { git = "https://github.com/Sosthene00/sp-client", 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/daemon.rs b/src/daemon.rs index a3d8c80..222b68a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -5,12 +5,12 @@ use bitcoincore_rpc::json::{ WalletCreateFundedPsbtOptions, }; use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; -use sp_client::bitcoin::bip158::BlockFilter; -use sp_client::bitcoin::{ +use sdk_common::sp_client::bitcoin::bip158::BlockFilter; +use sdk_common::sp_client::bitcoin::{ block, Address, Amount, Block, BlockHash, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, }; -use sp_client::bitcoin::{consensus::deserialize, hashes::hex::FromHex}; +use sdk_common::sp_client::bitcoin::{consensus::deserialize, hashes::hex::FromHex}; // use crossbeam_channel::Receiver; // use parking_lot::Mutex; use serde_json::{json, Value}; @@ -328,7 +328,7 @@ impl Daemon { txid: &Txid, blockhash: Option, ) -> Result { - use sp_client::bitcoin::consensus::serde::{hex::Lower, Hex, With}; + use sdk_common::sp_client::bitcoin::consensus::serde::{hex::Lower, Hex, With}; let tx = self.get_transaction(txid, blockhash)?; #[derive(serde::Serialize)] diff --git a/src/main.rs b/src/main.rs index 5b6cdea..2c7f930 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,28 +10,45 @@ use std::{ use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; -use log::{debug, error}; -use sdk_common::{ - error::AnkError, network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}, silentpayments::create_transaction_for_address_with_shared_secret +use sdk_common::sp_client::bitcoin::{ + absolute::LockTime, + consensus::{deserialize, serialize}, + hex::{DisplayHex, FromHex}, + key::TapTweak, + script::PushBytesBuf, + sighash::{Prevouts, SighashCache}, + taproot::Signature, + transaction::Version, + Amount, Network, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, + XOnlyPublicKey, }; -use sp_client::bitcoin::{ - absolute::LockTime, consensus::{deserialize, serialize}, hex::{DisplayHex, FromHex}, key::TapTweak, script::PushBytesBuf, sighash::{Prevouts, SighashCache}, taproot::Signature, transaction::Version, Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey -}; -use sp_client::{ +use sdk_common::sp_client::{ bitcoin::secp256k1::{ rand::{thread_rng, Rng}, Keypair, Message as Secp256k1Message, PublicKey, Secp256k1, ThirtyTwoByteHash, }, spclient::SpWallet, }; +use sdk_common::{ + error::AnkError, + network::{AnkFlag, AnkNetworkMsg, FaucetMessage, NewTxMessage}, + silentpayments::create_transaction_for_address_with_shared_secret, +}; -use sp_client::db::{JsonFile, Storage}; -use sp_client::silentpayments::sending::{generate_recipient_pubkeys, SilentPaymentAddress}; -use sp_client::silentpayments::utils::receiving::{calculate_tweak_data, get_pubkey_from_input}; -use sp_client::silentpayments::utils::sending::calculate_partial_secret; -use sp_client::spclient::{derive_keys_from_seed, Recipient, SpClient, SpendKey}; -use tokio::{net::{TcpListener, TcpStream}, time}; +use sdk_common::sp_client::db::{JsonFile, Storage}; +use sdk_common::sp_client::silentpayments::sending::{ + generate_recipient_pubkeys, SilentPaymentAddress, +}; +use sdk_common::sp_client::silentpayments::utils::receiving::{ + calculate_tweak_data, get_pubkey_from_input, +}; +use sdk_common::sp_client::silentpayments::utils::sending::calculate_partial_secret; +use sdk_common::sp_client::spclient::{derive_keys_from_seed, Recipient, SpClient, SpendKey}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; +use tokio::{ + net::{TcpListener, TcpStream}, + time, +}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; diff --git a/src/scan.rs b/src/scan.rs index 93f8f30..aac69ca 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -5,12 +5,12 @@ use std::sync::Arc; use anyhow::{Error, Result}; use electrum_client::ElectrumApi; use hex::FromHex; -use sp_client::bitcoin::bip158::BlockFilter; -use sp_client::bitcoin::hex::DisplayHex; -use sp_client::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; -use sp_client::bitcoin::{BlockHash, OutPoint, Transaction, TxOut, XOnlyPublicKey}; -use sp_client::silentpayments::receiving::Receiver; -use sp_client::spclient::{OutputSpendStatus, OwnedOutput}; +use sdk_common::sp_client::bitcoin::bip158::BlockFilter; +use sdk_common::sp_client::bitcoin::hex::DisplayHex; +use sdk_common::sp_client::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; +use sdk_common::sp_client::bitcoin::{BlockHash, OutPoint, Transaction, TxOut, XOnlyPublicKey}; +use sdk_common::sp_client::silentpayments::receiving::Receiver; +use sdk_common::sp_client::spclient::{OutputSpendStatus, OwnedOutput}; use tokio::time::Instant; use crate::{electrumclient, MutexExt, SharedDaemon, SilentPaymentWallet}; From 7e5dc17841739b16fecb35dceaabb9d0406d18e7 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 28 May 2024 11:11:17 +0200 Subject: [PATCH 27/27] Add an argument for core rpc and network --- src/daemon.rs | 18 ++++++++++------- src/main.rs | 54 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 222b68a..a732ba3 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -16,6 +16,7 @@ use sdk_common::sp_client::bitcoin::{consensus::deserialize, hashes::hex::FromHe use serde_json::{json, Value}; use std::collections::HashMap; +use std::env; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; @@ -89,9 +90,7 @@ fn read_cookie(path: &Path) -> Result<(String, String)> { Ok((parts[0].to_owned(), parts[1].to_owned())) } -fn rpc_connect(rpcwallet: Option) -> Result { - let mut rpc_url = "http://127.0.0.1:39332".to_owned(); - +fn rpc_connect(rpcwallet: Option, network: Network, mut rpc_url: String) -> Result { match rpcwallet { Some(rpcwallet) => rpc_url.push_str(&rpcwallet), None => (), @@ -102,9 +101,12 @@ fn rpc_connect(rpcwallet: Option) -> Result { 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 home = env::var("HOME")?; + let mut cookie_path = PathBuf::from_str(&home)?; + cookie_path.push(".bitcoin"); + cookie_path.push(network.to_core_arg()); + cookie_path.push(".cookie"); + let daemon_auth = SensitiveAuth(Auth::CookieFile(cookie_path)); let builder = match daemon_auth.get_auth() { Auth::None => builder, Auth::UserPass(user, pass) => builder.auth(user, Some(pass)), @@ -127,11 +129,13 @@ pub struct Daemon { impl Daemon { pub(crate) fn connect( rpcwallet: Option, + rpc_url: String, + network: Network, // config: &Config, // exit_flag: &ExitFlag, // metrics: &Metrics, ) -> Result { - let mut rpc = rpc_connect(rpcwallet)?; + let mut rpc = rpc_connect(rpcwallet, network, rpc_url)?; loop { match rpc_poll(&mut rpc, false) { diff --git a/src/main.rs b/src/main.rs index 2c7f930..1261e40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,9 @@ use std::{ collections::HashMap, env, fmt::Debug, + fs, net::SocketAddr, + path::PathBuf, str::FromStr, sync::{Arc, Mutex, MutexGuard, OnceLock}, time::{Duration, Instant}, @@ -10,6 +12,7 @@ use std::{ use bitcoincore_rpc::json::{self as bitcoin_json}; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; +use log::{debug, error, info, warn}; use sdk_common::sp_client::bitcoin::{ absolute::LockTime, consensus::{deserialize, serialize}, @@ -288,7 +291,10 @@ fn faucet_send( let wallet = sp_wallet.get_wallet()?; let signed_psbt = create_transaction_for_address_with_shared_secret( - recipient, &wallet, Some(commitment), fee_estimate, + recipient, + &wallet, + Some(commitment), + fee_estimate, )?; let psbt = Psbt::from_str(&signed_psbt)?; @@ -349,7 +355,7 @@ fn faucet_send( 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 mut op_return = PushBytesBuf::new(); op_return.extend_from_slice(&Vec::from_hex(commitment)?)?; let data_spk = ScriptBuf::new_op_return(op_return); @@ -751,36 +757,50 @@ async fn handle_zmq(peers: PeerMap, shared_daemon: SharedDaemon) { async fn main() -> Result<()> { env_logger::init(); - let addr = env::args() + // This is rudimentary, if you change the network don't forget to change rpc_url either, we won't do that for you + let rpc_url = env::args() .nth(1) + .unwrap_or_else(|| "127.0.0.1:38332".to_owned()); + let listening_addr = env::args() + .nth(2) .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 wallet_name = env::args().nth(3).unwrap_or_else(|| "default".to_owned()); + let network_arg: String = env::args().nth(4).unwrap_or_else(|| "signet".to_owned()); + let core_wallet: Option = env::args().nth(5); - MESSAGECACHE.set(MessageCache::new()).expect("Message Cache initialization failed"); + let network = Network::from_core_arg(&network_arg)?; + + if network == Network::Bitcoin { + warn!("Running on mainnet, you're on your own"); + } + + MESSAGECACHE + .set(MessageCache::new()) + .expect("Message Cache initialization failed"); tokio::spawn(clean_up()); let peers = PeerMap::new(Mutex::new(HashMap::new())); // Connect the rpc daemon - let shared_daemon = Arc::new(Mutex::new(Daemon::connect(core_wallet)?)); + let shared_daemon = Arc::new(Mutex::new(Daemon::connect(core_wallet, rpc_url, network)?)); let current_tip: u32 = shared_daemon .lock_anyhow()? .get_current_height()? .try_into()?; - let mut config_dir = env::var("HOME")?; - config_dir.push_str("/.4nk"); - let sp_wallet_file = JsonFile::new(&config_dir, &wallet_name); + let mut config_dir = PathBuf::from_str(&env::var("HOME")?)?; + config_dir.push(".4nk"); + let sp_wallet_file = JsonFile::new(&config_dir, &PathBuf::from_str(&wallet_name)?); + fs::create_dir_all(config_dir)?; // load an existing sp_wallet, or create a new one + let is_testnet = if network == Network::Bitcoin { + false + } else { + true + }; let sp_wallet = match >::load(&sp_wallet_file) { Err(_) => { let mut seed = [0u8; 64]; @@ -845,9 +865,9 @@ async fn main() -> Result<()> { tokio::spawn(handle_zmq(peers.clone(), shared_daemon.clone())); // Create the event loop and TCP listener we'll accept connections on. - let try_socket = TcpListener::bind(&addr).await; + let try_socket = TcpListener::bind(&listening_addr).await; let listener = try_socket.expect("Failed to bind"); - debug!("Listening on: {}", addr); + debug!("Listening on: {}", listening_addr); // Let's spawn the handling of each connection in a separate task. while let Ok((stream, addr)) = listener.accept().await {