diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a2e3aca0..b1b91fe4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Rust ${{ matrix.toolchain }} toolchain uses: actions-rs/toolchain@v1 with: @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Rust stable toolchain uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/cln-plugin.yaml b/.github/workflows/cln-plugin.yaml index 11300c40..36c5074a 100644 --- a/.github/workflows/cln-plugin.yaml +++ b/.github/workflows/cln-plugin.yaml @@ -10,8 +10,11 @@ jobs: cache-cln: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + check-latest: true - name: Create CLN cache id: cache-cln uses: actions/cache@v3 @@ -21,6 +24,8 @@ jobs: path: lightning key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }} - name: Compile CLN + env: + PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring if: ${{ steps.cache-cln.outputs.cache-hit != 'true' }} run: | sudo apt-get update && sudo apt-get install gettext @@ -32,8 +37,11 @@ jobs: needs: cache-cln runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + check-latest: true - name: Install bitcoind run: | wget https://bitcoincore.org/bin/bitcoin-core-${{ env.bitcoind_version }}/bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz @@ -52,8 +60,8 @@ jobs: cd lightning && sudo make install - name: Install teos and the plugin run: | - cargo install --path teos - cargo install --path watchtower-plugin + cargo install --locked --path teos + cargo install --locked --path watchtower-plugin - name: Add test dependencies run: | cd watchtower-plugin/tests @@ -61,4 +69,4 @@ jobs: - name: Run tests run: | cd watchtower-plugin/tests - DEVELOPER=1 SLOW_MACHINE=1 poetry run pytest test.py -s + DEVELOPER=1 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s diff --git a/.gitignore b/.gitignore index a8c94cac..3b7e0f70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ target __pycache__ -Cargo.lock .vscode .idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..b99a1d52 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3876 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.5", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2 0.4.4", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-object-pool" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb901c30ebc2fc4ab46395bbfbdba9542c16559d853645d75190c3056caf3bc" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +dependencies = [ + "async-io", + "blocking", + "cfg-if 1.0.0", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi 0.3.9", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.8", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.5", + "instant", + "pin-project-lite 0.2.8", + "rand 0.8.5", + "tokio 1.20.1", +] + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + +[[package]] +name = "basic-cookies" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitcoin" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +dependencies = [ + "base64-compat", + "bech32", + "bitcoin_hashes", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0e67dbf7a9971e7f4276f6089e9e814ce0f624a03216b7d92d00351ae7fb3e" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2ae16202721ba8c3409045681fac790a5ddc791f05731a2df22c0c6bffc0f1" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f08493fa7707effc63254c66c6ea908675912493cd67952eda23c09fae2610b1" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6547abe025f4027edacd9edaa357aded014eecec42a5070d9b885c3c334aba2" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cln-plugin" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2043841c090a404cb81b145c8ad3c66bae122ba722387fc322b93c157d596433" +dependencies = [ + "anyhow", + "bytes 1.1.0", + "cln-rpc", + "futures", + "log", + "serde", + "serde_json", + "tokio 1.20.1", + "tokio-stream", + "tokio-util 0.6.9", +] + +[[package]] +name = "cln-rpc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb71ceca239c83a06fb494028b4a6b7b38ad4dd9c0410b7ea6013b90e15045" +dependencies = [ + "anyhow", + "bytes 1.1.0", + "futures-util", + "hex", + "log", + "native-tls", + "serde", + "serde_json", + "tokio 1.20.1", + "tokio-util 0.6.9", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctor" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curl" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.4.4", + "winapi 0.3.9", +] + +[[package]] +name = "curl-sys" +version = "0.4.55+curl-7.83.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi 0.3.9", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "der-oid-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c73af209b6a5dc8ca7cbaba720732304792cddc933cfea3d74509c2b1ef2f436" +dependencies = [ + "num-bigint", + "num-traits", + "syn", +] + +[[package]] +name = "der-parser" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cddf120f700b411b2b02ebeb7f04dc0b7c8835909a6c2f52bf72ed0dd3433b2" +dependencies = [ + "der-oid-macro", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.8", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.8", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "gloo-timers" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 0.2.25", + "tokio-util 0.3.1", + "tracing", + "tracing-futures", +] + +[[package]] +name = "h2" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +dependencies = [ + "bytes 1.1.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 1.20.1", + "tokio-util 0.6.9", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "headers" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +dependencies = [ + "base64", + "bitflags", + "bytes 1.1.0", + "headers-core", + "http", + "httpdate 1.0.2", + "mime", + "sha-1 0.10.0", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes 1.1.0", + "fnv", + "itoa 1.0.1", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes 1.1.0", + "http", + "pin-project-lite 0.2.8", +] + +[[package]] +name = "httparse" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "httpmock" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c159c4fc205e6c1a9b325cb7ec135d13b5f47188ce175dabb76ec847f331d9bd" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-trait", + "base64", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper 0.14.18", + "isahc", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio 1.20.1", + "url", +] + +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.2.7", + "http", + "http-body 0.3.1", + "httparse", + "httpdate 0.3.2", + "itoa 0.4.8", + "pin-project", + "socket2 0.3.19", + "tokio 0.2.25", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes 1.1.0", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.11", + "http", + "http-body 0.4.4", + "httparse", + "httpdate 1.0.2", + "itoa 1.0.1", + "pin-project-lite 0.2.8", + "socket2 0.4.4", + "tokio 1.20.1", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.18", + "pin-project-lite 0.2.8", + "tokio 1.20.1", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper 0.14.18", + "native-tls", + "tokio 1.20.1", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http", + "log", + "mime", + "once_cell", + "polling", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8423b78fc94d12ef1a4a9d13c348c9a78766dda0cc18817adf0faf77e670c8" +dependencies = [ + "base64-compat", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jsonrpc-core" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4467ab6dfa369b69e52bd0692e480c4d117410538526a57a304a0f2250fd95e" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jsonrpc-http-server" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522a047cac0958097ee71d047dd71cb84979fd2fa21c7a68fbe12736bef870a2" +dependencies = [ + "futures", + "hyper 0.13.10", + "jsonrpc-core", + "jsonrpc-server-utils", + "log", + "net2", + "parking_lot", + "unicase", +] + +[[package]] +name = "jsonrpc-server-utils" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce68fa279a2822b3619369cd024f8a4f8e5ce485468834f8679a3c7919aae2d" +dependencies = [ + "bytes 0.5.6", + "futures", + "globset", + "jsonrpc-core", + "lazy_static", + "log", + "tokio 0.2.25", + "tokio-util 0.3.1", + "unicase", +] + +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30455341b0e18f276fa64540aff54deafb54c589de6aca68659c63dd2d5d823" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf796c978e9b4d983414f4caedc9273aa33ee214c5b887bd55fde84c85d2dc4" +dependencies = [ + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "libnghttp2-sys" +version = "0.1.7+1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lightning" +version = "0.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d885bf509066af86ae85354c8959028ad6192c22a2657ef8271e94029d30f9d0" +dependencies = [ + "bitcoin", +] + +[[package]] +name = "lightning-block-sync" +version = "0.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f1ed50f41785af19f5cd1225b668e87ef0d59bb84e6f8ef2542933e6082a2c" +dependencies = [ + "bitcoin", + "chunked_transfer", + "futures", + "lightning", + "serde", + "serde_json", +] + +[[package]] +name = "lightning-net-tokio" +version = "0.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0170619152c4d6b947d5ed0de427b85691482a293e0cae52d4336a2220a776" +dependencies = [ + "bitcoin", + "lightning", + "tokio 1.20.1", +] + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if 1.0.0", + "value-bag", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.8.5", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + +[[package]] +name = "oid-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe554cb2393bc784fd678c82c84cc0599c31ceadc7f03a594911f822cb8d1815" +dependencies = [ + "der-parser", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.22.0+1.1.1q" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" +dependencies = [ + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "pem" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +dependencies = [ + "base64", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "winapi 0.3.9", +] + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" +dependencies = [ + "bytes 1.1.0", + "prost-derive 0.8.0", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes 1.1.0", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes 1.1.0", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost 0.9.0", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes 1.1.0", + "prost 0.9.0", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[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 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.5", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rcgen" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5911d1403f4143c9d56a702069d593e8d0f3fab880a85e103604d0893ea31ba7" +dependencies = [ + "chrono", + "pem", + "ring", + "x509-parser", + "yasna", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.5", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +dependencies = [ + "base64", + "bytes 1.1.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.11", + "http", + "http-body 0.4.4", + "hyper 0.14.18", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.8", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 1.20.1", + "tokio-native-tls", + "tokio-socks", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rusqlite" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustversion" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "indexmap", + "itoa 1.0.1", + "ryu", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.1", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" + +[[package]] +name = "similar" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" + +[[package]] +name = "simple_logger" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75a9723083573ace81ad0cdfc50b858aa3c366c48636edb4109d73122a0c0ea" +dependencies = [ + "atty", + "colored", + "log", + "time", + "winapi 0.3.9", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "string_cache" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "teos" +version = "0.1.2" +dependencies = [ + "bitcoin", + "bitcoincore-rpc", + "hex", + "home", + "jsonrpc-http-server", + "lightning", + "lightning-block-sync", + "lightning-net-tokio", + "log", + "prost 0.9.0", + "rand 0.8.5", + "rcgen", + "rusqlite", + "serde", + "serde_json", + "simple_logger", + "structopt", + "tempdir", + "teos-common", + "tokio 1.20.1", + "tokio-stream", + "toml", + "tonic 0.6.2", + "tonic-build", + "torut", + "triggered", + "warp", +] + +[[package]] +name = "teos-common" +version = "0.1.2" +dependencies = [ + "bitcoin", + "chacha20poly1305", + "hex", + "lightning", + "prost 0.9.0", + "rand 0.8.5", + "rusqlite", + "serde", + "serde_json", + "tonic 0.6.2", + "tonic-build", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi 0.3.9", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +dependencies = [ + "itoa 1.0.1", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "memchr", + "mio 0.6.23", + "num_cpus", + "pin-project-lite 0.1.12", + "slab", +] + +[[package]] +name = "tokio" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +dependencies = [ + "autocfg", + "bytes 1.1.0", + "libc", + "memchr", + "mio 0.8.4", + "num_cpus", + "once_cell", + "pin-project-lite 0.2.8", + "signal-hook-registry", + "socket2 0.4.4", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite 0.2.8", + "tokio 1.20.1", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.20.1", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio 1.20.1", + "webpki", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio 1.20.1", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.8", + "tokio 1.20.1", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio 1.20.1", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.12", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.2.8", + "tokio 1.20.1", +] + +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.2.8", + "tokio 1.20.1", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796c5e1cd49905e65dd8e700d4cb1dffcbfdb4fc9d017de08c1a537afd83627c" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes 1.1.0", + "futures-core", + "futures-util", + "h2 0.3.11", + "http", + "http-body 0.4.4", + "hyper 0.14.18", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost 0.8.0", + "prost-derive 0.8.0", + "tokio 1.20.1", + "tokio-rustls", + "tokio-stream", + "tokio-util 0.6.9", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes 1.1.0", + "futures-core", + "futures-util", + "h2 0.3.11", + "http", + "http-body 0.4.4", + "hyper 0.14.18", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost 0.9.0", + "prost-derive 0.9.0", + "tokio 1.20.1", + "tokio-rustls", + "tokio-stream", + "tokio-util 0.6.9", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "torut" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" +dependencies = [ + "base32", + "base64", + "derive_more", + "ed25519-dalek", + "hex", + "hmac", + "rand 0.7.3", + "serde", + "serde_derive", + "sha2", + "sha3", + "tokio 1.20.1", +] + +[[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite 0.2.8", + "rand 0.8.5", + "slab", + "tokio 1.20.1", + "tokio-util 0.7.0", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.8", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "triggered" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce148eae0d1a376c1b94ae651fc3261d9cb8294788b962b7382066376503a2d1" + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64", + "byteorder", + "bytes 1.1.0", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha-1 0.9.8", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" +dependencies = [ + "bytes 1.1.0", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper 0.14.18", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 1.20.1", + "tokio-stream", + "tokio-tungstenite", + "tokio-util 0.6.9", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +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.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "watchtower-plugin" +version = "0.1.2" +dependencies = [ + "backoff", + "bitcoin", + "cln-plugin", + "hex", + "home", + "httpmock", + "log", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "tempdir", + "teos-common", + "tokio 1.20.1", + "tonic 0.5.2", +] + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "which" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" +dependencies = [ + "either", + "lazy_static", + "libc", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[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-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[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-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.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "x509-parser" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc90836a84cb72e6934137b1504d0cae304ef5d83904beb0c8d773bbfe256ed" +dependencies = [ + "base64", + "chrono", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", +] + +[[package]] +name = "yasna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" +dependencies = [ + "chrono", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 8b1c12fe..4efa2041 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -19,7 +19,6 @@ You can get Bitcoin Core from [bitcoincore.org](https://bitcoincore.org/en/downl Bitcoin needs to be running with the following options enabled: -- `txindex` to be able to look for non-wallet transactions - `server` to run rpc commands Here's an example of a `bitcoin.conf` you can use for mainnet. **DO NOT USE THE PROVIDED RPC USER AND PASSWORD.** @@ -31,9 +30,6 @@ rpcuser=user rpcpassword=passwd rpcservertimeout=600 -# [blockchain] -txindex=1 - # [others] daemon=1 debug=1 diff --git a/INSTALL.md b/INSTALL.md index 24140d59..35d422f3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,9 +3,9 @@ The tower can be installed and tested using cargo: ``` -git clone https://github.com/sr-gi/rust-teos.git +git clone https://github.com/talaia-labs/rust-teos.git cd rust-teos -cargo install --path teos +cargo install --locked --path teos ``` You can run tests with: diff --git a/README.md b/README.md index 7a6ad481..fa7d4b00 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ btc_network = regtest ### Running `teosd` with tor -This requires a tor daemon running on the same machine as `teosd` and a control port open on that daemon. +This requires a Tor daemon running on the same machine as `teosd` and a control port open on that daemon. -Download tor from the [torproject site](https://www.torproject.org/download/). +Download Tor from the [torproject site](https://www.torproject.org/download/). -To open tor's control port, you add the following to the tor config file ([source](https://2019.www.torproject.org/docs/faq.html.en#torrc)): +To open tor's control port, you add the following to the Tor config file ([source](https://2019.www.torproject.org/docs/faq.html.en#torrc)): ``` ## The port on which Tor will listen for local connections from Tor @@ -85,7 +85,7 @@ CookieAuthentication 1 CookieAuthFileGroupReadable 1 ``` -Once the tor daemon is running, and the control port is open, make sure to enable the `tor_support` flag `teosd`. +Once the Tor daemon is running, and the control port is open, make sure to enable `--torsupport` when running `teosd`. ### Tower id and signing key diff --git a/teos-common/Cargo.toml b/teos-common/Cargo.toml index 49d8b6c6..6c10fbe1 100644 --- a/teos-common/Cargo.toml +++ b/teos-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "teos-common" -version = "0.0.1" +version = "0.1.2" authors = ["Sergi Delgado Segura "] edition = "2018" diff --git a/teos-common/build.rs b/teos-common/build.rs index c132feb7..a66e76f1 100644 --- a/teos-common/build.rs +++ b/teos-common/build.rs @@ -6,7 +6,14 @@ fn main() -> Result<(), Box> { .field_attribute("appointment_data", "#[serde(rename = \"appointment\")]") .field_attribute("user_id", "#[serde(with = \"hex::serde\")]") .field_attribute("locator", "#[serde(with = \"hex::serde\")]") + .field_attribute( + "locators", + "#[serde(with = \"crate::ser::serde_vec_bytes\")]", + ) .field_attribute("encrypted_blob", "#[serde(with = \"hex::serde\")]") + .field_attribute("dispute_txid", "#[serde(with = \"crate::ser::serde_be\")]") + .field_attribute("penalty_txid", "#[serde(with = \"crate::ser::serde_be\")]") + .field_attribute("penalty_rawtx", "#[serde(with = \"hex::serde\")]") .field_attribute( "GetAppointmentResponse.status", "#[serde(with = \"crate::ser::serde_status\")]", diff --git a/teos-common/src/lib.rs b/teos-common/src/lib.rs index 009e3464..287ffffb 100644 --- a/teos-common/src/lib.rs +++ b/teos-common/src/lib.rs @@ -2,6 +2,8 @@ //! //! Functionality shared between users and towers. +// FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661 +#[allow(clippy::derive_partial_eq_without_eq)] pub mod protos { tonic::include_proto!("common.teos.v2"); } diff --git a/teos-common/src/ser.rs b/teos-common/src/ser.rs index 84879d29..4946ba3b 100644 --- a/teos-common/src/ser.rs +++ b/teos-common/src/ser.rs @@ -15,9 +15,98 @@ where seq.end() } +pub mod serde_be { + use super::*; + use serde::de::{self, Deserializer}; + + pub fn serialize(v: &[u8], s: S) -> Result + where + S: Serializer, + { + let mut v = v.to_owned(); + v.reverse(); + hex::serialize(v, s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct BEVisitor; + + impl<'de> de::Visitor<'de> for BEVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a hex encoded string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let mut v = + hex::decode(v).map_err(|_| E::custom("cannot deserialize the given value"))?; + v.reverse(); + Ok(v) + } + } + + deserializer.deserialize_any(BEVisitor) + } +} + +pub mod serde_vec_bytes { + use super::*; + use serde::de::{self, Deserializer, SeqAccess}; + + pub fn serialize(v: &[Vec], s: S) -> Result + where + S: Serializer, + { + let mut seq = s.serialize_seq(Some(v.len()))?; + for element in v.iter() { + seq.serialize_element(&hex::encode(element))?; + } + seq.end() + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + struct VecVisitor; + + impl<'de> de::Visitor<'de> for VecVisitor { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a hex encoded string") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut result = Vec::new(); + while let Some(v) = seq.next_element::()? { + result + .push(hex::decode(v).map_err(|_| { + de::Error::custom("cannot deserialize the given value") + })?); + } + + Ok(result) + } + } + + deserializer.deserialize_any(VecVisitor) + } +} + pub mod serde_status { + use super::*; use serde::de::{self, Deserializer}; - use serde::ser::Serializer; use std::str::FromStr; use crate::appointment::AppointmentStatus; diff --git a/teos/Cargo.toml b/teos/Cargo.toml index c03291a2..4f25c94b 100644 --- a/teos/Cargo.toml +++ b/teos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "teos" -version = "0.0.1" +version = "0.1.2" authors = ["Sergi Delgado Segura "] license = "MIT" edition = "2018" @@ -47,7 +47,7 @@ teos-common = { path = "../teos-common" } tonic-build = "0.6" [dev-dependencies] -chunked_transfer = "1.4" -rand = "0.8.4" jsonrpc-http-server = "17.1.0" +rand = "0.8.4" +tempdir = "0.3.7" tokio-stream = { version = "0.1.5", features = [ "net" ] } diff --git a/teos/build.rs b/teos/build.rs index 56482d4e..0bd14775 100644 --- a/teos/build.rs +++ b/teos/build.rs @@ -6,11 +6,11 @@ fn main() -> Result<(), Box> { .field_attribute("tower_id", "#[serde(with = \"hex::serde\")]") .field_attribute( "user_ids", - "#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]", + "#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]", ) .field_attribute( "GetUserResponse.appointments", - "#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]", + "#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]", ) .compile( &[ diff --git a/teos/src/api/http.rs b/teos/src/api/http.rs index 177f8c66..94250458 100644 --- a/teos/src/api/http.rs +++ b/teos/src/api/http.rs @@ -1,10 +1,10 @@ -use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use std::convert::Infallible; use std::error::Error; use std::net::SocketAddr; use tokio::time::Duration; use tonic::transport::Channel; -use triggered::Listener; +use triggered::{Listener, Trigger}; use warp::{http::StatusCode, reject, reply, Filter, Rejection, Reply}; use teos_common::appointment::LOCATOR_LEN; @@ -58,17 +58,6 @@ impl ApiError { } } -pub fn serialize_vec_bytes(v: &[Vec], s: S) -> Result -where - S: Serializer, -{ - let mut seq = s.serialize_seq(Some(v.len()))?; - for element in v.iter() { - seq.serialize_element(&hex::encode(element))?; - } - seq.end() -} - fn with_grpc( grpc_endpoint: PublicTowerServicesClient, ) -> impl Filter,), Error = Infallible> + Clone { @@ -299,7 +288,12 @@ async fn handle_rejection(err: Rejection) -> Result { } } -pub async fn serve(http_bind: SocketAddr, grpc_bind: String, shutdown_signal: Listener) { +pub async fn serve( + http_bind: SocketAddr, + grpc_bind: String, + service_ready: Trigger, + shutdown_signal: Listener, +) { let grpc_conn = loop { match PublicTowerServicesClient::connect(grpc_bind.clone()).await { Ok(conn) => break conn, @@ -309,8 +303,9 @@ pub async fn serve(http_bind: SocketAddr, grpc_bind: String, shutdown_signal: Li } } }; - let (_, server) = warp::serve(router(grpc_conn)) - .bind_with_graceful_shutdown(http_bind, async { shutdown_signal.await }); + let (_, server) = + warp::serve(router(grpc_conn)).bind_with_graceful_shutdown(http_bind, shutdown_signal); + service_ready.trigger(); server.await } @@ -326,7 +321,7 @@ mod test_helpers { use crate::api::internal::InternalAPI; use crate::protos::public_tower_services_server::PublicTowerServicesServer; - use crate::test_utils::{create_api_with_config, ApiConfig}; + use crate::test_utils::{create_api_with_config, ApiConfig, BitcoindStopper}; pub(crate) enum RequestBody<'a> { Jsonify(&'a str), @@ -337,8 +332,8 @@ mod test_helpers { pub(crate) async fn run_tower_in_background_with_config( api_config: ApiConfig, - ) -> (SocketAddr, Arc) { - let internal_rpc_api = create_api_with_config(api_config).await; + ) -> (SocketAddr, Arc, BitcoindStopper) { + let (internal_rpc_api, bitcoind_stopper) = create_api_with_config(api_config).await; let cloned = internal_rpc_api.clone(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -352,13 +347,14 @@ mod test_helpers { .unwrap(); }); - (addr, cloned) + (addr, cloned, bitcoind_stopper) } - pub(crate) async fn run_tower_in_background() -> SocketAddr { - run_tower_in_background_with_config(ApiConfig::default()) - .await - .0 + pub(crate) async fn run_tower_in_background() -> (SocketAddr, BitcoindStopper) { + let (sock_addr, _, bitcoind_stopper) = + run_tower_in_background_with_config(ApiConfig::default()).await; + + (sock_addr, bitcoind_stopper) } pub(crate) async fn check_api_error<'a>( @@ -430,7 +426,7 @@ mod tests_failures { #[tokio::test] async fn test_no_json_request_body() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error("/register", RequestBody::Body(""), server_addr).await; assert!(api_error.error.contains("EOF while parsing")); @@ -440,7 +436,7 @@ mod tests_failures { #[tokio::test] async fn test_wrong_json_request_body() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error("/register", RequestBody::DoNotJsonify(""), server_addr).await; assert!(api_error.error.contains("expected struct")); @@ -450,7 +446,7 @@ mod tests_failures { #[tokio::test] async fn test_empty_json_request_body() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error("/register", RequestBody::Jsonify(r#"{}"#), server_addr).await; assert!(api_error.error.contains("missing field")); @@ -460,7 +456,7 @@ mod tests_failures { #[tokio::test] async fn test_empty_field() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error( "/register", RequestBody::Jsonify(r#"{"user_id": ""}"#), @@ -474,7 +470,7 @@ mod tests_failures { #[tokio::test] async fn test_wrong_field_hex_encoding_odd() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error( "/register", RequestBody::Jsonify(r#"{"user_id": "a"}"#), @@ -488,7 +484,7 @@ mod tests_failures { #[tokio::test] async fn test_wrong_hex_encoding_character() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error("/register", RequestBody::Jsonify(r#"{"user_id": "022fa2900ed7fc07b4e8ca3ea081e846245b0497944644aa78ea0b994ac22074dZ"}"#), @@ -502,7 +498,7 @@ mod tests_failures { #[tokio::test] async fn test_wrong_field_size() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error( "/register", RequestBody::Jsonify(r#"{"user_id": "aa"}"#), @@ -517,7 +513,7 @@ mod tests_failures { #[tokio::test] async fn test_wrong_field_type() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error( "/register", RequestBody::DoNotJsonify(r#"{"user_id": 1}"#), @@ -532,7 +528,7 @@ mod tests_failures { #[tokio::test] async fn test_request_missing_field() { // We'll use a different endpoint here since we need a json object with more than one field - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let (api_error, status) = check_api_error( "/add_appointment", RequestBody::Jsonify(r#"{"signature": "aa"}"#), @@ -548,7 +544,7 @@ mod tests_failures { #[tokio::test] async fn test_empty_request_body() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -563,12 +559,12 @@ mod tests_failures { .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED) + assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED); } #[tokio::test] async fn test_payload_too_large() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -584,12 +580,12 @@ mod tests_failures { .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE) + assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); } #[tokio::test] async fn test_wrong_endpoint() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -605,12 +601,12 @@ mod tests_failures { .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::NOT_FOUND) + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_wrong_method() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -625,7 +621,7 @@ mod tests_failures { .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED) + assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); } } @@ -645,7 +641,7 @@ mod tests_methods { #[tokio::test] async fn test_register() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; let response = request_to_api::( "/register", @@ -660,7 +656,7 @@ mod tests_methods { #[tokio::test] async fn test_register_max_slots() { - let (server_addr, _) = + let (server_addr, _, _s) = run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await; let user_id = get_random_user_id(); @@ -697,7 +693,7 @@ mod tests_methods { #[tokio::test] async fn test_register_service_unavailable() { - let (server_addr, _) = run_tower_in_background_with_config( + let (server_addr, _, _s) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; @@ -725,7 +721,7 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); @@ -764,8 +760,8 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment_non_registered() { - let server_addr = run_tower_in_background().await; - let (user_sk, _) = cryptography::get_random_keypair(); + let (server_addr, _s) = run_tower_in_background().await; + let (user_sk, _s) = cryptography::get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); @@ -792,7 +788,7 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment_already_triggered() { // Get the InternalAPI so we can mess with the inner state - let (server_addr, internal_api) = + let (server_addr, internal_api, _s) = run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await; // Register @@ -837,7 +833,7 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment_service_unavailable() { - let (server_addr, _) = run_tower_in_background_with_config( + let (server_addr, _, _s) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; @@ -867,7 +863,7 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); @@ -922,7 +918,7 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment_non_registered() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; // User is not registered let (user_sk, _) = cryptography::get_random_keypair(); @@ -955,7 +951,7 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment_not_found() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); @@ -998,7 +994,7 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment_service_unavailable() { - let (server_addr, _) = run_tower_in_background_with_config( + let (server_addr, _, _s) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; @@ -1033,7 +1029,7 @@ mod tests_methods { #[tokio::test] async fn test_get_subscription_info() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); @@ -1069,7 +1065,7 @@ mod tests_methods { #[tokio::test] async fn test_get_subscription_info_non_registered() { - let server_addr = run_tower_in_background().await; + let (server_addr, _s) = run_tower_in_background().await; // User is not registered let (user_sk, _) = cryptography::get_random_keypair(); @@ -1097,7 +1093,7 @@ mod tests_methods { #[tokio::test] async fn test_get_subscription_info_service_unavailable() { let (user_sk, _) = cryptography::get_random_keypair(); - let (server_addr, _) = run_tower_in_background_with_config( + let (server_addr, _, _s) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; diff --git a/teos/src/api/internal.rs b/teos/src/api/internal.rs index 5c48beef..e79c4273 100644 --- a/teos/src/api/internal.rs +++ b/teos/src/api/internal.rs @@ -394,7 +394,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_all_appointments() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let response = internal_api .get_all_appointments(Request::new(())) @@ -402,12 +402,12 @@ mod tests_private_api { .unwrap() .into_inner(); - assert!(matches!(response, msgs::GetAllAppointmentsResponse { .. })) + assert!(matches!(response, msgs::GetAllAppointmentsResponse { .. })); } #[tokio::test] async fn test_get_all_appointments_watcher() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // Add data to the Watcher so we can retrieve it later on let (user_sk, user_pk) = get_random_keypair(); @@ -435,7 +435,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_all_appointments_responder() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // Add data to the Responser so we can retrieve it later on internal_api @@ -457,7 +457,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_appointments() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let locator = Locator::new(get_random_tx().txid()).to_vec(); let response = internal_api @@ -471,7 +471,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_appointments_watcher() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; for i in 0..3 { // Create a dispute tx to be used for creating different dummy appointments with the same locator. @@ -521,7 +521,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_appointments_responder() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; for i in 0..3 { // Create a dispute tx to be used for creating different trackers. @@ -572,7 +572,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_tower_info_empty() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let response = internal_api .get_tower_info(Request::new(())) @@ -588,7 +588,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_tower_info() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // Register a user let (user_sk, user_pk) = get_random_keypair(); @@ -627,7 +627,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_users() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let mut users = HashSet::new(); // Add a couple of users @@ -649,7 +649,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_users_empty() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let response = internal_api .get_users(Request::new(())) @@ -662,7 +662,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_user() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // Register a user and get it back let (user_sk, user_pk) = get_random_keypair(); @@ -705,7 +705,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_user_not_found() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // Non-registered user let (_, user_pk) = get_random_keypair(); @@ -726,7 +726,7 @@ mod tests_private_api { #[tokio::test] async fn test_stop() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; assert!(!internal_api.shutdown_trigger.is_triggered()); internal_api.stop(Request::new(())).await.unwrap(); @@ -746,7 +746,7 @@ mod tests_public_api { #[tokio::test] async fn test_register() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let (_, user_pk) = get_random_keypair(); @@ -766,7 +766,7 @@ mod tests_public_api { #[tokio::test] async fn test_register_wrong_user_id() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let mut user_ids = Vec::new(); @@ -797,7 +797,7 @@ mod tests_public_api { #[tokio::test] async fn test_register_max_slots() { - let internal_api = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await; + let (internal_api, _s) = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await; let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk).to_vec(); @@ -825,7 +825,7 @@ mod tests_public_api { #[tokio::test] async fn test_register_service_unavailable() { - let internal_api = + let (internal_api, _s) = create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await; let (_, user_pk) = get_random_keypair(); @@ -845,7 +845,7 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // User must be registered let (user_sk, user_pk) = get_random_keypair(); @@ -871,7 +871,7 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_non_registered() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // User is not registered this time let (user_sk, _) = get_random_keypair(); @@ -899,7 +899,7 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_not_enough_slots() { - let internal_api = create_api_with_config(ApiConfig::new(0, DURATION)).await; + let (internal_api, _s) = create_api_with_config(ApiConfig::new(0, DURATION)).await; // User is registered but has no slots let (user_sk, user_pk) = get_random_keypair(); @@ -928,7 +928,7 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_subscription_expired() { - let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // User is registered but subscription is expired let (user_sk, user_pk) = get_random_keypair(); @@ -954,7 +954,7 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_already_triggered() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); @@ -985,7 +985,7 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_service_unavailable() { - let internal_api = + let (internal_api, _s) = create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await; let (user_sk, _) = get_random_keypair(); @@ -1009,7 +1009,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // The user must be registered let (user_sk, user_pk) = get_random_keypair(); @@ -1037,12 +1037,12 @@ mod tests_public_api { assert!(matches!( response, common_msgs::GetAppointmentResponse { .. } - )) + )); } #[tokio::test] async fn test_get_appointment_non_registered() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // Add a first user to link the appointment to him let (user_sk, user_pk) = get_random_keypair(); @@ -1070,7 +1070,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment_non_existent() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // The user is registered but the appointment does not exist let (user_sk, user_pk) = get_random_keypair(); @@ -1097,7 +1097,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment_subscription_expired() { - let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // Register the user let (user_sk, user_pk) = get_random_keypair(); @@ -1125,7 +1125,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment_service_unavailable() { - let internal_api = + let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await; let (user_sk, _) = get_random_keypair(); @@ -1148,7 +1148,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_subscription_info() { - let internal_api = create_api().await; + let (internal_api, _s) = create_api().await; // The user must be registered let (user_sk, user_pk) = get_random_keypair(); @@ -1167,12 +1167,12 @@ mod tests_public_api { assert!(matches!( response, common_msgs::GetSubscriptionInfoResponse { .. } - )) + )); } #[tokio::test] async fn test_get_subscription_info_non_registered() { - let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // The user is not registered let (user_sk, _) = get_random_keypair(); @@ -1195,7 +1195,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_subscription_info_expired() { - let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // The user is registered but the subscription has expired let (user_sk, user_pk) = get_random_keypair(); @@ -1219,7 +1219,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_subscription_info_service_unavailable() { - let internal_api = + let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await; let (user_sk, _) = get_random_keypair(); diff --git a/teos/src/api/tor.rs b/teos/src/api/tor.rs index 9d821d3c..55c7cfea 100644 --- a/teos/src/api/tor.rs +++ b/teos/src/api/tor.rs @@ -1,16 +1,44 @@ +use std::convert::TryInto; use std::io::{Error, ErrorKind}; use std::net::SocketAddr; +use std::path::PathBuf; + +use tokio::fs; use tokio::net::TcpStream; use tokio::time::{sleep, Duration}; use torut::control::UnauthenticatedConn; use torut::onion::TorSecretKeyV3; -use triggered::Listener; +use triggered::{Listener, Trigger}; + +/// Loads a Tor key from disk (if found). +async fn load_tor_key(path: PathBuf) -> Option { + log::info!("Loading Tor secret key from disk"); + let key = fs::read(path.join("onion_v3_sk")) + .await + .map_err(|e| log::warn!("Tor secret key cannot be loaded. {}", e)) + .ok()?; + let key: [u8; 64] = key + .try_into() + .map_err(|_| log::error!("Cannot convert loaded data into Tor secret key")) + .ok()?; + + Some(TorSecretKeyV3::from(key)) +} + +/// Stores a Tor key to disk. +async fn store_tor_key(key: &TorSecretKeyV3, path: PathBuf) { + if let Err(e) = fs::write(path.join("onion_v3_sk"), key.as_bytes()).await { + log::error!("Cannot store Tor secret key. {}", e); + } +} /// Expose an onion service that re-directs to the public api. pub async fn expose_onion_service( tor_control_port: u16, - api_port: u16, + api_endpoint: SocketAddr, onion_port: u16, + path: PathBuf, + service_ready: Trigger, shutdown_signal_tor: Listener, ) -> Result<(), Error> { let stream = connect_tor_cp(format!("127.0.0.1:{}", tor_control_port).parse().unwrap()) @@ -39,7 +67,14 @@ pub async fn expose_onion_service( auth_conn.set_async_event_handler(Some(|_| async move { Ok(()) })); - let key = TorSecretKeyV3::generate(); + let key = if let Some(key) = load_tor_key(path.clone()).await { + key + } else { + log::info!("Generating fresh Tor secret key"); + let key = TorSecretKeyV3::generate(); + store_tor_key(&key, path).await; + key + }; auth_conn .add_onion_v3( @@ -48,11 +83,7 @@ pub async fn expose_onion_service( false, false, None, - &mut [( - onion_port, - format!("127.0.0.1:{}", api_port).parse().unwrap(), - )] - .iter(), + &mut [(onion_port, api_endpoint)].iter(), ) .await .map_err(|e| { @@ -63,6 +94,7 @@ pub async fn expose_onion_service( })?; print_onion_service(key.clone(), onion_port); + service_ready.trigger(); // NOTE: Needed to keep connection with control port & hidden service running, as soon as we leave // this function the control port stream is dropped and the hidden service is killed @@ -88,7 +120,7 @@ async fn connect_tor_cp(addr: SocketAddr) -> Result { let sock = TcpStream::connect(addr).await.map_err(|_| { Error::new( ErrorKind::ConnectionRefused, - "failed to connect to tor control port", + "failed to connect to Tor control port", ) })?; Ok(sock) @@ -97,12 +129,45 @@ async fn connect_tor_cp(addr: SocketAddr) -> Result { fn print_onion_service(key: TorSecretKeyV3, onion_port: u16) { let onion_addr = key.public().get_onion_address(); let onion = format!("{}:{}", onion_addr, onion_port); - log::info!("onion service: {}", onion); + log::info!("Onion service: {}", onion); } #[cfg(test)] mod tests { use super::*; + use tempdir::TempDir; + + use teos_common::test_utils::get_random_user_id; + + #[tokio::test] + async fn test_store_load_key() { + let key = TorSecretKeyV3::generate(); + let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); + + store_tor_key(&key, tmp_path.path().into()).await; + let loaded_key = load_tor_key(tmp_path.path().into()).await; + + assert_eq!(key, loaded_key.unwrap()) + } + + #[tokio::test] + async fn test_load_key_inexistent() { + let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); + let loaded_key = load_tor_key(tmp_path.path().into()).await; + + assert_eq!(loaded_key, None); + } + + #[tokio::test] + async fn test_load_key_wrong_format() { + let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); + fs::write(tmp_path.path().join("onion_v3_sk"), "random stuff") + .await + .unwrap(); + let loaded_key = load_tor_key(tmp_path.path().into()).await; + + assert_eq!(loaded_key, None); + } #[tokio::test] async fn test_connect_tor_cp_fail() { @@ -111,7 +176,7 @@ mod tests { match connect_tor_cp(addr).await { Ok(_) => {} Err(e) => { - assert_eq!("failed to connect to tor control port", e.to_string()) + assert_eq!("failed to connect to Tor control port", e.to_string()) } } } diff --git a/teos/src/bitcoin_cli.rs b/teos/src/bitcoin_cli.rs index 8f10ea58..ee6bb99b 100644 --- a/teos/src/bitcoin_cli.rs +++ b/teos/src/bitcoin_cli.rs @@ -9,6 +9,8 @@ * at your option. */ +use std::convert::TryInto; +use std::io::{Error, ErrorKind}; use std::sync::Arc; use tokio::sync::Mutex; @@ -17,7 +19,7 @@ use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::hex::ToHex; use bitcoin::{Block, Transaction}; use lightning::util::ser::Writeable; -use lightning_block_sync::http::HttpEndpoint; +use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; use lightning_block_sync::rpc::RpcClient; use lightning_block_sync::{AsyncBlockSourceResult, BlockHeaderData, BlockSource}; @@ -74,6 +76,7 @@ impl<'a> BitcoindClient<'a> { port: u16, rpc_user: &'a str, rpc_password: &'a str, + teos_network: &'a str, ) -> std::io::Result> { let http_endpoint = HttpEndpoint::for_host(host.to_owned()).with_port(port); let rpc_credentials = base64::encode(&format!("{}:{}", rpc_user, rpc_password)); @@ -87,10 +90,20 @@ impl<'a> BitcoindClient<'a> { rpc_password, }; - // Test that bitcoind is reachable - match client.get_best_block_hash_and_height().await { - Ok(_) => Ok(client), - Err(e) => Err(e), + // Test that bitcoind is reachable. + let btc_network = client.get_chain().await?; + + // Assert teos runs on the same chain/network as bitcoind. + if btc_network != teos_network { + Err(Error::new( + ErrorKind::InvalidInput, + format!( + "bitcoind is running on {} but teosd is set to run on {}", + btc_network, teos_network + ), + )) + } else { + Ok(client) } } @@ -127,4 +140,24 @@ impl<'a> BitcoindClient<'a> { rpc.call_method::("getrawtransaction", &[txid_hex]) .await } + + /// Gets bitcoind's network. + pub async fn get_chain(&self) -> std::io::Result { + // A wrapper type to extract "chain" key from getblockchaininfo JsonResponse. + struct BtcNetwork(String); + impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(BtcNetwork(self.0["chain"].as_str().unwrap().to_string())) + } + } + + // Ask the RPC client for the network bitcoind is running on. + let rpc = self.bitcoind_rpc_client.lock().await; + let btc_network = rpc + .call_method::("getblockchaininfo", &[]) + .await?; + + Ok(btc_network.0) + } } diff --git a/teos/src/carrier.rs b/teos/src/carrier.rs index 3deeb05e..6b576e14 100644 --- a/teos/src/carrier.rs +++ b/teos/src/carrier.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Condvar, Mutex}; use crate::responder::ConfirmationStatus; use crate::{errors, rpc_errors}; -use bitcoin::{BlockHash, Transaction, Txid}; +use bitcoin::{Transaction, Txid}; use bitcoincore_rpc::{ jsonrpc::error::Error::Rpc as RpcError, jsonrpc::error::Error::Transport as TransportError, Client as BitcoindClient, Error::JsonRpc as JsonRpcError, RpcApi, @@ -22,7 +22,7 @@ pub struct Carrier { /// A map of receipts already issued by the [Carrier]. /// Used to prevent potentially re-sending the same transaction over and over. issued_receipts: HashMap, - /// The last known block header. + /// The last known block height. block_height: u32, } @@ -41,6 +41,11 @@ impl Carrier { } } + /// The last known block height. + pub(crate) fn block_height(&self) -> u32 { + self.block_height + } + /// Clears the receipts cached by the [Carrier]. Should be called periodically to prevent it from /// growing unbounded. pub(crate) fn clear_receipts(&mut self) { @@ -100,11 +105,14 @@ impl Carrier { } rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN => { log::info!( - "Transaction is already in the blockchain: {}. Getting confirmation count", + "Transaction was confirmed long ago, not keeping track of it: {}", tx.txid() ); - ConfirmationStatus::ConfirmedIn(self.get_tx_height(&tx.txid()).unwrap()) + // Given we are not using txindex, if a transaction bounces we cannot get its confirmation count. However, [send_transaction] is guarded by + // checking whether the transaction id can be found in the [Responder]'s [TxIndex], meaning that if the transaction bounces it was confirmed long + // ago (> IRREVOCABLY_RESOLVED), so we don't need to worry about it. + ConfirmationStatus::IrrevocablyResolved } rpc_errors::RPC_DESERIALIZATION_ERROR => { // Adding this here just for completeness. We should never end up here. The Carrier only sends txs handed by the Responder, @@ -139,77 +147,44 @@ impl Carrier { receipt } - /// Gets the block height at where a given [Transaction] was confirmed at (if any). - fn get_tx_height(&self, txid: &Txid) -> Option { - if let Some(block_hash) = self.get_block_hash_for_tx(txid) { - self.get_block_height(&block_hash) - } else { - None - } - } - - /// Queries the height of a given [Block](bitcoin::Block). Returns it if the block can be found. Returns [None] otherwise. - fn get_block_height(&self, block_hash: &BlockHash) -> Option { - self.hang_until_bitcoind_reachable(); - - match self.bitcoin_cli.get_block_header_info(block_hash) { - Ok(header_data) => Some(header_data.height as u32), - Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code { - rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => { - log::info!("Block not found: {}", block_hash); - None - } - e => { - log::error!("Unexpected error code when calling getblockheader: {}", e); - None - } - }, - Err(JsonRpcError(TransportError(_))) => { - // Connection refused, bitcoind is down. - log::error!("Connection lost with bitcoind, retrying request when possible"); - self.flag_bitcoind_unreachable(); - self.get_block_height(block_hash) - } - // TODO: This may need finer catching. - Err(e) => { - log::error!("Unexpected JSONRPCError when calling getblockheader: {}", e); - None - } - } - } - - /// Gets the block hash where a given [Transaction] was confirmed at (if any). - pub(crate) fn get_block_hash_for_tx(&self, txid: &Txid) -> Option { + /// Checks whether a given transaction can be found in the mempool. + /// + /// This uses `getrawtransaction` under the hood and, therefore, its behavior depends on whether `txindex` is enabled in bitcoind. + /// If `txindex` is disabled (default), it will only pull data from the mempool. Otherwise, it will also pull data from the transaction + /// index. Hence, we need to check whether the returned struct has any of the block related datum set (such as `blockhash`). + pub(crate) fn in_mempool(&self, txid: &Txid) -> bool { self.hang_until_bitcoind_reachable(); match self.bitcoin_cli.get_raw_transaction_info(txid, None) { - Ok(tx_data) => tx_data.blockhash, + Ok(tx) => tx.blockhash.is_none(), Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code { rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => { - log::info!("Transaction not found in mempool nor blockchain: {}", txid); - None + log::info!("Transaction not found in mempool: {}", txid); + false } e => { + // DISCUSS: This could result in a silent error with unknown consequences log::error!( "Unexpected error code when calling getrawtransaction: {}", e ); - None + false } }, Err(JsonRpcError(TransportError(_))) => { // Connection refused, bitcoind is down. log::error!("Connection lost with bitcoind, retrying request when possible"); self.flag_bitcoind_unreachable(); - self.get_block_hash_for_tx(txid) + self.in_mempool(txid) } // TODO: This may need finer catching. Err(e) => { + // DISCUSS: This could result in a silent error with unknown consequences log::error!( "Unexpected JSONRPCError when calling getrawtransaction: {}", e ); - None + false } } } @@ -221,7 +196,7 @@ mod tests { use std::thread; use crate::test_utils::{get_random_tx, start_server, BitcoindMock, MockOptions, START_HEIGHT}; - use teos_common::test_utils::TX_HEX; + use teos_common::test_utils::{TXID_HEX, TX_HEX}; use bitcoin::consensus; use bitcoin::hashes::hex::FromHex; @@ -241,11 +216,10 @@ mod tests { #[test] fn test_clear_receipts() { - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); @@ -265,11 +239,29 @@ mod tests { #[test] fn test_send_transaction_ok() { - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); + let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); + let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock.server); + + let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); + let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let r = carrier.send_transaction(&tx); + + assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height)); + + // Check the receipt is on the cache + assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + } + + #[test] + fn test_send_transaction_ok_already_in_mempool() { + let bitcoind_mock = BitcoindMock::new(MockOptions::in_mempool()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); @@ -289,7 +281,7 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); @@ -311,7 +303,7 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); @@ -328,21 +320,19 @@ mod tests { #[test] fn test_send_transaction_verify_already_in_chain() { - let bitcoind_mock = BitcoindMock::new(MockOptions::new( + let bitcoind_mock = BitcoindMock::new(MockOptions::with_error( rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN as i64, - BlockHash::default(), - START_HEIGHT, )); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let r = carrier.send_transaction(&tx); - assert_eq!(r, ConfirmationStatus::ConfirmedIn(start_height)); + assert_eq!(r, ConfirmationStatus::IrrevocablyResolved); // Check the receipt is on the cache assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); @@ -355,7 +345,7 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); @@ -373,7 +363,7 @@ mod tests { #[test] fn test_send_transaction_connection_error() { // Try to connect to an offline bitcoind. - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); let bitcoind_reachable = Arc::new((Mutex::new(false), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; @@ -400,91 +390,86 @@ mod tests { } #[test] - fn test_get_tx_height_ok() { - let target_height = 21; - let bitcoind_mock = - BitcoindMock::new(MockOptions::with_block(BlockHash::default(), target_height)); + fn test_in_mempool() { + let bitcoind_mock = BitcoindMock::new(MockOptions::in_mempool()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); - assert_eq!( - carrier.get_tx_height(&tx.txid()), - Some(target_height as u32) - ); + let txid = Txid::from_hex(TXID_HEX).unwrap(); + assert!(carrier.in_mempool(&txid)); } #[test] - fn test_get_tx_height_not_found() { - // Hee we are not testing the case where the block hash is unknown (which will also return None). This is because we only - // learn block hashes from bitcoind, and once a block is known, it cannot disappear (ir can be disconnected, but not banish). - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + fn test_not_in_mempool() { + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); - assert_eq!(carrier.get_tx_height(&tx.txid()), None); + let txid = Txid::from_hex(TXID_HEX).unwrap(); + assert!(!carrier.in_mempool(&txid)); } #[test] - fn test_get_block_height_ok() { - let target_height = 21; - let block_hash = BlockHash::default(); - let bitcoind_mock = BitcoindMock::new(MockOptions::with_block(block_hash, target_height)); + fn test_not_in_mempool_via_error() { + let bitcoind_mock = BitcoindMock::new(MockOptions::with_error( + rpc_errors::RPC_INVALID_ADDRESS_OR_KEY as i64, + )); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - assert_eq!( - carrier.get_block_height(&block_hash), - Some(target_height as u32) - ); + let txid = Txid::from_hex(TXID_HEX).unwrap(); + assert!(!carrier.in_mempool(&txid)); } #[test] - fn test_get_block_height_not_found() { - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + fn test_in_mempool_unexpected_error() { + let bitcoind_mock = + BitcoindMock::new(MockOptions::with_error(rpc_errors::RPC_MISC_ERROR as i64)); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - assert_eq!(carrier.get_block_height(&BlockHash::default()), None); + let txid = Txid::from_hex(TXID_HEX).unwrap(); + assert!(!carrier.in_mempool(&txid)); } #[test] - fn test_get_block_hash_for_tx_ok() { - let block_hash = BlockHash::default(); - let bitcoind_mock = BitcoindMock::new(MockOptions::with_block(block_hash, 21)); - let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + fn test_in_mempool_connection_error() { + // Try to connect to an offline bitcoind. + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); + let bitcoind_reachable = Arc::new((Mutex::new(false), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable.clone(), start_height); - let tx = consensus::deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - assert_eq!(carrier.get_block_hash_for_tx(&tx.txid()), Some(block_hash)); - } + let txid = Txid::from_hex(TXID_HEX).unwrap(); + let delay = std::time::Duration::new(3, 0); - #[test] - fn test_get_block_hash_for_tx_not_found() { - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); - let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); - let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock); + thread::spawn(move || { + thread::sleep(delay); + let (reachable, notifier) = &*bitcoind_reachable; + *reachable.lock().unwrap() = true; + notifier.notify_all(); + }); - let tx = consensus::deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - assert_eq!(carrier.get_block_hash_for_tx(&tx.txid()), None); + let before = std::time::Instant::now(); + carrier.in_mempool(&txid); + + // Check the request has hanged for ~delay + assert_eq!( + (std::time::Instant::now() - before).as_secs(), + delay.as_secs() + ); } } diff --git a/teos/src/cli_config.rs b/teos/src/cli_config.rs index 4f937206..ba085b8d 100644 --- a/teos/src/cli_config.rs +++ b/teos/src/cli_config.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use structopt::StructOpt; #[derive(Debug, StructOpt, Clone)] -#[structopt(rename_all = "snake_case")] +#[structopt(rename_all = "lower_case")] pub enum Command { /// Gets information about all appointments stored in the tower GetAllAppointments, @@ -37,7 +37,7 @@ pub struct GetAppointmentsData { #[derive(StructOpt, Debug)] #[structopt(rename_all = "lowercase")] #[structopt( - version = "0.0.1", + version = env!("CARGO_PKG_VERSION"), about = "The Eye of Satoshi - CLI", name = "teos-cli" )] @@ -54,10 +54,6 @@ pub struct Opt { #[structopt(long, default_value = "~/.teos")] pub data_dir: String, - /// Runs teos-cli in debug mode [default: false] - #[structopt(long)] - pub debug: bool, - /// Command #[structopt(subcommand)] pub command: Command, @@ -74,7 +70,6 @@ pub struct Opt { pub struct Config { pub rpc_bind: String, pub rpc_port: u16, - pub debug: bool, } impl Config { @@ -86,8 +81,6 @@ impl Config { if options.rpc_port.is_some() { self.rpc_port = options.rpc_port.unwrap(); } - - self.debug |= options.debug; } } @@ -102,7 +95,6 @@ impl Default for Config { Self { rpc_bind: "localhost".into(), rpc_port: 8814, - debug: false, } } } diff --git a/teos/src/conf_template.toml b/teos/src/conf_template.toml index fb21dc13..9540cb4a 100644 --- a/teos/src/conf_template.toml +++ b/teos/src/conf_template.toml @@ -2,7 +2,7 @@ api_bind = "127.0.0.1" api_port = 9814 tor_control_port = 9051 -onion_hidden_service_port = 2121 +onion_hidden_service_port = 9814 tor_support = false # RPC @@ -10,7 +10,7 @@ rpc_bind = "127.0.0.1" rpc_port = 8814 # bitcoind -btc_network = "bitcoin" +btc_network = "mainnet" btc_rpc_user = "CSW" btc_rpc_password = "NotSatoshi" btc_rpc_connect = "localhost" @@ -18,6 +18,7 @@ btc_rpc_port = 8332 # Flags debug = false +deps_debug = false overwrite_key = false # General diff --git a/teos/src/config.rs b/teos/src/config.rs index ceee19da..01e2b752 100644 --- a/teos/src/config.rs +++ b/teos/src/config.rs @@ -1,10 +1,7 @@ //! Logic related to the tower configuration and command line parameter parsing. -use bitcoin::network::constants::Network; use serde::Deserialize; -use std; use std::path::PathBuf; -use std::str::FromStr; use structopt::StructOpt; pub fn data_dir_absolute_path(data_dir: String) -> PathBuf { @@ -47,7 +44,7 @@ impl std::error::Error for ConfigError {} /// Holds all the command line options. #[derive(StructOpt, Debug, Clone)] #[structopt(rename_all = "lowercase")] -#[structopt(version = "0.0.1", about = "The Eye of Satoshi - Lightning watchtower")] +#[structopt(version = env!("CARGO_PKG_VERSION"), about = "The Eye of Satoshi - Lightning watchtower")] pub struct Opt { /// Address teos HTTP(s) API will bind to [default: localhost] #[structopt(long)] @@ -93,19 +90,23 @@ pub struct Opt { #[structopt(long)] pub debug: bool, + /// Runs third party libs in debug mode + #[structopt(long)] + pub deps_debug: bool, + /// Overwrites the tower secret key. THIS IS IRREVERSIBLE AND WILL CHANGE YOUR TOWER ID #[structopt(long)] pub overwrite_key: bool, - /// If set, creates a tor endpoint to serve API data. This endpoint is additional to the clearnet HTTP API + /// If set, creates a Tor endpoint to serve API data. This endpoint is additional to the clearnet HTTP API #[structopt(long)] pub tor_support: bool, - /// tor control port [default: 9051] + /// Tor control port [default: 9051] #[structopt(long)] pub tor_control_port: Option, - /// Port for the onion hidden service to listen on [default: 2121] + /// Port for the onion hidden service to listen on [default: 9814] #[structopt(long)] pub onion_hidden_service_port: Option, } @@ -136,6 +137,7 @@ pub struct Config { // Flags pub debug: bool, + pub deps_debug: bool, pub overwrite_key: bool, // General @@ -194,6 +196,7 @@ impl Config { self.tor_support |= options.tor_support; self.debug |= options.debug; + self.deps_debug |= options.deps_debug; self.overwrite_key = options.overwrite_key; } @@ -213,25 +216,26 @@ impl Config { return Err(ConfigError("btc_rpc_password must be set".to_owned())); } - match Network::from_str(&self.btc_network) { - Ok(network) => { - // Set the port to it's default (depending on the network) if it has not been - // overwritten at this point. - if self.btc_rpc_port == 0 { - self.btc_rpc_port = match network { - Network::Testnet => 18332, - Network::Signet => 38332, - Network::Regtest => 18443, - _ => 8332, - } - } - Ok(()) - } - Err(_) => { - Err(ConfigError(format!("btc_network not recognized. Expected {{bitcoin, testnet, signet, regtest}}, received {}", - self.btc_network))) - } + // Normalize the network option to the ones used by bitcoind. + if ["mainnet", "testnet"].contains(&self.btc_network.as_str()) { + self.btc_network = self.btc_network.trim_end_matches("net").into(); } + + let default_rpc_port = match self.btc_network.as_str() { + "main" => 8332, + "test" => 18332, + "regtest" => 18443, + "signet" => 38332, + _ => return Err(ConfigError(format!("btc_network not recognized. Expected {{mainnet, testnet, signet, regtest}}, received {}", self.btc_network))) + }; + + // Set the port to it's default (depending on the network) if it has not been + // overwritten at this point. + if self.btc_rpc_port == 0 { + self.btc_rpc_port = default_rpc_port; + } + + Ok(()) } /// Checks whether the config has been set with only with default values. @@ -253,16 +257,17 @@ impl Default for Config { api_port: 9814, tor_support: false, tor_control_port: 9051, - onion_hidden_service_port: 2121, + onion_hidden_service_port: 9814, rpc_bind: "127.0.0.1".into(), rpc_port: 8814, - btc_network: "bitcoin".into(), + btc_network: "mainnet".into(), btc_rpc_user: String::new(), btc_rpc_password: String::new(), btc_rpc_connect: "localhost".into(), btc_rpc_port: 0, debug: false, + deps_debug: false, overwrite_key: false, subscription_slots: 10000, subscription_duration: 4320, @@ -297,6 +302,7 @@ mod tests { data_dir: String::from("~/.teos"), debug: false, + deps_debug: false, overwrite_key: false, } } @@ -326,7 +332,9 @@ mod tests { // Tests that the default configuration does not pass verification checks. This is on purpose so some fields are // required to be updated by the user. let mut config = Config::default(); - assert!(matches!(config.verify(), Err(ConfigError { .. }))); + assert!( + matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_rpc_user must be set")) + ); } #[test] @@ -349,7 +357,9 @@ mod tests { btc_network: "wrong_network".to_owned(), ..Default::default() }; - assert!(matches!(config.verify(), Err(ConfigError { .. }))); + assert!( + matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_network not recognized")) + ); } #[test] diff --git a/teos/src/extended_appointment.rs b/teos/src/extended_appointment.rs index b39db0fa..88920de0 100644 --- a/teos/src/extended_appointment.rs +++ b/teos/src/extended_appointment.rs @@ -22,7 +22,7 @@ impl UUID { /// Therefore, it provides a hard-to-forge id while reducing the tower lookups and the required data to be stored (no reverse maps). pub fn new(locator: Locator, user_id: UserId) -> Self { let mut uuid_data = locator.to_vec(); - uuid_data.extend(&user_id.0.serialize()); + uuid_data.extend(user_id.0.serialize()); UUID(ripemd160::Hash::hash(&uuid_data).into_inner()) } diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index 8441e03b..7ae8a23d 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -175,7 +175,10 @@ impl Gatekeeper { .available_slots .checked_add(self.subscription_slots) .ok_or(MaxSlotsReached)?; - user_info.subscription_expiry = block_count + self.subscription_duration; + user_info.subscription_expiry = user_info + .subscription_expiry + .checked_add(self.subscription_duration) + .unwrap_or(u32::MAX); self.dbm.lock().unwrap().update_user(user_id, user_info); user_info @@ -322,11 +325,13 @@ impl chain::Listen for Gatekeeper { // Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired. let outdated_users = self.get_outdated_user_ids(height); - self.registered_users - .lock() - .unwrap() - .retain(|id, _| !outdated_users.contains(id)); - self.dbm.lock().unwrap().batch_remove_users(&outdated_users); + if !outdated_users.is_empty() { + self.registered_users + .lock() + .unwrap() + .retain(|id, _| !outdated_users.contains(id)); + self.dbm.lock().unwrap().batch_remove_users(&outdated_users); + } // Update last known block height self.last_known_block_height @@ -499,13 +504,10 @@ mod tests { .store(chain.get_block_count(), Ordering::Relaxed); let updated_receipt = gatekeeper.add_update_user(user_id).unwrap(); - assert_eq!( - updated_receipt.available_slots(), - receipt.available_slots() * 2 - ); + assert_eq!(updated_receipt.available_slots(), SLOTS * 2); assert_eq!( updated_receipt.subscription_expiry(), - receipt.subscription_expiry() + 1 + START_HEIGHT as u32 + DURATION * 2 ); // Data in the database should have been updated too diff --git a/teos/src/lib.rs b/teos/src/lib.rs index ce4ac6d5..2e87bcf1 100644 --- a/teos/src/lib.rs +++ b/teos/src/lib.rs @@ -2,6 +2,8 @@ //! //! A watchtower implementation written in Rust. +// FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661 +#[allow(clippy::derive_partial_eq_without_eq)] pub mod protos { tonic::include_proto!("teos.v2"); } @@ -20,6 +22,7 @@ pub mod responder; #[doc(hidden)] mod rpc_errors; pub mod tls; +mod tx_index; pub mod watcher; #[cfg(test)] diff --git a/teos/src/main.rs b/teos/src/main.rs index 4602186f..7fdd158b 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -1,4 +1,5 @@ -use simple_logger::init_with_level; +use log::LevelFilter; +use simple_logger::SimpleLogger; use std::fs; use std::io::ErrorKind; use std::ops::{Deref, DerefMut}; @@ -31,6 +32,7 @@ use teos::responder::Responder; use teos::tls::tls_init; use teos::watcher::Watcher; +use teos_common::constants::IRREVOCABLY_RESOLVED; use teos_common::cryptography::get_random_keypair; use teos_common::TowerId; @@ -43,7 +45,7 @@ where B: DerefMut + Sized + Send + Sync, T: BlockSource, { - let mut last_n_blocks = Vec::new(); + let mut last_n_blocks = Vec::with_capacity(n); for _ in 0..n { let block = poller.fetch_block(&last_known_block).await.unwrap(); last_known_block = poller @@ -83,11 +85,22 @@ async fn main() { }); // Set log level - if conf.debug { - init_with_level(log::Level::Debug).unwrap() - } else { - init_with_level(log::Level::Info).unwrap() - } + SimpleLogger::new() + .with_level(if conf.deps_debug { + LevelFilter::Debug + } else { + LevelFilter::Warn + }) + .with_module_level( + "teos", + if conf.debug { + LevelFilter::Debug + } else { + LevelFilter::Info + }, + ) + .init() + .unwrap(); if is_default { log::info!("Loading default configuration") @@ -130,6 +143,7 @@ async fn main() { conf.btc_rpc_port, &conf.btc_rpc_user, &conf.btc_rpc_password, + &conf.btc_network, ) .await { @@ -174,10 +188,31 @@ async fn main() { } else { validate_best_block_header(&mut derefed).await.unwrap() }; + + // DISCUSS: This is not really required (and only triggered in regtest). This is only in place so the caches can be + // populated with enough blocks mainly because the size of the cache is based on the amount of blocks passed when initializing. + // However, we could add an additional parameter to specify the size of the cache, and initialize with however may blocks we + // could pull from the backend. Adding this functionality just for regtest seemed unnecessary though, hence the check. + if tip.height < IRREVOCABLY_RESOLVED { + log::error!( + "Not enough blocks to start teosd (required: {}). Mine at least {} more", + IRREVOCABLY_RESOLVED, + IRREVOCABLY_RESOLVED - tip.height + ); + std::process::exit(1); + } + log::info!("Last known block: {}", tip.header.block_hash()); - let mut poller = ChainPoller::new(&mut derefed, Network::from_str(&conf.btc_network).unwrap()); - let last_n_blocks = get_last_n_blocks(&mut poller, tip, 6).await; + // This is how chain poller names bitcoin networks. + let btc_network = match conf.btc_network.as_str() { + "main" => "bitcoin", + "test" => "testnet", + any => any, + }; + + let mut poller = ChainPoller::new(&mut derefed, Network::from_str(btc_network).unwrap()); + let last_n_blocks = get_last_n_blocks(&mut poller, tip, IRREVOCABLY_RESOLVED as usize).await; // Build components let gatekeeper = Arc::new(Gatekeeper::new( @@ -188,12 +223,18 @@ async fn main() { dbm.clone(), )); - let carrier = Carrier::new(rpc, bitcoind_reachable.clone(), tip.deref().height); - let responder = Arc::new(Responder::new(carrier, gatekeeper.clone(), dbm.clone())); + let carrier = Carrier::new(rpc, bitcoind_reachable.clone(), tip.height); + let responder = Arc::new(Responder::new( + &last_n_blocks, + tip.height, + carrier, + gatekeeper.clone(), + dbm.clone(), + )); let watcher = Arc::new(Watcher::new( gatekeeper.clone(), responder.clone(), - last_n_blocks, + &last_n_blocks[0..6], tip.height, tower_sk, TowerId(tower_pk), @@ -283,25 +324,40 @@ async fn main() { .unwrap(); }); + let (http_service_ready, ready_signal_http) = triggered::trigger(); let http_api_task = task::spawn(http::serve( http_api_addr, internal_rpc_api_uri, + http_service_ready, shutdown_signal_http, )); + ready_signal_http.await; // Add Tor Onion Service for public API let mut tor_task = Option::None; + let (tor_service_ready, ready_signal_tor) = triggered::trigger(); if conf.tor_support { - log::info!("Starting up hidden tor service"); + log::info!("Starting up Tor hidden service"); let tor_control_port = conf.tor_control_port; - let api_port = conf.api_port; let onion_port = conf.onion_hidden_service_port; tor_task = Some(task::spawn(async move { - tor::expose_onion_service(tor_control_port, api_port, onion_port, shutdown_signal_tor) - .await - .unwrap(); + if let Err(e) = tor::expose_onion_service( + tor_control_port, + http_api_addr, + onion_port, + path_network, + tor_service_ready, + shutdown_signal_tor, + ) + .await + { + eprintln!("Cannot connect to the Tor backend: {}", e); + std::process::exit(1); + } })); + + ready_signal_tor.await } log::info!("Tower ready"); diff --git a/teos/src/responder.rs b/teos/src/responder.rs index 0e81b740..6616949e 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -4,9 +4,10 @@ use std::collections::{HashMap, HashSet}; use std::iter::FromIterator; use std::sync::{Arc, Mutex}; -use bitcoin::consensus; +use bitcoin::{consensus, BlockHash}; use bitcoin::{BlockHeader, Transaction, Txid}; use lightning::chain; +use lightning_block_sync::poll::ValidatedBlock; use teos_common::constants; use teos_common::protos as common_msgs; @@ -16,6 +17,7 @@ use crate::carrier::Carrier; use crate::dbm::DBM; use crate::extended_appointment::UUID; use crate::gatekeeper::{Gatekeeper, UserInfo}; +use crate::tx_index::TxIndex; use crate::watcher::Breach; /// Number of missed confirmations to wait before rebroadcasting a transaction. @@ -26,6 +28,7 @@ const CONFIRMATIONS_BEFORE_RETRY: u8 = 6; pub enum ConfirmationStatus { ConfirmedIn(u32), InMempoolSince(u32), + IrrevocablyResolved, Rejected(i32), ReorgedOut, } @@ -59,6 +62,14 @@ impl ConfirmationStatus { None } } + + /// Whether the transaction was accepted by the underlying node. + pub fn accepted(&self) -> bool { + matches!( + self, + ConfirmationStatus::ConfirmedIn(_) | &ConfirmationStatus::InMempoolSince(_) + ) + } } /// Minimal data required in memory to keep track of transaction trackers. @@ -130,6 +141,8 @@ pub struct Responder { trackers: Mutex>, /// A map between [Txid]s and [UUID]s. tx_tracker_map: Mutex>>, + /// A local, pruned, [TxIndex] used to avoid the need of `txindex=1`. + tx_index: Mutex>, /// A [Carrier] instance. Data is sent to the `bitcoind` through it. carrier: Mutex, /// A [Gatekeeper] instance. Data regarding users is requested to it. @@ -140,7 +153,13 @@ pub struct Responder { impl Responder { /// Creates a new [Responder] instance. - pub fn new(carrier: Carrier, gatekeeper: Arc, dbm: Arc>) -> Self { + pub fn new( + last_n_blocs: &[ValidatedBlock], + last_known_block_height: u32, + carrier: Carrier, + gatekeeper: Arc, + dbm: Arc>, + ) -> Self { let mut trackers = HashMap::new(); let mut tx_tracker_map: HashMap> = HashMap::new(); @@ -158,6 +177,7 @@ impl Responder { carrier: Mutex::new(carrier), trackers: Mutex::new(trackers), tx_tracker_map: Mutex::new(tx_tracker_map), + tx_index: Mutex::new(TxIndex::new(last_n_blocs, last_known_block_height)), dbm, gatekeeper, } @@ -189,12 +209,20 @@ impl Responder { return tracker.status; } - let status = self - .carrier - .lock() - .unwrap() - .send_transaction(&breach.penalty_tx); - if !matches!(status, ConfirmationStatus::Rejected { .. }) { + let mut carrier = self.carrier.lock().unwrap(); + let tx_index = self.tx_index.lock().unwrap(); + + // Check whether the transaction is in mempool or part of our internal txindex. Send it to our node otherwise. + let status = if carrier.in_mempool(&breach.penalty_tx.txid()) { + // If it's in mempool we assume it was just included + ConfirmationStatus::InMempoolSince(carrier.block_height()) + } else if let Some(block_hash) = tx_index.get(&breach.penalty_tx.txid()) { + ConfirmationStatus::ConfirmedIn(tx_index.get_height(block_hash).unwrap() as u32) + } else { + carrier.send_transaction(&breach.penalty_tx) + }; + + if status.accepted() { self.add_tracker(uuid, breach, user_id, status); } @@ -377,12 +405,15 @@ impl Responder { let mut trackers = self.trackers.lock().unwrap(); let mut carrier = self.carrier.lock().unwrap(); + let tx_index = self.tx_index.lock().unwrap(); for (uuid, (penalty_tx, dispute_tx)) in txs.into_iter() { let status = if let Some(dispute_tx) = dispute_tx { - // The tracker was reorged out, and the dispute may potentially not be in the chain anymore. - if carrier.get_block_hash_for_tx(&dispute_tx.txid()).is_some() { - // Dispute tx is on chain, so we only need to care about the penalty + // The tracker was reorged out, and the dispute may potentially not be in the chain (or mempool) anymore. + if tx_index.contains_key(&dispute_tx.txid()) + | carrier.in_mempool(&dispute_tx.txid()) + { + // Dispute tx is on chain (or mempool), so we only need to care about the penalty carrier.send_transaction(&penalty_tx) } else { // Dispute tx has also been reorged out, meaning that both transactions need to be broadcast. @@ -472,11 +503,13 @@ impl Responder { updated_users: &HashMap, reason: DeletionReason, ) { - self.delete_trackers_from_memory(uuids, reason); - self.dbm - .lock() - .unwrap() - .batch_remove_appointments(uuids, updated_users); + if !uuids.is_empty() { + self.delete_trackers_from_memory(uuids, reason); + self.dbm + .lock() + .unwrap() + .batch_remove_appointments(uuids, updated_users); + } } } @@ -501,7 +534,13 @@ impl chain::Listen for Responder { log::info!("New block received: {}", header.block_hash()); self.carrier.lock().unwrap().update_height(height); - if self.trackers.lock().unwrap().len() > 0 { + let txs = txdata + .iter() + .map(|(_, tx)| (tx.txid(), header.block_hash())) + .collect(); + self.tx_index.lock().unwrap().update(*header, &txs); + + if !self.trackers.lock().unwrap().is_empty() { // Complete those appointments that are due at this height let completed_trackers = self.check_confirmations( &txdata.iter().map(|(_, tx)| tx.txid()).collect::>(), @@ -553,6 +592,10 @@ impl chain::Listen for Responder { fn block_disconnected(&self, header: &BlockHeader, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); self.carrier.lock().unwrap().update_height(height); + self.tx_index + .lock() + .unwrap() + .remove_disconnected_block(&header.block_hash()); for tracker in self.trackers.lock().unwrap().values_mut() { // The transaction has been unconfirmed. Flag it as reorged out so we can rebroadcast it. @@ -568,19 +611,19 @@ mod tests { use super::*; use lightning::chain::Listen; - use std::ops::Deref; use std::sync::{Arc, Mutex}; use crate::dbm::DBM; use crate::gatekeeper::UserInfo; use crate::rpc_errors; use crate::test_utils::{ - create_carrier, generate_dummy_appointment_with_user, generate_uuid, get_random_breach, - get_random_tracker, get_random_tx, store_appointment_and_fks_to_db, Blockchain, - MockedServerQuery, AVAILABLE_SLOTS, DURATION, EXPIRY_DELTA, SLOTS, START_HEIGHT, - SUBSCRIPTION_EXPIRY, SUBSCRIPTION_START, + create_carrier, generate_dummy_appointment_with_user, generate_uuid, get_last_n_blocks, + get_random_breach, get_random_tracker, get_random_tx, store_appointment_and_fks_to_db, + BitcoindStopper, Blockchain, MockedServerQuery, AVAILABLE_SLOTS, DURATION, EXPIRY_DELTA, + SLOTS, START_HEIGHT, SUBSCRIPTION_EXPIRY, SUBSCRIPTION_START, }; + use teos_common::constants::IRREVOCABLY_RESOLVED; use teos_common::dbm::Error as DBError; use teos_common::test_utils::get_random_user_id; @@ -638,21 +681,32 @@ mod tests { } } - fn create_responder( - chain: &Blockchain, + async fn create_responder( + chain: &mut Blockchain, gatekeeper: Arc, dbm: Arc>, query: MockedServerQuery, - ) -> Responder { - let tip = chain.tip(); - Responder::new(create_carrier(query, tip.deref().height), gatekeeper, dbm) + ) -> (Responder, BitcoindStopper) { + let height = if chain.tip().height < IRREVOCABLY_RESOLVED { + chain.tip().height + } else { + IRREVOCABLY_RESOLVED + }; + + let last_n_blocks = get_last_n_blocks(chain, height as usize).await; + + let (carrier, bitcoind_stopper) = create_carrier(query, chain.tip().height); + ( + Responder::new(&last_n_blocks, chain.tip().height, carrier, gatekeeper, dbm), + bitcoind_stopper, + ) } - fn init_responder_with_chain_and_dbm( + async fn init_responder_with_chain_and_dbm( mocked_query: MockedServerQuery, - chain: &Blockchain, + chain: &mut Blockchain, dbm: Arc>, - ) -> Responder { + ) -> (Responder, BitcoindStopper) { let gk = Gatekeeper::new( chain.get_block_count(), SLOTS, @@ -660,13 +714,13 @@ mod tests { EXPIRY_DELTA, dbm.clone(), ); - create_responder(chain, Arc::new(gk), dbm, mocked_query) + create_responder(chain, Arc::new(gk), dbm, mocked_query).await } - fn init_responder(mocked_query: MockedServerQuery) -> Responder { + async fn init_responder(mocked_query: MockedServerQuery) -> (Responder, BitcoindStopper) { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); - init_responder_with_chain_and_dbm(mocked_query, &chain, dbm) + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); + init_responder_with_chain_and_dbm(mocked_query, &mut chain, dbm).await } #[test] @@ -709,13 +763,14 @@ mod tests { assert_eq!(ConfirmationStatus::ReorgedOut.to_db_data(), None); } - #[test] - fn test_responder_new() { + #[tokio::test] + async fn test_responder_new() { // A fresh responder has no associated data - let chain = Blockchain::default().with_height(START_HEIGHT); + let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let responder = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm.clone()); + let (responder, _s) = + init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm.clone()) + .await; assert!(responder.is_fresh()); // If we add some trackers to the system and create a new Responder reusing the same db @@ -736,15 +791,16 @@ mod tests { } // Create a new Responder reusing the same DB and check that the data is loaded - let another_r = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm); + let (another_r, _) = + init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; assert!(!responder.is_fresh()); assert_eq!(responder, another_r); } - #[test] - fn test_handle_breach_delivered() { + #[tokio::test] + async fn test_handle_breach_accepted() { let start_height = START_HEIGHT as u32; - let responder = init_responder(MockedServerQuery::Regular); + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let user_id = get_random_user_id(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -788,11 +844,82 @@ mod tests { .contains_key(&another_breach.penalty_tx.txid())); } - #[test] - fn test_handle_breach_rejected() { - let responder = init_responder(MockedServerQuery::Error( + #[tokio::test] + async fn test_handle_breach_accepted_in_mempool() { + let start_height = START_HEIGHT as u32; + let (responder, _s) = init_responder(MockedServerQuery::InMempoool).await; + + let user_id = get_random_user_id(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + store_appointment_and_fks_to_db(&responder.dbm.lock().unwrap(), uuid, &appointment); + + let breach = get_random_breach(); + let penalty_txid = breach.penalty_tx.txid(); + + assert_eq!( + responder.handle_breach(uuid, breach, user_id), + ConfirmationStatus::InMempoolSince(start_height) + ); + assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); + assert_eq!( + responder.trackers.lock().unwrap()[&uuid].status, + ConfirmationStatus::InMempoolSince(start_height) + ); + assert!(responder + .tx_tracker_map + .lock() + .unwrap() + .contains_key(&penalty_txid)); + } + + #[tokio::test] + async fn test_handle_breach_accepted_in_txindex() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; + + let user_id = get_random_user_id(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + store_appointment_and_fks_to_db(&responder.dbm.lock().unwrap(), uuid, &appointment); + + let breach = get_random_breach(); + let penalty_txid = breach.penalty_tx.txid(); + + // Add the tx to our txindex + let target_block_hash = *responder.tx_index.lock().unwrap().blocks().get(2).unwrap(); + responder + .tx_index + .lock() + .unwrap() + .index_mut() + .insert(penalty_txid, target_block_hash); + let target_height = responder + .tx_index + .lock() + .unwrap() + .get_height(&target_block_hash) + .unwrap() as u32; + + assert_eq!( + responder.handle_breach(uuid, breach, user_id), + ConfirmationStatus::ConfirmedIn(target_height) + ); + assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); + assert_eq!( + responder.trackers.lock().unwrap()[&uuid].status, + ConfirmationStatus::ConfirmedIn(target_height) + ); + assert!(responder + .tx_tracker_map + .lock() + .unwrap() + .contains_key(&penalty_txid)); + } + + #[tokio::test] + async fn test_handle_breach_rejected() { + let (responder, _s) = init_responder(MockedServerQuery::Error( rpc_errors::RPC_VERIFY_ERROR as i64, - )); + )) + .await; let user_id = get_random_user_id(); let uuid = generate_uuid(); @@ -811,9 +938,9 @@ mod tests { .contains_key(&penalty_txid)); } - #[test] - fn test_add_tracker() { - let responder = init_responder(MockedServerQuery::Regular); + #[tokio::test] + async fn test_add_tracker() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let start_height = START_HEIGHT as u32; // Add the necessary FKs in the database @@ -933,12 +1060,12 @@ mod tests { ); } - #[test] - fn test_has_tracker() { + #[tokio::test] + async fn test_has_tracker() { // Has tracker should return true as long as the given tracker is held by the Responder. // As long as the tracker is in Responder.trackers and Responder.tx_tracker_map, the return // must be true. - let responder = init_responder(MockedServerQuery::Regular); + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Add a new tracker let user_id = get_random_user_id(); @@ -964,11 +1091,11 @@ mod tests { assert!(!responder.has_tracker(uuid)); } - #[test] - fn test_get_tracker() { + #[tokio::test] + async fn test_get_tracker() { // Should return a tracker as long as it exists let start_height = START_HEIGHT as u32; - let responder = init_responder(MockedServerQuery::Regular); + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Store the user and the appointment in the database so we can add the tracker later on (due to FK restrictions) let user_id = get_random_user_id(); @@ -1004,9 +1131,9 @@ mod tests { assert_eq!(responder.get_tracker(uuid), None); } - #[test] - fn test_check_confirmations() { - let responder = init_responder(MockedServerQuery::Regular); + #[tokio::test] + async fn test_check_confirmations() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let target_height = (START_HEIGHT * 2) as u32; // Unconfirmed transactions that miss a confirmation will be added to missed_confirmations (if not there) or their missed confirmation count till be increased @@ -1110,9 +1237,9 @@ mod tests { } } - #[test] - fn test_get_txs_to_rebroadcast() { - let responder = init_responder(MockedServerQuery::Regular); + #[tokio::test] + async fn test_get_txs_to_rebroadcast() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let current_height = 100; let user_id = get_random_user_id(); @@ -1154,13 +1281,13 @@ mod tests { } } - assert_eq!(responder.get_txs_to_rebroadcast(current_height), txs) + assert_eq!(responder.get_txs_to_rebroadcast(current_height), txs); } - #[test] - fn test_get_txs_to_rebroadcast_reorged() { + #[tokio::test] + async fn test_get_txs_to_rebroadcast_reorged() { // For reorged transactions this works a bit different, the dispute transaction will also be returned here - let responder = init_responder(MockedServerQuery::Regular); + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let current_height = 100; let user_id = get_random_user_id(); @@ -1198,7 +1325,7 @@ mod tests { // Since we are adding trackers using add_trackers we'll need to manually change the state of the transaction // (reorged transactions are not passed to add_tracker, they are detected after they are already there). - // Not doing should will trigger an error in the dbm since reorged transactions are not stored in the db. + // Not doing so will trigger an error in the dbm since reorged transactions are not stored in the db. if i % 2 == 0 { responder .trackers @@ -1216,12 +1343,12 @@ mod tests { } // Since we have only added confirmed and reorged transactions, we should get back only the reorged ones. - assert_eq!(responder.get_txs_to_rebroadcast(current_height), txs) + assert_eq!(responder.get_txs_to_rebroadcast(current_height), txs); } - #[test] - fn test_get_outdated_trackers() { - let responder = init_responder(MockedServerQuery::Regular); + #[tokio::test] + async fn test_get_outdated_trackers() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Outdated trackers are those whose associated subscription is outdated and have not been confirmed yet (they don't have // a single confirmation). @@ -1267,11 +1394,11 @@ mod tests { ); } - #[test] - fn test_rebroadcast_accepted() { + #[tokio::test] + async fn test_rebroadcast_accepted() { // This test positive rebroadcast cases, including reorgs. However, complex reorg logic is not tested here, it will need a // dedicated test (against bitcoind, not mocked). - let responder = init_responder(MockedServerQuery::Regular); + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let current_height = 100; // Add user to the database @@ -1335,13 +1462,14 @@ mod tests { assert!(rejected.is_empty()); } - #[test] - fn test_rebroadcast_rejected() { + #[tokio::test] + async fn test_rebroadcast_rejected() { // This test negative rebroadcast cases, including reorgs. However, complex reorg logic is not tested here, it will need a // dedicated test (against bitcoind, not mocked). - let responder = init_responder(MockedServerQuery::Error( + let (responder, _s) = init_responder(MockedServerQuery::Error( rpc_errors::RPC_VERIFY_ERROR as i64, - )); + )) + .await; let current_height = 100; // Add user to the database @@ -1404,9 +1532,9 @@ mod tests { assert!(accepted.is_empty()); } - #[test] - fn test_delete_trackers_from_memory() { - let responder = init_responder(MockedServerQuery::Regular); + #[tokio::test] + async fn test_delete_trackers_from_memory() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Add user to the database let user_id = get_random_user_id(); @@ -1461,9 +1589,9 @@ mod tests { } } - #[test] - fn test_delete_trackers() { - let responder = init_responder(MockedServerQuery::Regular); + #[tokio::test] + async fn test_delete_trackers() { + let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Add user to the database let user_id = get_random_user_id(); @@ -1597,12 +1725,13 @@ mod tests { } } - #[test] - fn test_filtered_block_connected() { + #[tokio::test] + async fn test_filtered_block_connected() { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let start_height = START_HEIGHT * 2; let mut chain = Blockchain::default().with_height(start_height); - let responder = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm); + let (responder, _s) = + init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; // block_connected is used to keep track of the confirmation received (or missed) by the trackers the Responder // is keeping track of. @@ -1841,11 +1970,12 @@ mod tests { ); } - #[test] - fn test_block_disconnected() { + #[tokio::test] + async fn test_block_disconnected() { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); - let responder = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm); + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); + let (responder, _s) = + init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; // Add user to the database let user_id = get_random_user_id(); diff --git a/teos/src/test_utils.rs b/teos/src/test_utils.rs index df4684f2..f8cdf7ac 100644 --- a/teos/src/test_utils.rs +++ b/teos/src/test_utils.rs @@ -8,13 +8,12 @@ */ use rand::Rng; -use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; use std::thread; use jsonrpc_http_server::jsonrpc_core::error::ErrorCode as JsonRpcErrorCode; use jsonrpc_http_server::jsonrpc_core::{Error as JsonRpcError, IoHandler, Params, Value}; -use jsonrpc_http_server::{Server, ServerBuilder}; +use jsonrpc_http_server::{CloseHandle, Server, ServerBuilder}; use bitcoincore_rpc::{Auth, Client as BitcoindClient}; @@ -36,6 +35,7 @@ use lightning_block_sync::{ AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache, }; +use teos_common::constants::IRREVOCABLY_RESOLVED; use teos_common::cryptography::{get_random_bytes, get_random_keypair}; use teos_common::test_utils::{generate_random_appointment, get_random_user_id, TXID_HEX, TX_HEX}; use teos_common::UserId; @@ -46,6 +46,7 @@ use crate::dbm::DBM; use crate::extended_appointment::{ExtendedAppointment, UUID}; use crate::gatekeeper::{Gatekeeper, UserInfo}; use crate::responder::{ConfirmationStatus, Responder, TransactionTracker}; +use crate::rpc_errors; use crate::watcher::{Breach, Watcher}; pub(crate) const SLOTS: u32 = 21; @@ -82,25 +83,10 @@ impl Blockchain { pub fn with_height(mut self, height: usize) -> Self { self.blocks.reserve_exact(height); - let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32])); - for i in 1..=height { - let prev_block = &self.blocks[i - 1]; - let prev_blockhash = prev_block.block_hash(); - let time = prev_block.header.time + height as u32; - let txdata = vec![get_random_tx()]; - let hashes = txdata.iter().map(|obj| obj.txid().as_hash()); - self.blocks.push(Block { - header: BlockHeader { - version: 0, - prev_blockhash, - merkle_root: bitcoin_merkle_root(hashes).unwrap().into(), - time, - bits, - nonce: 0, - }, - txdata, - }); + for _ in 1..=height { + self.generate(None); } + self } @@ -368,18 +354,15 @@ pub(crate) fn store_appointment_and_fks_to_db( } pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec { - let tip = chain.tip(); - let poller = ChainPoller::new(chain, Network::Bitcoin); + let mut last_n_blocks = Vec::with_capacity(n); + let mut last_known_block = Ok(chain.tip()); + let poller = ChainPoller::new(chain, Network::Regtest); - let mut last_n_blocks = Vec::new(); - let mut last_known_block = tip; for _ in 0..n { - let block = poller.fetch_block(&last_known_block).await.unwrap(); - last_known_block = poller - .look_up_previous_header(&last_known_block) - .await - .unwrap(); + let header = last_known_block.unwrap(); + let block = poller.fetch_block(&header).await.unwrap(); last_n_blocks.push(block); + last_known_block = poller.look_up_previous_header(&header).await; } last_n_blocks @@ -387,32 +370,43 @@ pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec Carrier { +pub(crate) fn create_carrier(query: MockedServerQuery, height: u32) -> (Carrier, BitcoindStopper) { let bitcoind_mock = match query { - MockedServerQuery::Regular => BitcoindMock::new(MockOptions::empty()), + MockedServerQuery::Regular => BitcoindMock::new(MockOptions::default()), + MockedServerQuery::InMempoool => BitcoindMock::new(MockOptions::in_mempool()), MockedServerQuery::Error(x) => BitcoindMock::new(MockOptions::with_error(x)), }; let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); - Carrier::new(bitcoin_cli, bitcoind_reachable, height) + ( + Carrier::new(bitcoin_cli, bitcoind_reachable, height), + bitcoind_mock.stopper, + ) } -pub(crate) fn create_responder( - tip: ValidatedBlockHeader, +pub(crate) async fn create_responder( + chain: &mut Blockchain, gatekeeper: Arc, dbm: Arc>, server_url: &str, ) -> Responder { + let height = chain.tip().height; + // For the local TxIndex logic to be sound, our index needs to have, at least, IRREVOCABLY_RESOLVED blocks + debug_assert!(height >= IRREVOCABLY_RESOLVED); + + let last_n_blocks = get_last_n_blocks(chain, IRREVOCABLY_RESOLVED as usize).await; + let bitcoin_cli = Arc::new(BitcoindClient::new(server_url, Auth::None).unwrap()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, tip.deref().height); + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, height); - Responder::new(carrier, gatekeeper, dbm) + Responder::new(&last_n_blocks, height, carrier, gatekeeper, dbm) } pub(crate) async fn create_watcher( @@ -421,20 +415,23 @@ pub(crate) async fn create_watcher( gatekeeper: Arc, bitcoind_mock: BitcoindMock, dbm: Arc>, -) -> Watcher { +) -> (Watcher, BitcoindStopper) { let last_n_blocks = get_last_n_blocks(chain, 6).await; - start_server(bitcoind_mock); + start_server(bitcoind_mock.server); let (tower_sk, tower_pk) = get_random_keypair(); let tower_id = UserId(tower_pk); - Watcher::new( - gatekeeper, - responder, - last_n_blocks, - chain.get_block_count(), - tower_sk, - tower_id, - dbm, + ( + Watcher::new( + gatekeeper, + responder, + &last_n_blocks, + chain.get_block_count(), + tower_sk, + tower_id, + dbm, + ), + bitcoind_mock.stopper, ) } #[derive(Clone)] @@ -469,8 +466,10 @@ impl Default for ApiConfig { } } -pub(crate) async fn create_api_with_config(api_config: ApiConfig) -> Arc { - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); +pub(crate) async fn create_api_with_config( + api_config: ApiConfig, +) -> (Arc, BitcoindStopper) { + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); @@ -481,8 +480,9 @@ pub(crate) async fn create_api_with_config(api_config: ApiConfig) -> Arc Arc Arc { +pub(crate) async fn create_api() -> (Arc, BitcoindStopper) { create_api_with_config(ApiConfig::default()).await } + +#[derive(Clone)] +pub struct BitcoindStopper { + close_handle: CloseHandle, +} + +impl BitcoindStopper { + pub fn new(close_handle: CloseHandle) -> Self { + Self { close_handle } + } + + pub fn close_handle(&self) -> CloseHandle { + self.close_handle.clone() + } +} + +impl Drop for BitcoindStopper { + fn drop(&mut self) { + self.close_handle().close() + } +} + pub(crate) struct BitcoindMock { pub url: String, pub server: Server, + stopper: BitcoindStopper, } +#[derive(Default)] pub(crate) struct MockOptions { error_code: Option, - block_hash: Option, - height: Option, + in_mempool: bool, } impl MockOptions { - pub fn new(error_code: i64, block_hash: BlockHash, height: usize) -> Self { - Self { - error_code: Some(error_code), - block_hash: Some(block_hash), - height: Some(height), - } - } - - pub fn empty() -> Self { - Self { - error_code: None, - block_hash: None, - height: None, - } - } - pub fn with_error(error_code: i64) -> Self { Self { error_code: Some(error_code), - block_hash: None, - height: None, + in_mempool: false, } } - #[allow(dead_code)] - pub fn with_block(block_hash: BlockHash, height: usize) -> Self { + pub fn in_mempool() -> Self { Self { error_code: None, - block_hash: Some(block_hash), - height: Some(height), + in_mempool: true, } } } @@ -558,15 +565,10 @@ impl BitcoindMock { Err(JsonRpcError::new(JsonRpcErrorCode::ServerError(error))) }); io.add_alias("sendrawtransaction", "error"); + io.add_alias("getrawtransaction", "error"); } else { BitcoindMock::add_sendrawtransaction(&mut io); - } - - if let Some(block_hash) = options.block_hash { - BitcoindMock::add_getrawtransaction(&mut io, block_hash.to_string()); - if let Some(height) = options.height { - BitcoindMock::add_getblockheader(&mut io, block_hash.to_string(), height); - } + BitcoindMock::add_getrawtransaction(&mut io, options.in_mempool); } let server = ServerBuilder::new(io) @@ -576,6 +578,7 @@ impl BitcoindMock { Self { url: format!("http://{}", server.address()), + stopper: BitcoindStopper::new(server.close_handle()), server, } } @@ -586,41 +589,25 @@ impl BitcoindMock { }); } - fn add_getrawtransaction(io: &mut IoHandler, block_hash: String) { + fn add_getrawtransaction(io: &mut IoHandler, in_mempool: bool) { io.add_sync_method("getrawtransaction", move |_params: Params| { - match _params { - Params::Array(x) => match x[1] { - Value::Bool(x) => { - if x { - Ok(serde_json::json!({"hex": TX_HEX, "txid": TXID_HEX, "hash": TXID_HEX, "size": 0, - "vsize": 0, "version": 1, "locktime": 0, "vin": [], "vout": [], "blockhash": block_hash })) - } else { - Ok(Value::String(TX_HEX.to_owned())) + if !in_mempool { + Err(JsonRpcError::new(JsonRpcErrorCode::ServerError(rpc_errors::RPC_INVALID_ADDRESS_OR_KEY as i64))) + } else { + match _params { + Params::Array(x) => match x[1] { + Value::Bool(x) => { + if x { + Ok(serde_json::json!({"hex": TX_HEX, "txid": TXID_HEX, "hash": TXID_HEX, "size": 0, + "vsize": 0, "version": 1, "locktime": 0, "vin": [], "vout": [] })) + } else { + Ok(Value::String(TX_HEX.to_owned())) + } } - } - _ => panic!("Boolean param not found"), - }, - _ => panic!("No params found"), - } - }) - } - - fn add_getblockheader(io: &mut IoHandler, block_hash: String, height: usize) { - io.add_sync_method("getblockheader", move |_params: Params| { - match _params { - Params::Array(x) => match x[1] { - Value::Bool(x) => { - if x { - Ok(serde_json::json!({"hash": block_hash, "confirmations": 1, "height": height, "version": 1, - "merkleroot": "4eca41cf0fa551346842eb317564a403e39553444790a65f949f95bc18d24643", "time": 1645719068, "nonce": 2, "bits": "207fffff", - "difficulty": 0.0, "chainwork": "0000000000000000000000000000000000000000000000000000000000001146", "nTx": 1})) - } else { - Ok(Value::String(TX_HEX.to_owned())) - } - } - _ => panic!("Boolean param not found"), - }, - _ => panic!("No params found"), + _ => panic!("Boolean param not found"), + }, + _ => panic!("No params found"), + } } }) } @@ -630,8 +617,8 @@ impl BitcoindMock { } } -pub(crate) fn start_server(bitcoind: BitcoindMock) { +pub(crate) fn start_server(server: Server) { thread::spawn(move || { - bitcoind.server.wait(); + server.wait(); }); } diff --git a/teos/src/tx_index.rs b/teos/src/tx_index.rs new file mode 100644 index 00000000..1ff7052b --- /dev/null +++ b/teos/src/tx_index.rs @@ -0,0 +1,401 @@ +use std::collections::{HashMap, VecDeque}; +use std::fmt; +use std::hash::Hash; + +use bitcoin::hash_types::BlockHash; +use bitcoin::{BlockHeader, Transaction, Txid}; +use lightning_block_sync::poll::ValidatedBlock; + +use teos_common::appointment::Locator; + +/// A trait implemented by types that can be used as key in a [TxIndex]. +pub trait Key: Hash { + fn from_txid(txid: Txid) -> Self; +} + +impl Key for Txid { + fn from_txid(txid: Txid) -> Self { + txid + } +} + +impl Key for Locator { + fn from_txid(txid: Txid) -> Self { + Locator::new(txid) + } +} + +pub enum Type { + Transaction, + BlockHash, +} + +pub enum Data { + Transaction(Transaction), + BlockHash(BlockHash), +} + +impl fmt::Display for Data { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Data::Transaction(_) => write!(f, "Transaction"), + Data::BlockHash(_) => write!(f, "BlockHash"), + } + } +} + +/// A trait implemented by types that can be used as value in a [TxIndex]. +pub trait Value { + fn get_type() -> Type; + fn from_data(d: Data) -> Self; +} + +impl Value for BlockHash { + fn get_type() -> Type { + Type::BlockHash + } + + fn from_data(d: Data) -> Self { + match d { + Data::BlockHash(b) => b, + other => panic!("Cannot build a BlockHash from {}", other), + } + } +} + +impl Value for Transaction { + fn get_type() -> Type { + Type::Transaction + } + + fn from_data(d: Data) -> Self { + match d { + Data::Transaction(t) => t, + other => panic!("Cannot build a BlockHash from {}", other), + } + } +} + +/// Data structure used to index locators computed from parsed blocks. +/// +/// Holds up to `size` blocks with their corresponding computed [Locator]s. +#[derive(Debug)] +pub struct TxIndex { + /// A [K]:[V] map. + index: HashMap, + /// Vector of block hashes covered by the index. + blocks: VecDeque, + /// Map of [BlockHash]:[Vec]. Used to remove data from the index. + tx_in_block: HashMap>, + /// The height of the last block included in the index. + tip: u32, + /// Maximum size of the index. + size: usize, +} + +impl TxIndex +where + K: Key + std::cmp::Eq + Copy, + V: Value + Clone, + Self: Sized, +{ + pub fn new(last_n_blocks: &[ValidatedBlock], height: u32) -> Self { + let size = last_n_blocks.len(); + let mut tx_index = Self { + index: HashMap::new(), + blocks: VecDeque::with_capacity(size), + tx_in_block: HashMap::new(), + tip: height, + size, + }; + + for block in last_n_blocks.iter().rev() { + if let Some(prev_block_hash) = tx_index.blocks.back() { + if block.header.prev_blockhash != *prev_block_hash { + panic!("last_n_blocks contains unchained blocks"); + } + }; + + let map = block + .txdata + .iter() + .map(|tx| { + ( + K::from_txid(tx.txid()), + match V::get_type() { + Type::Transaction => V::from_data(Data::Transaction(tx.clone())), + Type::BlockHash => { + V::from_data(Data::BlockHash(block.header.block_hash())) + } + }, + ) + }) + .collect(); + + tx_index.update(block.header, &map); + } + + tx_index + } + + /// Gets an item from the index if present. [None] otherwise. + pub fn get<'a>(&'a self, k: &'a K) -> Option<&V> { + self.index.get(k) + } + + /// Checks whether the index contains a certain key. + pub fn contains_key(&self, k: &K) -> bool { + self.index.contains_key(k) + } + + /// Checks if the index if full. + pub fn is_full(&self) -> bool { + self.blocks.len() > self.size + } + + /// Get's the height of a given block based on its position in the block queue. + pub fn get_height(&self, block_hash: &BlockHash) -> Option { + let pos = self.blocks.iter().position(|x| x == block_hash)?; + Some(self.tip as usize + pos + 1 - self.blocks.len()) + } + + /// Updates the index by adding data from a new block. Removes the oldest block if the index is full afterwards. + pub fn update(&mut self, block_header: BlockHeader, data: &HashMap) { + self.blocks.push_back(block_header.block_hash()); + + let ks = data + .iter() + .map(|(k, v)| { + self.index.insert(*k, v.clone()); + *k + }) + .collect(); + + self.tx_in_block.insert(block_header.block_hash(), ks); + + if self.is_full() { + // Avoid logging during bootstrap + log::info!("New block added to index: {}", block_header.block_hash()); + self.tip += 1; + self.remove_oldest_block(); + } + } + + /// Fixes the index by removing disconnected data. + pub fn remove_disconnected_block(&mut self, block_hash: &BlockHash) { + if let Some(ks) = self.tx_in_block.remove(block_hash) { + self.index.retain(|k, _| !ks.contains(k)); + + // Blocks should be disconnected from last backwards. Log if that's not the case so we can revisit this and fix it. + if let Some(ref h) = self.blocks.pop_back() { + if h != block_hash { + log::error!("Disconnected block does not match the oldest block stored in the TxIndex ({} != {})", block_hash, h); + } + } + } else { + log::warn!("The index is already empty"); + } + } + + /// Removes the oldest block from the index. + /// This removes data from `self.blocks`, `self.tx_in_block` and `self.index`. + pub fn remove_oldest_block(&mut self) { + let h = self.blocks.pop_front().unwrap(); + let ks = self.tx_in_block.remove(&h).unwrap(); + self.index.retain(|k, _| !ks.contains(k)); + + log::info!("Oldest block removed from index: {}", h); + } +} + +impl fmt::Display for TxIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "index: {:?}\n\nblocks: {:?}\n\ntx_in_block: {:?}\n\nsize: {}", + self.index, self.blocks, self.tx_in_block, self.size + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ops::Deref; + + use crate::test_utils::{get_last_n_blocks, Blockchain}; + + use bitcoin::Block; + + impl TxIndex + where + K: Key + std::cmp::Eq + Copy, + V: Value + Clone, + Self: Sized, + { + pub fn index_mut(&mut self) -> &mut HashMap { + &mut self.index + } + + pub fn blocks(&self) -> &VecDeque { + &self.blocks + } + } + + #[tokio::test] + async fn test_new() { + let height = 10; + let mut chain = Blockchain::default().with_height(height as usize); + let last_six_blocks = get_last_n_blocks(&mut chain, 6).await; + let blocks: Vec = last_six_blocks + .iter() + .map(|block| block.deref().clone()) + .collect(); + + let cache: TxIndex = TxIndex::new(&last_six_blocks, height); + assert_eq!(blocks.len(), cache.size); + for block in blocks.iter() { + assert!(cache.blocks().contains(&block.block_hash())); + + let mut locators = Vec::new(); + for tx in block.txdata.iter() { + let locator = Locator::new(tx.txid()); + assert!(cache.contains_key(&locator)); + locators.push(locator); + } + + assert_eq!(cache.tx_in_block[&block.block_hash()], locators); + } + } + + #[tokio::test] + async fn test_get_height() { + let cache_size = 10; + let height = 50; + let mut chain = Blockchain::default().with_height_and_txs(height, 42); + let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await; + + // last_n_blocks is ordered from latest to earliest + let first_block = last_n_blocks.get(cache_size - 1).unwrap(); + let last_block = last_n_blocks.get(0).unwrap(); + let mid = last_n_blocks.get(cache_size / 2).unwrap(); + + let cache: TxIndex = TxIndex::new(&last_n_blocks, height as u32); + + assert_eq!( + cache.get_height(&first_block.block_hash()).unwrap(), + height - cache_size + 1 + ); + assert_eq!(cache.get_height(&last_block.block_hash()).unwrap(), height); + assert_eq!( + cache.get_height(&mid.block_hash()).unwrap(), + height - cache_size / 2 + ); + } + + #[tokio::test] + async fn test_get_height_not_found() { + let cache_size = 10; + let height = 50; + let mut chain = Blockchain::default().with_height_and_txs(height, 42); + let cache: TxIndex = TxIndex::new( + &get_last_n_blocks(&mut chain, cache_size).await, + height as u32, + ); + + let fake_hash = BlockHash::default(); + assert!(matches!(cache.get_height(&fake_hash), None)); + } + + #[tokio::test] + async fn test_update() { + let height = 10; + let mut chain = Blockchain::default().with_height(height as usize); + let mut last_n_blocks = get_last_n_blocks(&mut chain, 7).await; + + // Store the last block to use it for an update and the first to check eviction + // Notice that the list of blocks is ordered from last to first. + let last_block = last_n_blocks.remove(0); + let first_block = last_n_blocks.last().unwrap().deref().clone(); + + // Init the cache with the 6 block before the last + let mut cache = TxIndex::new(&last_n_blocks, height); + + // Update the cache with the last block + let locator_tx_map = last_block + .txdata + .iter() + .map(|tx| (Locator::new(tx.txid()), tx.clone())) + .collect(); + + cache.update(last_block.deref().header, &locator_tx_map); + + // Check that the new data is in the cache + assert!(cache.blocks().contains(&last_block.block_hash())); + for (locator, _) in locator_tx_map.iter() { + assert!(cache.contains_key(locator)); + } + assert_eq!( + cache.tx_in_block[&last_block.block_hash()], + locator_tx_map.keys().cloned().collect::>() + ); + + // Check that the data from the first block has been evicted + assert!(!cache.blocks().contains(&first_block.block_hash())); + for tx in first_block.txdata.iter() { + assert!(!cache.contains_key(&Locator::new(tx.txid()))); + } + assert!(!cache.tx_in_block.contains_key(&first_block.block_hash())); + } + + #[tokio::test] + async fn test_remove_disconnected_block() { + let cache_size = 6; + let height = cache_size * 2; + let mut chain = Blockchain::default().with_height_and_txs(height, 42); + let mut cache: TxIndex = TxIndex::new( + &get_last_n_blocks(&mut chain, cache_size).await, + height as u32, + ); + + // TxIndex::fix removes the last connected block and removes all the associated data + for i in 0..cache_size { + let header = chain + .at_height(chain.get_block_count() as usize - i) + .deref() + .header; + let locators = cache.tx_in_block.get(&header.block_hash()).unwrap().clone(); + + // Make sure there's data regarding the target block in the cache before fixing it + assert_eq!(cache.blocks().len(), cache.size - i); + assert!(cache.blocks().contains(&header.block_hash())); + assert!(!locators.is_empty()); + for locator in locators.iter() { + assert!(cache.contains_key(locator)); + } + + cache.remove_disconnected_block(&header.block_hash()); + + // Check that the block data is not in the cache anymore + assert_eq!(cache.blocks().len(), cache.size - i - 1); + assert!(!cache.blocks().contains(&header.block_hash())); + assert!(cache.tx_in_block.get(&header.block_hash()).is_none()); + for locator in locators.iter() { + assert!(!cache.contains_key(locator)); + } + } + + // At this point the cache should be empty, fixing it further shouldn't do anything + for i in cache_size..cache_size * 2 { + assert!(cache.index.is_empty()); + assert!(cache.blocks().is_empty()); + assert!(cache.tx_in_block.is_empty()); + + let header = chain + .at_height(chain.get_block_count() as usize - i) + .deref() + .header; + cache.remove_disconnected_block(&header.block_hash()); + } + } +} diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index 750289b8..5d8b9587 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -4,12 +4,10 @@ use log; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; -use std::fmt; use std::iter::FromIterator; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -use bitcoin::hash_types::BlockHash; use bitcoin::secp256k1::SecretKey; use bitcoin::{BlockHeader, Transaction}; use lightning::chain; @@ -24,133 +22,7 @@ use crate::dbm::DBM; use crate::extended_appointment::{AppointmentSummary, ExtendedAppointment, UUID}; use crate::gatekeeper::{Gatekeeper, MaxSlotsReached, UserInfo}; use crate::responder::{ConfirmationStatus, Responder, TransactionTracker}; - -/// Data structure used to cache locators computed from parsed blocks. -/// -/// Holds up to `size` blocks with their corresponding computed [Locator]s. -#[derive(Debug)] -struct LocatorCache { - /// A [Locator]:[Transaction] map. - cache: HashMap, - /// Vector of block hashes corresponding to the cached blocks. - blocks: Vec, - /// Map of [BlockHash]:[Vec]. Used to remove data from the cache. - tx_in_block: HashMap>, - /// Maximum size of the cache. - size: usize, -} - -impl fmt::Display for LocatorCache { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "cache: {:?}\n\nblocks: {:?}\n\ntx_in_block: {:?}\n\nsize: {}", - self.cache, self.blocks, self.tx_in_block, self.size - ) - } -} - -impl LocatorCache { - /// Creates a new [LocatorCache] instance. - /// The cache is initialized using the provided vector of blocks. - /// The size of the cache is defined as the size of `last_n_blocks`. - /// - /// # Panics - /// - /// Panics if any of the blocks in `last_n_blocks` is unchained. That is, if the given blocks - /// are not linked in strict descending order. - fn new(last_n_blocks: Vec) -> LocatorCache { - let size = last_n_blocks.len(); - let mut cache = LocatorCache { - cache: HashMap::new(), - blocks: Vec::with_capacity(size), - tx_in_block: HashMap::new(), - size, - }; - - for block in last_n_blocks.into_iter().rev() { - if let Some(prev_block_hash) = cache.blocks.last() { - if block.header.prev_blockhash != *prev_block_hash { - panic!("last_n_blocks contains unchained blocks"); - } - }; - - let locator_tx_map = block - .txdata - .iter() - .map(|tx| (Locator::new(tx.txid()), tx.clone())) - .collect(); - - cache.update(block.header, &locator_tx_map); - } - - cache - } - - /// Gets a transaction from the cache if present. [None] otherwise. - fn get_tx(&self, locator: Locator) -> Option<&Transaction> { - self.cache.get(&locator) - } - - /// Checks if the cache if full. - fn is_full(&self) -> bool { - self.blocks.len() > self.size - } - - /// Updates the cache by adding data from a new block. Removes the oldest block if the cache is full afterwards. - fn update( - &mut self, - block_header: BlockHeader, - locator_tx_map: &HashMap, - ) { - self.blocks.push(block_header.block_hash()); - - let locators = locator_tx_map - .iter() - .map(|(l, tx)| { - self.cache.insert(*l, tx.clone()); - *l - }) - .collect(); - - self.tx_in_block.insert(block_header.block_hash(), locators); - - if self.is_full() { - // Avoid logging during bootstrap - log::info!("New block added to cache: {}", block_header.block_hash()); - self.remove_oldest_block(); - } - } - - /// Fixes the [LocatorCache] removing disconnected data. - fn fix(&mut self, header: &BlockHeader) { - if let Some(locators) = self.tx_in_block.remove(&header.block_hash()) { - for locator in locators.iter() { - self.cache.remove(locator); - } - - // Blocks should be disconnected from last backwards. Log if that's not the case so we can revisit this and fix it. - if let Some(h) = self.blocks.pop() { - if h != header.block_hash() { - log::error!("Disconnected block does not match the oldest block stored in the LocatorCache ({} != {})", header.block_hash(), h); - } - } - } else { - log::warn!("The cache is already empty"); - } - } - - /// Removes the oldest block from the cache. - /// This removes data from `self.blocks`, `self.tx_in_block` and `self.cache`. - fn remove_oldest_block(&mut self) { - let oldest_hash = self.blocks.remove(0); - for locator in self.tx_in_block.remove(&oldest_hash).unwrap() { - self.cache.remove(&locator); - } - - log::info!("Oldest block removed from cache: {}", oldest_hash); - } -} +use crate::tx_index::TxIndex; /// Structure holding data regarding a breach. /// @@ -241,7 +113,7 @@ pub struct Watcher { /// A map between [Locator]s (user identifiers for [Appointment]s) and [UUID]s (tower identifiers). locator_uuid_map: Mutex>>, /// A cache of the [Locator]s computed for the transactions in the last few blocks. - locator_cache: Mutex, + locator_cache: Mutex>, /// A [Responder] instance. Data will be passed to it once triggered (if valid). responder: Arc, /// A [Gatekeeper] instance. Data regarding users is requested to it. @@ -261,7 +133,7 @@ impl Watcher { pub fn new( gatekeeper: Arc, responder: Arc, - last_n_blocks: Vec, + last_n_blocks: &[ValidatedBlock], last_known_block_height: u32, signing_key: SecretKey, tower_id: TowerId, @@ -282,7 +154,7 @@ impl Watcher { Watcher { appointments: Mutex::new(appointments), locator_uuid_map: Mutex::new(locator_uuid_map), - locator_cache: Mutex::new(LocatorCache::new(last_n_blocks)), + locator_cache: Mutex::new(TxIndex::new(last_n_blocks, last_known_block_height)), responder, gatekeeper, last_known_block_height: AtomicU32::new(last_known_block_height), @@ -362,7 +234,7 @@ impl Watcher { .locator_cache .lock() .unwrap() - .get_tx(extended_appointment.locator()) + .get(&extended_appointment.locator()) { // Appointments that were triggered in blocks held in the cache Some(dispute_tx) => { @@ -678,11 +550,13 @@ impl Watcher { updated_users: &HashMap, reason: DeletionReason, ) { - self.delete_appointments_from_memory(uuids, reason); - self.dbm - .lock() - .unwrap() - .batch_remove_appointments(uuids, updated_users); + if !uuids.is_empty() { + self.delete_appointments_from_memory(uuids, reason); + self.dbm + .lock() + .unwrap() + .batch_remove_appointments(uuids, updated_users); + } } /// Ges the number of users currently registered with the tower. @@ -877,7 +751,10 @@ impl chain::Listen for Watcher { /// Fixes the [LocatorCache] by removing the disconnected data and updates the last_known_block_height. fn block_disconnected(&self, header: &BlockHeader, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); - self.locator_cache.lock().unwrap().fix(header); + self.locator_cache + .lock() + .unwrap() + .remove_disconnected_block(&header.block_hash()); self.last_known_block_height .store(height - 1, Ordering::Release); } @@ -894,8 +771,8 @@ mod tests { use crate::rpc_errors; use crate::test_utils::{ create_carrier, create_responder, create_watcher, generate_dummy_appointment, - generate_dummy_appointment_with_user, generate_uuid, get_last_n_blocks, get_random_breach, - get_random_tx, store_appointment_and_fks_to_db, BitcoindMock, Blockchain, MockOptions, + generate_dummy_appointment_with_user, generate_uuid, get_random_breach, get_random_tx, + store_appointment_and_fks_to_db, BitcoindMock, BitcoindStopper, Blockchain, MockOptions, MockedServerQuery, AVAILABLE_SLOTS, DURATION, EXPIRY_DELTA, SLOTS, START_HEIGHT, SUBSCRIPTION_EXPIRY, SUBSCRIPTION_START, }; @@ -905,7 +782,7 @@ mod tests { use bitcoin::hash_types::Txid; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, Secp256k1}; - use bitcoin::Block; + use lightning::chain::Listen; impl PartialEq for Watcher { @@ -934,13 +811,16 @@ mod tests { } } - async fn init_watcher(chain: &mut Blockchain) -> Watcher { + async fn init_watcher(chain: &mut Blockchain) -> (Watcher, BitcoindStopper) { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); init_watcher_with_db(chain, dbm).await } - async fn init_watcher_with_db(chain: &mut Blockchain, dbm: Arc>) -> Watcher { - let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + async fn init_watcher_with_db( + chain: &mut Blockchain, + dbm: Arc>, + ) -> (Watcher, BitcoindStopper) { + let bitcoind_mock = BitcoindMock::new(MockOptions::default()); let gk = Arc::new(Gatekeeper::new( chain.get_block_count(), @@ -949,7 +829,7 @@ mod tests { EXPIRY_DELTA, dbm.clone(), )); - let responder = create_responder(chain.tip(), gk.clone(), dbm.clone(), bitcoind_mock.url()); + let responder = create_responder(chain, gk.clone(), dbm.clone(), bitcoind_mock.url()).await; create_watcher( chain, Arc::new(responder), @@ -977,128 +857,12 @@ mod tests { assert_eq!(TowerId(recovered_pk), tower_id); } - #[tokio::test] - async fn test_cache_new() { - let mut chain = Blockchain::default().with_height(10); - let last_six_blocks = get_last_n_blocks(&mut chain, 6).await; - let blocks: Vec = last_six_blocks - .iter() - .map(|block| block.deref().clone()) - .collect(); - - let cache = LocatorCache::new(last_six_blocks); - assert_eq!(blocks.len(), cache.size); - for block in blocks.iter() { - assert!(cache.blocks.contains(&block.block_hash())); - - let mut locators = Vec::new(); - for tx in block.txdata.iter() { - let locator = Locator::new(tx.txid()); - assert!(cache.cache.contains_key(&locator)); - locators.push(locator); - } - - assert_eq!(cache.tx_in_block[&block.block_hash()], locators); - } - } - - #[tokio::test] - async fn test_cache_update() { - let mut chain = Blockchain::default().with_height(10); - let mut last_n_blocks = get_last_n_blocks(&mut chain, 7).await; - - // Safe the last block to use it for an update and the first to check eviction - // Notice that the list of blocks is ordered from last to first. - let last_block = last_n_blocks.remove(0); - let first_block = last_n_blocks.last().unwrap().deref().clone(); - - // Init the cache with the 6 block before the last - let mut cache = LocatorCache::new(last_n_blocks); - - // Update the cache with the last block - let locator_tx_map = last_block - .txdata - .iter() - .map(|tx| (Locator::new(tx.txid()), tx.clone())) - .collect(); - - cache.update(last_block.deref().header, &locator_tx_map); - - // Check that the new data is in the cache - assert!(cache.blocks.contains(&last_block.block_hash())); - for (locator, _) in locator_tx_map.iter() { - assert!(cache.cache.contains_key(locator)); - } - assert_eq!( - cache.tx_in_block[&last_block.block_hash()], - locator_tx_map.keys().cloned().collect::>() - ); - - // Check that the data from the first block has been evicted - assert!(!cache.blocks.contains(&first_block.block_hash())); - for tx in first_block.txdata.iter() { - assert!(!cache.cache.contains_key(&Locator::new(tx.txid()))); - } - assert!(!cache.tx_in_block.contains_key(&first_block.block_hash())); - } - - #[tokio::test] - async fn test_cache_fix() { - let cache_size = 6; - let mut chain = Blockchain::default().with_height_and_txs(cache_size * 2, 42); - - let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await; - - // Init the cache with the 6 block before the last - let mut cache = LocatorCache::new(last_n_blocks); - - // LocatorCache::fix removes the last connected block and removes all the associated data - for i in 0..cache_size { - let header = chain - .at_height(chain.get_block_count() as usize - i) - .deref() - .header; - let locators = cache.tx_in_block.get(&header.block_hash()).unwrap().clone(); - - // Make sure there's data regarding the target block in the cache before fixing it - assert_eq!(cache.blocks.len(), cache.size - i); - assert!(cache.blocks.contains(&header.block_hash())); - assert!(!locators.is_empty()); - for locator in locators.iter() { - assert!(cache.cache.contains_key(locator)); - } - - cache.fix(&header); - - // Check that the block data is not in the cache anymore - assert_eq!(cache.blocks.len(), cache.size - i - 1); - assert!(!cache.blocks.contains(&header.block_hash())); - assert!(cache.tx_in_block.get(&header.block_hash()).is_none()); - for locator in locators.iter() { - assert!(!cache.cache.contains_key(locator)); - } - } - - // At this point the cache should be empty, fixing it further shouldn't do anything - for i in cache_size..cache_size * 2 { - assert!(cache.cache.is_empty()); - assert!(cache.blocks.is_empty()); - assert!(cache.tx_in_block.is_empty()); - - let header = chain - .at_height(chain.get_block_count() as usize - i) - .deref() - .header; - cache.fix(&header); - } - } - #[tokio::test] async fn test_new() { // A fresh watcher has no associated data let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let watcher = init_watcher_with_db(&mut chain, dbm.clone()).await; + let (watcher, _s) = init_watcher_with_db(&mut chain, dbm.clone()).await; assert!(watcher.is_fresh()); let (user_sk, user_pk) = get_random_keypair(); @@ -1116,7 +880,7 @@ mod tests { } // Create a new Responder reusing the same DB and check that the data is loaded - let another_w = init_watcher_with_db(&mut chain, dbm).await; + let (another_w, _as) = init_watcher_with_db(&mut chain, dbm).await; assert!(!another_w.is_fresh()); assert_eq!(watcher, another_w); } @@ -1127,7 +891,7 @@ mod tests { // Not testing the update / rejection logic, since that's already covered in the Gatekeeper, just that the data makes // sense and the signature verifies. let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; let tower_pk = watcher.tower_id.0; let (_, user_pk) = get_random_keypair(); @@ -1152,7 +916,7 @@ mod tests { async fn test_add_appointment() { let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); let tip_txs = chain.blocks.last().unwrap().txdata.clone(); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // add_appointment should add a given appointment to the Watcher given the following logic: // - if the appointment does not exist for a given user, add the appointment @@ -1288,10 +1052,11 @@ mod tests { // Transaction rejected // Update the Responder with a new Carrier - *watcher.responder.get_carrier().lock().unwrap() = create_carrier( + let (carrier, _as) = create_carrier( MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), chain.tip().deref().height, ); + *watcher.responder.get_carrier().lock().unwrap() = carrier; let dispute_tx = &tip_txs[tip_txs.len() - 2]; let invalid_appointment = generate_dummy_appointment(Some(&dispute_tx.txid())).inner; @@ -1374,7 +1139,7 @@ mod tests { #[tokio::test] async fn test_store_appointment() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // Register the user let (_, user_pk) = get_random_keypair(); @@ -1435,7 +1200,7 @@ mod tests { #[tokio::test] async fn test_store_triggered_appointment() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // Register the user let (_, user_pk) = get_random_keypair(); @@ -1460,10 +1225,11 @@ mod tests { // A properly formatted but invalid transaction should be rejected by the Responder // Update the Responder with a new Carrier that will reject the transaction - *watcher.responder.get_carrier().lock().unwrap() = create_carrier( + let (carrier, _as) = create_carrier( MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), chain.tip().deref().height, ); + *watcher.responder.get_carrier().lock().unwrap() = carrier; let dispute_tx = get_random_tx(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); @@ -1497,7 +1263,7 @@ mod tests { #[tokio::test] async fn test_get_appointment() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; let appointment = generate_dummy_appointment(None).inner; @@ -1603,7 +1369,7 @@ mod tests { async fn test_get_breaches() { let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); let txs = chain.blocks.last().unwrap().txdata.clone(); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // Let's create some locators based on the transactions in the last block let mut locator_tx_map = HashMap::new(); @@ -1635,7 +1401,7 @@ mod tests { async fn test_filter_breaches() { let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 12); let txs = chain.blocks.last().unwrap().txdata.clone(); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // Let's create some locators based on the transactions in the last block let mut locator_tx_map = HashMap::new(); @@ -1704,7 +1470,7 @@ mod tests { #[tokio::test] async fn test_delete_appointments_from_memory() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // Add some appointments both to memory and to the database let mut to_be_deleted = HashMap::new(); @@ -1755,7 +1521,7 @@ mod tests { // TODO: This is an adaptation of Responder::test_delete_trackers, merge together once the method // is implemented using generics. let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // Delete appointments removes data from the appointments and locator_uuid_map // Add data to the map first @@ -1886,7 +1652,7 @@ mod tests { #[tokio::test] async fn test_filtered_block_connected() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // block_connected for the Watcher is used to keep track of what new transactions has been mined whose may be potential // channel breaches. @@ -2032,10 +1798,11 @@ mod tests { watcher.add_appointment(appointment.inner, sig).unwrap(); // Set the carrier response - *watcher.responder.get_carrier().lock().unwrap() = create_carrier( + let (carrier, _as) = create_carrier( MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), chain.tip().deref().height, ); + *watcher.responder.get_carrier().lock().unwrap() = carrier; watcher.block_connected( &chain.generate(Some(vec![dispute_tx])), @@ -2105,7 +1872,7 @@ mod tests { async fn test_block_disconnected() { let mut chain = Blockchain::default().with_height(START_HEIGHT); let start_height = START_HEIGHT as u32; - let watcher = init_watcher(&mut chain).await; + let (watcher, _s) = init_watcher(&mut chain).await; // block_disconnected for the Watcher fixes the locator cache by removing the disconnected block // and updates the last_known_block_height to the previous block height @@ -2114,7 +1881,7 @@ mod tests { .locator_cache .lock() .unwrap() - .blocks + .blocks() .contains(&last_block_header.block_hash())); watcher.block_disconnected(&last_block_header, start_height); @@ -2127,7 +1894,7 @@ mod tests { .locator_cache .lock() .unwrap() - .blocks + .blocks() .contains(&last_block_header.block_hash())); } } diff --git a/watchtower-plugin/Cargo.toml b/watchtower-plugin/Cargo.toml index fcf85f88..ae864f2e 100755 --- a/watchtower-plugin/Cargo.toml +++ b/watchtower-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "watchtower-plugin" -version = "0.1.0" +version = "0.1.2" authors = ["Sergi Delgado Segura "] license = "MIT" edition = "2018" @@ -16,7 +16,7 @@ path = "src/main.rs" backoff = { version = "0.4.0", features = ["tokio"] } hex = { version = "0.4.3", features = [ "serde" ] } home = "0.5.3" -reqwest = { version = "0.11", features = [ "blocking", "json" ] } +reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] } log = "0.4.16" rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } serde = "1.0.130" diff --git a/watchtower-plugin/README.md b/watchtower-plugin/README.md index 815a1a7f..f0738d9a 100644 --- a/watchtower-plugin/README.md +++ b/watchtower-plugin/README.md @@ -8,11 +8,15 @@ commitment transaction is generated. It also keeps a summary of the messages sen The plugin has the following methods: -- `registertower tower_id` : registers the user id (compressed public key) with a given tower. -- `list_towers`: lists all registered towers. -- `gettowerinfo tower_id`: gets all the locally stored data about a given tower. -- `retrytower tower_id`: tries to send pending appointment to a (previously) unreachable tower. -- `getappointment tower_id locator`: queries a given tower about an appointment. +- `registertower `: registers the user id (compressed public key) with a given tower. +- `gettowerinfo `: gets all the locally stored data about a given tower. +- `retrytower `: tries to send pending appointment to a (previously) unreachable tower. +- `abandontower `: deletes all data associated with a given tower. +- `listtowers`: lists all registered towers. +- `getappointment `: queries a given tower about an appointment. +- `getsubscriptioninfo `: gets the subscription information by querying the tower. +- `getappointmentreceipt `: pulls a given appointment receipt from the local database. +- `getregistrationreceipt `: pulls the latest registration receipt from the local database. The plugin also has an implicit method to send appointments to the registered towers for every new commitment transaction. @@ -21,7 +25,7 @@ The plugin also has an implicit method to send appointments to the registered to The first step to add the plugin to CLN is installing it. To do so you need to run (from the `rust-teos` folder): ``` -cargo install --path watchtower-plugin +cargo install --locked --path watchtower-plugin ``` That will generate a binary called `watchtower-client`. That's the binary we need to link to CLN. @@ -105,6 +109,9 @@ Config options can be setup directly in the [CLN config file](https://github.com - `watchtower-port`: default tower API port. - `watchtower-max-retry-time`: the maximum time a retry strategy will try to reach a temporary unreachable tower before giving up. +- `watchtower-proxy`: informs the plugin that you have a SOCKS5 proxy at the given `ip:port`. Notice this is necessary if you want to connect to a tower through Tor! + +**DISCLAIMER**: This option will be eventually replaced by the CoreLN `proxy` / `always-use-proxy` options. In the current state of the `cln-plugin` crate there is no option to access the CoreLN main configuration, therefore the need for a temporary, plugin-specific, option. # Getting started diff --git a/watchtower-plugin/src/dbm.rs b/watchtower-plugin/src/dbm.rs index 4a0c647c..84a73985 100755 --- a/watchtower-plugin/src/dbm.rs +++ b/watchtower-plugin/src/dbm.rs @@ -244,6 +244,15 @@ impl DBM { Ok(receipt) } + /// Removes a tower record from the database. + /// + /// This triggers a cascade deletion of all related data, such as appointments, appointment receipts, etc. As long as there is a single + /// reference to them. + pub fn remove_tower_record(&self, tower_id: TowerId) -> Result<(), Error> { + let query = "DELETE FROM towers WHERE tower_id=?"; + self.remove_data(query, params![tower_id.to_vec()]) + } + /// Loads all tower records from the database. pub fn load_towers(&self) -> HashMap { let mut towers = HashMap::new(); @@ -316,7 +325,32 @@ impl DBM { tx.commit() } - /// Loads the appointment receipts associated to a given tower + /// Loads a given appointment receipt of a given tower from the database. + pub fn load_appointment_receipt( + &self, + tower_id: TowerId, + locator: Locator, + ) -> Result { + let mut stmt = self + .connection + .prepare("SELECT * FROM appointment_receipts WHERE tower_id = ?1 and locator = ?2") + .unwrap(); + + stmt.query_row(params![tower_id.to_vec(), locator.to_vec()], |row| { + let start_block = row.get::<_, u32>(2).unwrap(); + let user_sig = row.get::<_, String>(3).unwrap(); + let tower_sig = row.get::<_, String>(4).unwrap(); + + Ok(AppointmentReceipt::with_signature( + user_sig, + start_block, + tower_sig, + )) + }) + .map_err(|_| Error::NotFound) + } + + /// Loads the appointment receipts associated to a given tower. /// /// TODO: Currently this is only loading a summary of the receipt, if we need to really load all the information /// for any reason this method may need to be renamed. @@ -371,6 +405,22 @@ impl DBM { appointments } + /// Loads an appointment from the database. + pub fn load_appointment(&self, locator: Locator) -> Result { + let mut stmt = self + .connection + .prepare("SELECT encrypted_blob, to_self_delay FROM appointments WHERE locator = ?") + .unwrap(); + + stmt.query_row(params![locator.to_vec()], |row| { + let encrypted_blob = row.get::<_, Vec>(0).unwrap(); + let to_self_delay = row.get::<_, u32>(1).unwrap(); + + Ok(Appointment::new(locator, encrypted_blob, to_self_delay)) + }) + .map_err(|_| Error::NotFound) + } + /// Stores an appointment into the database. /// /// Appointments are only stored as a whole when they are pending or invalid. @@ -623,6 +673,19 @@ mod tests { .unwrap(); stmt.exists(params![locator.to_vec()]).unwrap() } + + pub(crate) fn appointment_receipt_exists( + &self, + locator: Locator, + tower_id: TowerId, + ) -> bool { + let mut stmt = self + .connection + .prepare("SELECT * FROM appointment_receipts WHERE locator=?1 AND tower_id=?2 ") + .unwrap(); + stmt.exists(params![locator.to_vec(), tower_id.to_vec()]) + .unwrap() + } } #[test] @@ -776,6 +839,29 @@ mod tests { assert_eq!(dbm.load_towers(), HashMap::new()); } + #[test] + fn test_remove_tower_record() { + let mut dbm = DBM::in_memory().unwrap(); + + let tower_id = get_random_user_id(); + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + dbm.store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + assert!(matches!(dbm.remove_tower_record(tower_id), Ok(()))); + } + + #[test] + fn test_remove_tower_record_inexistent() { + let dbm = DBM::in_memory().unwrap(); + + assert!(matches!( + dbm.remove_tower_record(get_random_user_id()), + Err(Error::NotFound) + )); + } + #[test] fn test_store_load_appointment_receipts() { let mut dbm = DBM::in_memory().unwrap(); @@ -823,6 +909,57 @@ mod tests { assert_eq!(dbm.load_appointment_receipts(tower_id), receipts); } + #[test] + fn test_load_appointment_receipt() { + let mut dbm = DBM::in_memory().unwrap(); + let tower_id = get_random_user_id(); + let appointment = generate_random_appointment(None); + + // If there is no appointment receipt for the given (locator, tower_id) pair, Error::NotFound is returned + // Try first with both being unknown + assert!(matches!( + dbm.load_appointment_receipt(tower_id, appointment.locator), + Err(Error::NotFound) + )); + + // Add the tower but not the appointment and try again + let net_addr = "talaia.watch"; + let receipt = get_random_registration_receipt(); + dbm.store_tower_record(tower_id, net_addr, &receipt) + .unwrap(); + + assert!(matches!( + dbm.load_appointment_receipt(tower_id, appointment.locator), + Err(Error::NotFound) + )); + + // Add both + let tower_summary = TowerSummary::new( + net_addr.into(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + let appointment_receipt = AppointmentReceipt::with_signature( + "user_signature".into(), + 42, + "tower_signature".into(), + ); + dbm.store_appointment_receipt( + tower_id, + appointment.locator, + tower_summary.available_slots, + &appointment_receipt, + ) + .unwrap(); + + assert_eq!( + dbm.load_appointment_receipt(tower_id, appointment.locator) + .unwrap(), + appointment_receipt + ); + } + #[test] fn test_load_appointment_locators() { // `load_appointment_locators` is used to load locators from either `appointment_receipts`, `pending_appointments` or `invalid_appointments` @@ -889,7 +1026,27 @@ mod tests { ); } - // `store_appointments` is implicitly tested by `store_pending_appointment` and `store_invalid_appointment` + #[test] + fn test_store_load_appointment() { + let mut dbm = DBM::in_memory().unwrap(); + + let appointment = generate_random_appointment(None); + let tx = dbm.get_mut_connection().transaction().unwrap(); + DBM::store_appointment(&tx, &appointment).unwrap(); + tx.commit().unwrap(); + + let loaded_appointment = dbm.load_appointment(appointment.locator); + assert_eq!(appointment, loaded_appointment.unwrap()); + } + + #[test] + fn test_store_load_appointment_inexistent() { + let dbm = DBM::in_memory().unwrap(); + + let locator = generate_random_appointment(None).locator; + let loaded_appointment = dbm.load_appointment(locator); + assert!(matches!(loaded_appointment, Err(Error::NotFound))); + } #[test] fn test_store_pending_appointment() { diff --git a/watchtower-plugin/src/lib.rs b/watchtower-plugin/src/lib.rs index e23563df..dc7804c6 100755 --- a/watchtower-plugin/src/lib.rs +++ b/watchtower-plugin/src/lib.rs @@ -71,9 +71,14 @@ impl TowerStatus { *self == TowerStatus::Reachable } + /// Whether the tower is unreachable or not. + pub fn is_temporary_unreachable(&self) -> bool { + *self == TowerStatus::TemporaryUnreachable + } + /// Whether the tower is unreachable or not. pub fn is_unreachable(&self) -> bool { - *self == TowerStatus::TemporaryUnreachable || *self == TowerStatus::Unreachable + *self == TowerStatus::Unreachable } /// Whether the tower is misbehaving or not. @@ -411,7 +416,7 @@ mod tests { Vec::new(), ); - assert_eq!(tower_info.status, TowerStatus::Reachable); + assert!(tower_info.status.is_reachable()); assert!(tower_info.misbehaving_proof.is_none()); } diff --git a/watchtower-plugin/src/main.rs b/watchtower-plugin/src/main.rs index b4d14e55..07c883b1 100755 --- a/watchtower-plugin/src/main.rs +++ b/watchtower-plugin/src/main.rs @@ -22,7 +22,7 @@ use watchtower_plugin::net::http::{ add_appointment, post_request, process_post_response, AddAppointmentError, ApiResponse, RequestError, }; -use watchtower_plugin::retrier::Retrier; +use watchtower_plugin::retrier::RetryManager; use watchtower_plugin::wt_client::WTClient; use watchtower_plugin::TowerStatus; @@ -70,6 +70,8 @@ async fn register( tower_net_addr = format!("http://{}", tower_net_addr) } + let proxy = plugin.state().lock().unwrap().proxy.clone(); + let register_endpoint = format!("{}/register", tower_net_addr); log::info!("Registering in the Eye of Satoshi (tower_id={})", tower_id); @@ -79,6 +81,7 @@ async fn register( &common_msgs::RegisterRequest { user_id: user_id.to_vec(), }, + proxy, ) .await, ) @@ -131,6 +134,68 @@ async fn register( Ok(json!(receipt)) } +/// Gets the latest registration receipt from the client to a given tower (if it exists). +/// +/// This is pulled from the database +async fn get_registration_receipt( + plugin: Plugin>>, + v: serde_json::Value, +) -> Result { + let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?; + let state = plugin.state().lock().unwrap(); + + let response = state.get_registration_receipt(tower_id).map_err(|_| { + anyhow!( + "Cannot find {} within the known towers. Have you registered?", + tower_id + ) + })?; + + Ok(json!(response)) +} + +/// Gets the subscription information directly form the tower. +async fn get_subscription_info( + plugin: Plugin>>, + v: serde_json::Value, +) -> Result { + let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?; + + let (user_sk, tower_net_addr, proxy) = { + let state = plugin.state().lock().unwrap(); + if let Some(info) = state.towers.get(&tower_id) { + Ok((state.user_sk, info.net_addr.clone(), state.proxy.clone())) + } else { + Err(anyhow!("Unknown tower id: {}", tower_id)) + } + }?; + + let get_subscription_info = format!("{}/get_subscription_info", tower_net_addr); + let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk).unwrap(); + + let response: common_msgs::GetSubscriptionInfoResponse = process_post_response( + post_request( + &get_subscription_info, + &common_msgs::GetSubscriptionInfoRequest { signature }, + proxy, + ) + .await, + ) + .await + .map_err(|e| { + if e.is_connection() { + plugin + .state() + .lock() + .unwrap() + .set_tower_status(tower_id, TowerStatus::TemporaryUnreachable); + } + to_cln_error(e) + })?; + + Ok(json!(response)) +} + /// Gets information about an appointment from the tower. async fn get_appointment( plugin: Plugin>>, @@ -138,11 +203,10 @@ async fn get_appointment( ) -> Result { let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?; - let user_sk = plugin.state().lock().unwrap().user_sk; - let tower_net_addr = { + let (user_sk, tower_net_addr, proxy) = { let state = plugin.state().lock().unwrap(); if let Some(info) = state.towers.get(¶ms.tower_id) { - Ok(info.net_addr.clone()) + Ok((state.user_sk, info.net_addr.clone(), state.proxy.clone())) } else { Err(anyhow!("Unknown tower id: {}", params.tower_id)) } @@ -162,6 +226,7 @@ async fn get_appointment( locator: params.locator.to_vec(), signature, }, + proxy, ) .await, ) @@ -180,6 +245,36 @@ async fn get_appointment( Ok(json!(response)) } +/// Gets an appointment receipt from the client given a tower_id and a locator (if it exists). +/// +/// This is pulled from the database +async fn get_appointment_receipt( + plugin: Plugin>>, + v: serde_json::Value, +) -> Result { + let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?; + let state = plugin.state().lock().unwrap(); + + let response = state + .get_appointment_receipt(params.tower_id, params.locator) + .map_err(|_| { + if state.towers.contains_key(¶ms.tower_id) { + anyhow!( + "Cannot find {} within {}. Did you send that appointment?", + params.locator, + params.tower_id + ) + } else { + anyhow!( + "Cannot find {} within the known towers. Have you registered?", + params.tower_id + ) + } + })?; + + Ok(json!(response)) +} + /// Lists all the registered towers. /// /// The given information comes from memory, so it is summarized. @@ -223,24 +318,47 @@ async fn retry_tower( let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; let state = plugin.state().lock().unwrap(); if let Some(tower) = state.towers.get(&tower_id) { - if tower.status == TowerStatus::TemporaryUnreachable { + if tower.status.is_temporary_unreachable() { return Err(anyhow!("{} is already being retried", tower_id)); - } else if tower.status != TowerStatus::Unreachable { + } else if !tower.status.is_unreachable() { return Err(anyhow!( "Tower status must be unreachable to manually retry", )); } - state - .unreachable_towers - .send(tower_id) - .map_err(|e| anyhow!(e))?; + for locator in state + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .iter() + { + state + .unreachable_towers + .send((tower_id, *locator)) + .map_err(|e| anyhow!(e))?; + } Ok(json!(format!("Retrying {}", tower_id))) } else { Err(anyhow!("Unknown tower {}", tower_id)) } } +/// Forgets about a tower wiping out all local data associated to it. +async fn abandon_tower( + plugin: Plugin>>, + v: serde_json::Value, +) -> Result { + let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; + let mut state = plugin.state().lock().unwrap(); + if state.towers.get(&tower_id).is_some() { + state.remove_tower(tower_id).unwrap(); + Ok(json!(format!("{} successfully abandoned", tower_id))) + } else { + Err(anyhow!("Unknown tower {}", tower_id)) + } +} + /// Sends an appointment to all registered towers for every new commitment transaction. /// /// The appointment is built using the data provided by the backend (dispute txid and penalty transaction). @@ -284,9 +402,13 @@ async fn on_commitment_revocation( .map(|(id, info)| (*id, info.net_addr.clone(), info.status)) .collect::>(); + let proxy = plugin.state().lock().unwrap().proxy.clone(); + for (tower_id, net_addr, status) in towers { if status.is_reachable() { - match add_appointment(tower_id, &net_addr, &appointment, &signature).await { + match add_appointment(tower_id, &net_addr, proxy.clone(), &appointment, &signature) + .await + { Ok((slots, receipt)) => { plugin .state() @@ -307,17 +429,27 @@ async fn on_commitment_revocation( state.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable); state.add_pending_appointment(tower_id, &appointment); - state.unreachable_towers.send(tower_id).unwrap(); + state + .unreachable_towers + .send((tower_id, appointment.locator)) + .unwrap(); } } AddAppointmentError::ApiError(e) => match e.error_code { errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { - log::warn!("There is a subscription issue with {}", tower_id); + log::warn!( + "There is a subscription issue with {}. Adding {} to pending", + tower_id, + appointment.locator + ); let mut state = plugin.state().lock().unwrap(); state.set_tower_status(tower_id, TowerStatus::SubscriptionError); state.add_pending_appointment(tower_id, &appointment); - state.unreachable_towers.send(tower_id).unwrap(); + state + .unreachable_towers + .send((tower_id, appointment.locator)) + .unwrap(); } _ => { @@ -352,18 +484,28 @@ async fn on_commitment_revocation( } else { if status.is_subscription_error() { log::warn!( - "There is a subscription issue with {}. Adding appointment to pending", + "There is a subscription issue with {}. Adding {} to pending", tower_id, + appointment.locator ); } else { - log::warn!("{} is {}. Adding appointment to pending", tower_id, status); + log::warn!( + "{} is {}. Adding {} to pending", + tower_id, + status, + appointment.locator, + ); } - plugin - .state() - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); + let mut state = plugin.state().lock().unwrap(); + state.add_pending_appointment(tower_id, &appointment); + + if status.is_temporary_unreachable() { + state + .unreachable_towers + .send((tower_id, appointment.locator)) + .unwrap(); + } } } @@ -393,6 +535,11 @@ async fn main() -> Result<(), Error> { Value::Integer(900), "the time (in seconds) after where the retrier will give up trying to send data to a temporary unreachable tower", )) + .option(ConfigOption::new( + "watchtower-proxy", + Value::String(String::new()), + "Socks v5 proxy IP address and port for the watchtower client", + )) .option(ConfigOption::new( "dev-watchtower-max-retry-interval", Value::Integer(60), @@ -402,11 +549,24 @@ async fn main() -> Result<(), Error> { "registertower", "Registers the client public key (user id) with the tower.", register, + ).rpcmethod( + "getregistrationreceipt", + "Gets the latest registration receipt given a tower id.", + get_registration_receipt, ) .rpcmethod( "getappointment", "Gets appointment data from the tower given the tower id and the locator.", get_appointment, + ).rpcmethod( + "getappointmentreceipt", + "Gets a (local) appointment receipt given a tower id and an locator.", + get_appointment_receipt, + ) + .rpcmethod( + "getsubscriptioninfo", + "Gets the subscription information directly from the tower.", + get_subscription_info, ) .rpcmethod("listtowers", "Lists all registered towers.", list_towers) .rpcmethod( @@ -419,11 +579,26 @@ async fn main() -> Result<(), Error> { "Retries to send pending appointment to an unreachable tower.", retry_tower, ) + .rpcmethod( + "abandontower", + "Forgets about a tower and wipes all local data.", + abandon_tower, + ) .hook("commitment_revocation", on_commitment_revocation); if let Some(plugin) = builder.start().await? { // FIXME: This is a workaround. Ideally, `cln_plugin::options::Value` will implement `as_u64` so we can simply call and unwrap // given that we are certain the option exists. + state_clone.lock().unwrap().proxy = + if let Value::String(x) = plugin.option("watchtower-proxy").unwrap() { + if !x.is_empty() { + Some(x) + } else { + None + } + } else { + None + }; let max_elapsed_time = if let Value::Integer(x) = plugin.option("watchtower-max-retry-time").unwrap() { x as u16 @@ -440,8 +615,8 @@ async fn main() -> Result<(), Error> { 60 }; tokio::spawn(async move { - Retrier::new(state_clone, max_elapsed_time, max_interval_time) - .manage_retry(rx) + RetryManager::new(state_clone, rx, max_elapsed_time, max_interval_time) + .manage_retry() .await }); plugin.join().await diff --git a/watchtower-plugin/src/net/http.rs b/watchtower-plugin/src/net/http.rs index d00bb409..9cb17e4b 100644 --- a/watchtower-plugin/src/net/http.rs +++ b/watchtower-plugin/src/net/http.rs @@ -56,6 +56,7 @@ impl From for AddAppointmentError { pub async fn add_appointment( tower_id: TowerId, tower_net_addr: &str, + proxy: Option, appointment: &Appointment, signature: &str, ) -> Result<(u32, AppointmentReceipt), AddAppointmentError> { @@ -65,7 +66,7 @@ pub async fn add_appointment( tower_id ); let (response, receipt) = - send_appointment(tower_id, tower_net_addr, appointment, signature).await?; + send_appointment(tower_id, tower_net_addr, proxy, appointment, signature).await?; log::debug!("Appointment accepted and signed by {}", tower_id); log::debug!("Remaining slots: {}", response.available_slots); log::debug!("Start block: {}", response.start_block); @@ -77,6 +78,7 @@ pub async fn add_appointment( pub async fn send_appointment( tower_id: TowerId, tower_net_addr: &str, + proxy: Option, appointment: &Appointment, signature: &str, ) -> Result<(common_msgs::AddAppointmentResponse, AppointmentReceipt), AddAppointmentError> { @@ -89,6 +91,7 @@ pub async fn send_appointment( post_request( &format!("{}/add_appointment", tower_net_addr), &request_data, + proxy, ) .await, ) @@ -118,22 +121,39 @@ pub async fn send_appointment( } /// Generic function to post different types of requests to the tower. -pub async fn post_request(endpoint: &str, data: S) -> Result { - reqwest::Client::new() - .post(endpoint) - .json(&data) - .send() - .await - .map_err(|e| { - log::error!("{}", e); - if e.is_connect() | e.is_timeout() { - RequestError::ConnectionError( - "Cannot connect to the tower. Connection refused".into(), - ) - } else { - RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".into()) - } - }) +pub async fn post_request( + endpoint: &str, + data: S, + proxy: Option, +) -> Result { + let url = reqwest::Url::parse(endpoint).map_err(|e| { + RequestError::ConnectionError(format!("Cannot connect to the given URL. {}", e)) + })?; + let client = if url.host_str().unwrap().ends_with(".onion") { + if let Some(proxy) = proxy { + let proxy = reqwest::Proxy::http(format!("socks5h://{}", proxy)) + .map_err(|e| RequestError::ConnectionError(format!("{}", e)))?; + reqwest::Client::builder() + .proxy(proxy) + .build() + .map_err(|e| RequestError::ConnectionError(format!("{}", e)))? + } else { + return Err(RequestError::ConnectionError( + "Cannot connect to an onion address without a proxy".into(), + )); + } + } else { + reqwest::Client::new() + }; + + client.post(endpoint).json(&data).send().await.map_err(|e| { + log::debug!("POST request failed: {:?}", e); + if e.is_connect() | e.is_timeout() { + RequestError::ConnectionError("Cannot connect to the tower. Connection refused".into()) + } else { + RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".into()) + } + }) } /// Generic function to process the response of a given post request. @@ -202,6 +222,7 @@ mod tests { let (response, receipt) = add_appointment( TowerId(tower_pk), &format!("http://{}", server.address()), + None, &appointment, appointment_receipt.user_signature(), ) @@ -233,6 +254,7 @@ mod tests { let (response, receipt) = send_appointment( TowerId(tower_pk), &format!("http://{}", server.address()), + None, &appointment, appointment_receipt.user_signature(), ) @@ -265,6 +287,7 @@ mod tests { let error = send_appointment( tower_id, &format!("http://{}", server.address()), + None, &appointment, appointment_receipt.user_signature(), ) @@ -291,6 +314,7 @@ mod tests { let error = send_appointment( get_random_user_id(), "http://server_addr", + None, &generate_random_appointment(None), "user_sig", ) @@ -317,6 +341,7 @@ mod tests { let error = send_appointment( get_random_user_id(), &format!("http://{}", server.address()), + None, &generate_random_appointment(None), "user_sig", ) @@ -349,6 +374,7 @@ mod tests { let error = send_appointment( get_random_user_id(), &format!("http://{}", server.address()), + None, &generate_random_appointment(None), "user_sig", ) @@ -359,35 +385,6 @@ mod tests { assert!(matches!(error, AddAppointmentError::ApiError { .. })); } - #[tokio::test] - async fn test_send_appointment_unexpected() { - // An example to trigger an unexpected error would be to try to send data to a wrongly formatted url. - // This can not happen in the codebase, since the url is tested on registration, but it can be used to - // test that error path. Generally speaking, that error path should be unreachable. - let wrong_tower_net_addr = "server_addr"; - - let server = MockServer::start(); - server.mock(|when, then| { - when.method(POST).path("/add_appointment"); - then.status(200).header("content-type", "application/json"); - }); - - let error = send_appointment( - get_random_user_id(), - wrong_tower_net_addr, - &generate_random_appointment(None), - "user_sig", - ) - .await - .unwrap_err(); - - if let AddAppointmentError::RequestError(e) = error { - assert!(matches!(e, RequestError::Unexpected { .. })) - } else { - panic!("Funny enough, Unexpected error was expected") - } - } - #[tokio::test] async fn test_post_request() { let server = MockServer::start(); @@ -396,7 +393,7 @@ mod tests { then.status(200).header("content-type", "application/json"); }); - let response = post_request(&format!("http://{}", server.address()), json!("")) + let response = post_request(&format!("http://{}", server.address()), json!(""), None) .await .unwrap(); @@ -409,25 +406,13 @@ mod tests { let unreachable_server_url = "http://server_addr"; assert!(matches!( - post_request(unreachable_server_url, json!("")) + post_request(unreachable_server_url, json!(""), None,) .await .unwrap_err(), RequestError::ConnectionError { .. } )); } - #[tokio::test] - async fn test_post_request_unexpected_error() { - let malformed_server_url = "server_addr"; - - assert!(matches!( - post_request(malformed_server_url, json!("")) - .await - .unwrap_err(), - RequestError::Unexpected { .. } - )); - } - #[tokio::test] async fn test_process_post_response_json_error() { // `process_post_response` is a pass-trough function that maps json deserialization errors from `post_request`. @@ -440,7 +425,7 @@ mod tests { // Any expected response work here as long as it cannot be properly deserialized let error = process_post_response::>( - post_request(&format!("http://{}", server.address()), json!("")).await, + post_request(&format!("http://{}", server.address()), json!(""), None).await, ) .await .unwrap_err(); diff --git a/watchtower-plugin/src/retrier.rs b/watchtower-plugin/src/retrier.rs index 1f796fab..51f6b14e 100644 --- a/watchtower-plugin/src/retrier.rs +++ b/watchtower-plugin/src/retrier.rs @@ -1,165 +1,358 @@ +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::mpsc::{error::TryRecvError, UnboundedReceiver}; use backoff::future::retry_notify; use backoff::{Error, ExponentialBackoff}; +use teos_common::appointment::Locator; use teos_common::cryptography; use teos_common::errors; use teos_common::UserId as TowerId; use crate::net::http::{add_appointment, AddAppointmentError}; use crate::wt_client::WTClient; -use crate::AppointmentStatus; -pub struct Retrier { +pub struct RetryManager { wt_client: Arc>, + unreachable_towers: UnboundedReceiver<(TowerId, Locator)>, max_elapsed_time_secs: u16, max_interval_time_secs: u16, + retriers: HashMap>, } -impl Retrier { +impl RetryManager { pub fn new( wt_client: Arc>, + unreachable_towers: UnboundedReceiver<(TowerId, Locator)>, max_elapsed_time_secs: u16, max_interval_time_secs: u16, ) -> Self { - Self { + RetryManager { wt_client, + unreachable_towers, max_elapsed_time_secs, max_interval_time_secs, + retriers: HashMap::new(), } } - pub async fn manage_retry(&self, mut unreachable_towers: UnboundedReceiver) { + + /// Starts the retry manager's main logic loop. + /// This method will keep running until the `unreachable_towers` sender disconnects. + /// + /// It will receive any `(tower_id, locator)` pair and try to send the appointment with `locator` to + /// the tower with `tower_id`. This is done by spawning a tokio thread for each `tower_id` that tries + /// to send all the pending appointments. + pub async fn manage_retry(&mut self) { log::info!("Starting retry manager"); loop { - let tower_id = unreachable_towers.recv().await.unwrap(); - self.wt_client + match self.unreachable_towers.try_recv() { + Ok((tower_id, locator)) => { + // Not start a retry if the tower is flagged to be abandoned + if !self + .wt_client + .lock() + .unwrap() + .towers + .contains_key(&tower_id) + { + log::info!("Skipping retrying abandoned tower {}", tower_id); + continue; + } + self.add_pending_appointment(tower_id, locator); + } + Err(TryRecvError::Empty) => { + // Keep only running retriers and retriers ready to be started/re-started. + // This will remove failed ones and ones finished successfully and have no pending appointments. + // + // Note that a failed retrier could have received some new appointments to retry. In this case, we don't try to send + // them because we know that that tower is unreachable. We most likely received these new appointments while the tower + // was still flagged as temporarily unreachable when cleaning up after giving up retrying. + self.retriers.retain(|_, retrier| { + retrier.set_tower_status_if_failed(); + retrier.is_running() || retrier.should_start() + }); + // Start all the ready retriers. + for retrier in self.retriers.values() { + if retrier.should_start() { + self.start_retrying(retrier.clone()); + } + } + // Sleep to not waste a lot of CPU cycles. + tokio::time::sleep(Duration::from_secs(1)).await; + } + Err(TryRecvError::Disconnected) => break, + } + } + } + + /// Adds an appointment to pending for a given tower. + /// + /// If the tower is not currently being retried, a new entry for it is created, otherwise, the data is appended to the existing entry. + fn add_pending_appointment(&mut self, tower_id: TowerId, locator: Locator) { + if let std::collections::hash_map::Entry::Vacant(e) = self.retriers.entry(tower_id) { + log::debug!( + "Creating a new entry for tower {} with locator {}", + tower_id, + locator + ); + e.insert(Arc::new(Retrier::new( + self.wt_client.clone(), + tower_id, + locator, + ))); + } else { + log::debug!( + "Adding pending appointment {} to existing tower {}", + locator, + tower_id + ); + self.retriers + .get(&tower_id) + .unwrap() + .pending_appointments .lock() .unwrap() - .set_tower_status(tower_id, crate::TowerStatus::TemporaryUnreachable); + .insert(locator); + } + } + + fn start_retrying(&self, retrier: Arc) { + log::info!("Retrying tower {}", retrier.tower_id); + retrier.start(self.max_elapsed_time_secs, self.max_interval_time_secs); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RetrierStatus { + /// Retrier is stopped. This could happen if the retrier was never started or it started and + /// finished successfully. If a retrier is stopped and has some pending appointments, it should be + /// started/re-started, otherwise, it can be deleted safely. + Stopped, + /// Retrier is currently retrying the tower. If the retrier receives new appointments, it will + /// **try** to send them along (but it might not send them). + /// + /// If a retrier status is `Running`, then its associated tower is temporary unreachable. + Running, + /// Retrier failed retrying the tower. Should not be re-started. + /// + /// If a retrier status is `Failed`, then its associated tower is neither reachable nor temporary unreachable. + Failed, +} + +pub struct Retrier { + wt_client: Arc>, + tower_id: TowerId, + pending_appointments: Mutex>, + status: Mutex, +} + +impl Retrier { + pub fn new(wt_client: Arc>, tower_id: TowerId, locator: Locator) -> Self { + Self { + wt_client, + tower_id, + pending_appointments: Mutex::new(HashSet::from([locator])), + status: Mutex::new(RetrierStatus::Stopped), + } + } + + fn has_pending_appointments(&self) -> bool { + !self.pending_appointments.lock().unwrap().is_empty() + } + + fn set_status(&self, status: RetrierStatus) { + *self.status.lock().unwrap() = status; + } + + pub fn is_running(&self) -> bool { + *self.status.lock().unwrap() == RetrierStatus::Running + } + + pub fn should_start(&self) -> bool { + // A retrier can be started/re-started if it is stopped (i.e. not running and not failed) + // and has some pending appointments. + *self.status.lock().unwrap() == RetrierStatus::Stopped && self.has_pending_appointments() + } + + pub fn start(self: Arc, max_elapsed_time_secs: u16, max_interval_time_secs: u16) { + // We shouldn't be retrying failed and running retriers. + debug_assert_eq!(*self.status.lock().unwrap(), RetrierStatus::Stopped); - log::info!("Retrying tower {}", tower_id); - match retry_notify( + // Set the tower as temporary unreachable and the retrier status to running. + self.wt_client + .lock() + .unwrap() + .set_tower_status(self.tower_id, crate::TowerStatus::TemporaryUnreachable); + self.set_status(RetrierStatus::Running); + + tokio::spawn(async move { + let r = retry_notify( ExponentialBackoff { - max_elapsed_time: Some(Duration::from_secs(self.max_elapsed_time_secs as u64)), - max_interval: Duration::from_secs(self.max_interval_time_secs as u64), + max_elapsed_time: Some(Duration::from_secs(max_elapsed_time_secs as u64)), + max_interval: Duration::from_secs(max_interval_time_secs as u64), ..ExponentialBackoff::default() }, - || async { self.add_appointment(tower_id).await }, + || async { self.run().await }, |err, _| { - log::warn!("Retry error happened with {}. {}", tower_id, err); + log::warn!("Retry error happened with {}. {}", self.tower_id, err); }, ) - .await - { + .await; + + let mut state = self.wt_client.lock().unwrap(); + + match r { Ok(_) => { - log::info!("Retry strategy succeeded for {}", tower_id); - self.wt_client - .lock() - .unwrap() - .set_tower_status(tower_id, crate::TowerStatus::Reachable); + log::info!("Retry strategy succeeded for {}", self.tower_id); + // Set the tower status now so new appointment doesn't go to the retry manager. + state.set_tower_status(self.tower_id, crate::TowerStatus::Reachable); + // Retrier succeeded and can be re-used by re-starting it. + self.set_status(RetrierStatus::Stopped); } Err(e) => { - log::warn!("Retry strategy gave up for {}. {}", tower_id, e); // Notice we'll end up here after a permanent error. That is, either after finishing the backoff strategy - // unsuccessfully or by manually raising such an error (like when facing a tower misbehavior) - let mut wt_client = self.wt_client.lock().unwrap(); - if wt_client - .towers - .get(&tower_id) - .unwrap() - .status - .is_unreachable() - { - log::warn!("Setting {} as unreachable", tower_id); - wt_client.set_tower_status(tower_id, crate::TowerStatus::Unreachable); - } + // unsuccessfully or by manually raising such an error (like when facing a tower misbehavior). + log::warn!("Retry strategy gave up for {}. {}", self.tower_id, e); + + // Retrier failed and should be given up on. Avoid setting the tower status until the retrier is + // deleted/dropped. This way users performing manual retry will get an error as the tower will be + // temporary unreachable. + // We don't need to set the tower status now. Any new appointments we receive will not be retried anyways. + self.set_status(RetrierStatus::Failed); } } - } + }); } - async fn add_appointment(&self, tower_id: TowerId) -> Result<(), Error<&'static str>> { + async fn run(&self) -> Result<(), Error<&'static str>> { // Create a new scope so we can get all the data only locking the WTClient once. - let (appointments, net_addr, user_sk) = { + let (tower_id, net_addr, user_sk, proxy) = { let wt_client = self.wt_client.lock().unwrap(); - let appointments = wt_client - .dbm - .lock() + if wt_client.towers.get(&self.tower_id).is_none() { + return Err(Error::permanent("Tower was abandoned. Skipping retry")); + } + + if !self.has_pending_appointments() { + // will this ever happen ?? + // FIXME: success/Ok() here instead so not to mark the tower as unreachable. + return Err(Error::permanent("Tower has no data pending for retry")); + } + + let net_addr = wt_client + .towers + .get(&self.tower_id) .unwrap() - .load_appointments(tower_id, AppointmentStatus::Pending); - let net_addr = wt_client.towers.get(&tower_id).unwrap().net_addr.clone(); + .net_addr + .clone(); let user_sk = wt_client.user_sk; - (appointments, net_addr, user_sk) + (self.tower_id, net_addr, user_sk, wt_client.proxy.clone()) }; - for appointment in appointments { - match add_appointment( - tower_id, - &net_addr, - &appointment, - &cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(), - ) - .await - { - Ok((slots, receipt)) => { - let mut wt_client = self.wt_client.lock().unwrap(); - wt_client.add_appointment_receipt( - tower_id, - appointment.locator, - slots, - &receipt, - ); - wt_client.remove_pending_appointment(tower_id, appointment.locator); - log::debug!("Response verified and data stored in the database"); - } - Err(e) => { - match e { - AddAppointmentError::RequestError(e) => { - if e.is_connection() { - log::warn!( - "{} cannot be reached. Tower will be retried later", - tower_id, - ); - return Err(Error::transient("Tower cannot be reached")); - } - } - AddAppointmentError::ApiError(e) => match e.error_code { - errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { - log::warn!("There is a subscription issue with {}", tower_id); - return Err(Error::transient("Subscription error")); + while self.has_pending_appointments() { + let locators = self.pending_appointments.lock().unwrap().clone(); + for locator in locators.into_iter() { + let appointment = self + .wt_client + .lock() + .unwrap() + .dbm + .load_appointment(locator) + .unwrap(); + + match add_appointment( + tower_id, + &net_addr, + proxy.clone(), + &appointment, + &cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(), + ) + .await + { + Ok((slots, receipt)) => { + self.pending_appointments.lock().unwrap().remove(&locator); + let mut wt_client = self.wt_client.lock().unwrap(); + wt_client.add_appointment_receipt( + tower_id, + appointment.locator, + slots, + &receipt, + ); + wt_client.remove_pending_appointment(tower_id, appointment.locator); + log::debug!("Response verified and data stored in the database"); + } + Err(e) => { + match e { + AddAppointmentError::RequestError(e) => { + if e.is_connection() { + log::warn!( + "{} cannot be reached. Tower will be retried later", + tower_id, + ); + return Err(Error::transient("Tower cannot be reached")); + } } - _ => { - log::warn!( - "{} rejected the appointment. Error: {}, error_code: {}", - tower_id, - e.error, - e.error_code - ); - // We need to move the appointment from pending to invalid - // Add itn first to invalid and remove it from pending later so a cascade delete is not triggered - let mut wt_client = self.wt_client.lock().unwrap(); - wt_client.add_invalid_appointment(tower_id, &appointment); - wt_client.remove_pending_appointment(tower_id, appointment.locator); + AddAppointmentError::ApiError(e) => match e.error_code { + errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { + log::warn!("There is a subscription issue with {}", tower_id); + self.wt_client.lock().unwrap().set_tower_status( + tower_id, + crate::TowerStatus::SubscriptionError, + ); + return Err(Error::permanent("Subscription error")); + } + _ => { + log::warn!( + "{} rejected the appointment. Error: {}, error_code: {}", + tower_id, + e.error, + e.error_code + ); + // We need to move the appointment from pending to invalid + // Add it first to invalid and remove it from pending later so a cascade delete is not triggered + self.pending_appointments.lock().unwrap().remove(&locator); + let mut wt_client = self.wt_client.lock().unwrap(); + wt_client.add_invalid_appointment(tower_id, &appointment); + wt_client + .remove_pending_appointment(tower_id, appointment.locator); + } + }, + AddAppointmentError::SignatureError(proof) => { + log::warn!("Cannot recover known tower_id from the appointment receipt. Flagging tower as misbehaving"); + self.wt_client + .lock() + .unwrap() + .flag_misbehaving_tower(tower_id, proof); + return Err(Error::permanent("Tower misbehaved")); } - }, - AddAppointmentError::SignatureError(proof) => { - log::warn!("Cannot recover known tower_id from the appointment receipt. Flagging tower as misbehaving"); - self.wt_client - .lock() - .unwrap() - .flag_misbehaving_tower(tower_id, proof); - return Err(Error::permanent("Tower misbehaved")); } } } } } + Ok(()) } + + /// Sets the correct tower status if the retrier status is failed. + /// + /// This method MUST be called before getting rid of a failed retrier, and has + /// no effect on non-failed retriers. + pub fn set_tower_status_if_failed(&self) { + if *self.status.lock().unwrap() == RetrierStatus::Failed { + let mut state = self.wt_client.lock().unwrap(); + if let Some(tower) = state.towers.get(&self.tower_id) { + if tower.status.is_temporary_unreachable() { + log::warn!("Setting {} as unreachable", self.tower_id); + state.set_tower_status(self.tower_id, crate::TowerStatus::Unreachable); + } + } else { + log::info!("Skipping retrying abandoned tower {}", self.tower_id); + } + } + } } #[cfg(test)] @@ -168,7 +361,7 @@ mod tests { use httpmock::prelude::*; use serde_json::json; - use tokio::fs; + use tempdir::TempDir; use tokio::sync::mpsc::unbounded_channel; use teos_common::errors; @@ -185,8 +378,13 @@ mod tests { const MAX_INTERVAL_TIME: u16 = 1; impl Retrier { - fn dummy(wt_client: Arc>) -> Self { - Self::new(wt_client, 0, 0) + fn empty(wt_client: Arc>, tower_id: TowerId) -> Self { + Self { + wt_client, + tower_id, + pending_appointments: Mutex::new(HashSet::new()), + status: Mutex::new(RetrierStatus::Stopped), + } } } @@ -194,9 +392,11 @@ mod tests { // TODO: It'll be nice to toggle the mock on and off instead of having it always on. Not sure MockServer allows that though: // https://github.com/alexliesenfeld/httpmock/issues/67 async fn test_manage_retry_reachable() { - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new(WTClient::new(tmp_path.into(), tx.clone()).await)); + let wt_client = Arc::new(Mutex::new( + WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, + )); let server = MockServer::start(); // Add a tower with pending appointments @@ -234,11 +434,11 @@ mod tests { // Start the task and send the tower to the channel for retry let wt_client_clone = wt_client.clone(); let task = tokio::spawn(async move { - Retrier::new(wt_client_clone, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) - .manage_retry(rx) + RetryManager::new(wt_client_clone, rx, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) + .manage_retry() .await }); - tx.send(tower_id).unwrap(); + tx.send((tower_id, appointment.locator)).unwrap(); // Wait for the elapsed time and check how the tower status changed tokio::time::sleep(Duration::from_secs(MAX_ELAPSED_TIME as u64)).await; @@ -264,14 +464,15 @@ mod tests { api_mock.assert(); task.abort(); - fs::remove_dir_all(tmp_path).await.unwrap(); } #[tokio::test] async fn test_manage_retry_unreachable() { - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new(WTClient::new(tmp_path.into(), tx.clone()).await)); + let wt_client = Arc::new(Mutex::new( + WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, + )); // Add a tower with pending appointments let (_, tower_pk) = cryptography::get_random_keypair(); @@ -295,47 +496,44 @@ mod tests { let max_elapsed_time = MAX_ELAPSED_TIME + 1; let task = tokio::spawn(async move { - Retrier::new(wt_client_clone, max_elapsed_time, MAX_INTERVAL_TIME) - .manage_retry(rx) + RetryManager::new(wt_client_clone, rx, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) + .manage_retry() .await }); - tx.send(tower_id).unwrap(); + tx.send((tower_id, appointment.locator)).unwrap(); // Wait for the elapsed time and check how the tower status changed tokio::time::sleep(Duration::from_secs(max_elapsed_time as u64 / 3)).await; - assert_eq!( - wt_client - .lock() - .unwrap() - .towers - .get(&tower_id) - .unwrap() - .status, - TowerStatus::TemporaryUnreachable - ); + assert!(wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .status + .is_temporary_unreachable()); // Wait until the task gives up and check again tokio::time::sleep(Duration::from_secs(max_elapsed_time as u64)).await; - assert_eq!( - wt_client - .lock() - .unwrap() - .towers - .get(&tower_id) - .unwrap() - .status, - TowerStatus::Unreachable - ); + assert!(wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .status + .is_unreachable()); task.abort(); - fs::remove_dir_all(tmp_path).await.unwrap(); } #[tokio::test] async fn test_manage_retry_rejected() { - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new(WTClient::new(tmp_path.into(), tx.clone()).await)); + let wt_client = Arc::new(Mutex::new( + WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, + )); let server = MockServer::start(); // Add a tower with pending appointments @@ -369,11 +567,11 @@ mod tests { // Start the task and send the tower to the channel for retry let wt_client_clone = wt_client.clone(); let task = tokio::spawn(async move { - Retrier::new(wt_client_clone, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) - .manage_retry(rx) + RetryManager::new(wt_client_clone, rx, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) + .manage_retry() .await }); - tx.send(tower_id).unwrap(); + tx.send((tower_id, appointment.locator)).unwrap(); // Wait for the elapsed time and check how the tower status changed tokio::time::sleep(Duration::from_secs(MAX_ELAPSED_TIME as u64)).await; @@ -406,14 +604,15 @@ mod tests { api_mock.assert(); task.abort(); - fs::remove_dir_all(tmp_path).await.unwrap(); } #[tokio::test] - async fn test_retry_misbehaving() { - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + async fn test_manage_retry_misbehaving() { + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new(WTClient::new(tmp_path.into(), tx.clone()).await)); + let wt_client = Arc::new(Mutex::new( + WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, + )); let server = MockServer::start(); // Add a tower with pending appointments @@ -452,11 +651,11 @@ mod tests { // Start the task and send the tower to the channel for retry let wt_client_clone = wt_client.clone(); let task = tokio::spawn(async move { - Retrier::new(wt_client_clone, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) - .manage_retry(rx) + RetryManager::new(wt_client_clone, rx, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) + .manage_retry() .await }); - tx.send(tower_id).unwrap(); + tx.send((tower_id, appointment.locator)).unwrap(); // Wait for the elapsed time and check how the tower status changed tokio::time::sleep(Duration::from_secs(MAX_ELAPSED_TIME as u64)).await; @@ -471,16 +670,54 @@ mod tests { api_mock.assert(); task.abort(); - fs::remove_dir_all(tmp_path).await.unwrap(); } #[tokio::test] - async fn test_add_appointment() { + async fn test_manage_retry_abandoned() { + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); + let (tx, rx) = unbounded_channel(); + let wt_client = Arc::new(Mutex::new( + WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, + )); + let server = MockServer::start(); + + // Add a tower with pending appointments + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, server.base_url(), &receipt) + .unwrap(); + + // Remove the tower (to simulate it has been abandoned) + wt_client.lock().unwrap().remove_tower(tower_id).unwrap(); + + // Start the task and send the tower to the channel for retry + let wt_client_clone = wt_client.clone(); + let task = tokio::spawn(async move { + RetryManager::new(wt_client_clone, rx, MAX_ELAPSED_TIME, MAX_INTERVAL_TIME) + .manage_retry() + .await + }); + + // Send the id and check how it gets removed + tx.send((tower_id, generate_random_appointment(None).locator)) + .unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + assert!(!wt_client.lock().unwrap().towers.contains_key(&tower_id)); + + task.abort(); + } + + #[tokio::test] + async fn test_retry_tower() { let (tower_sk, tower_pk) = cryptography::get_random_keypair(); let tower_id = TowerId(tower_pk); - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.into(), unbounded_channel().0).await, + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); let server = MockServer::start(); @@ -514,20 +751,20 @@ mod tests { .json_body(json!(add_appointment_response)); }); - let r = Retrier::dummy(wt_client).add_appointment(tower_id).await; + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, appointment.locator); + let r = retrier.run().await; assert_eq!(r, Ok(())); api_mock.assert(); - - fs::remove_dir_all(tmp_path).await.unwrap(); } #[tokio::test] - async fn test_add_appointment_no_pending() { + async fn test_retry_tower_no_pending() { let (_, tower_pk) = cryptography::get_random_keypair(); let tower_id = TowerId(tower_pk); - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.into(), unbounded_channel().0).await, + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); let server = MockServer::start(); @@ -540,20 +777,20 @@ mod tests { .unwrap(); // If there are no pending appointments the method will simply return - let r = Retrier::dummy(wt_client).add_appointment(tower_id).await; - - assert_eq!(r, Ok(())); - - fs::remove_dir_all(tmp_path).await.unwrap(); + let r = Retrier::empty(wt_client, tower_id).run().await; + assert_eq!( + r, + Err(Error::permanent("Tower has no data pending for retry")) + ); } #[tokio::test] - async fn test_add_appointment_misbehaving() { + async fn test_retry_tower_misbehaving() { let (_, tower_pk) = cryptography::get_random_keypair(); let tower_id = TowerId(tower_pk); - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.into(), unbounded_channel().0).await, + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); let server = MockServer::start(); @@ -586,20 +823,21 @@ mod tests { .header("content-type", "application/json") .json_body(json!(add_appointment_response)); }); - let r = Retrier::dummy(wt_client).add_appointment(tower_id).await; + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, appointment.locator); + let r = retrier.run().await; assert_eq!(r, Err(Error::permanent("Tower misbehaved"))); api_mock.assert(); - - fs::remove_dir_all(tmp_path).await.unwrap(); } #[tokio::test] - async fn test_add_appointment_unreachable() { + async fn test_retry_tower_unreachable() { let (_, tower_pk) = cryptography::get_random_keypair(); let tower_id = TowerId(tower_pk); - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.into(), unbounded_channel().0).await, + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); // The tower we'd like to retry sending appointments to has to exist within the plugin @@ -616,19 +854,21 @@ mod tests { .lock() .unwrap() .add_pending_appointment(tower_id, &appointment); - let r = Retrier::dummy(wt_client).add_appointment(tower_id).await; - assert_eq!(r, Err(Error::transient("Tower cannot be reached"))); - fs::remove_dir_all(tmp_path).await.unwrap(); + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, appointment.locator); + let r = retrier.run().await; + + assert_eq!(r, Err(Error::transient("Tower cannot be reached"))); } #[tokio::test] - async fn test_add_appointment_subscription_error() { + async fn test_retry_tower_subscription_error() { let (_, tower_pk) = cryptography::get_random_keypair(); let tower_id = TowerId(tower_pk); - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.into(), unbounded_channel().0).await, + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); let server = MockServer::start(); @@ -656,21 +896,22 @@ mod tests { .lock() .unwrap() .add_pending_appointment(tower_id, &appointment); - let r = Retrier::dummy(wt_client).add_appointment(tower_id).await; - assert_eq!(r, Err(Error::transient("Subscription error"))); - api_mock.assert(); + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client, tower_id, appointment.locator); + let r = retrier.run().await; - fs::remove_dir_all(tmp_path).await.unwrap(); + assert_eq!(r, Err(Error::permanent("Subscription error"))); + api_mock.assert(); } #[tokio::test] - async fn test_add_appointment_rejected() { + async fn test_retry_tower_rejected() { let (_, tower_pk) = cryptography::get_random_keypair(); let tower_id = TowerId(tower_pk); - let tmp_path = &format!(".watchtower_{}/", get_random_user_id()); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.into(), unbounded_channel().0).await, + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); let server = MockServer::start(); @@ -698,9 +939,10 @@ mod tests { .lock() .unwrap() .add_pending_appointment(tower_id, &appointment); - let r = Retrier::dummy(wt_client.clone()) - .add_appointment(tower_id) - .await; + + // Since we are retrying manually, we need to add the data to pending appointments manually too + let retrier = Retrier::new(wt_client.clone(), tower_id, appointment.locator); + let r = retrier.run().await; assert_eq!(r, Ok(())); api_mock.assert(); @@ -712,7 +954,35 @@ mod tests { .unwrap() .invalid_appointments .contains(&appointment.locator)); + } + + #[tokio::test] + async fn test_retry_tower_abandoned() { + let (_, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); + let wt_client = Arc::new(Mutex::new( + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, + )); + let server = MockServer::start(); - fs::remove_dir_all(tmp_path).await.unwrap(); + // The tower we'd like to retry sending appointments to has to exist within the plugin + let receipt = get_random_registration_receipt(); + wt_client + .lock() + .unwrap() + .add_update_tower(tower_id, server.base_url(), &receipt) + .unwrap(); + + // Remove the tower (to simulate it has been abandoned) + wt_client.lock().unwrap().remove_tower(tower_id).unwrap(); + + // If there are no pending appointments the method will simply return + let r = Retrier::empty(wt_client, tower_id).run().await; + + assert_eq!( + r, + Err(Error::permanent("Tower was abandoned. Skipping retry")) + ); } } diff --git a/watchtower-plugin/src/wt_client.rs b/watchtower-plugin/src/wt_client.rs index e2bf439d..353323bd 100644 --- a/watchtower-plugin/src/wt_client.rs +++ b/watchtower-plugin/src/wt_client.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; use tokio::fs; use tokio::sync::mpsc::UnboundedSender; @@ -16,22 +15,26 @@ use crate::dbm::DBM; use crate::{MisbehaviorProof, SubscriptionError, TowerInfo, TowerStatus, TowerSummary}; /// Represents the watchtower client that is being used as the CoreLN plugin state. -#[derive(Clone)] pub struct WTClient { /// A [DBM] instance. - pub dbm: Arc>, + pub dbm: DBM, /// A collection of towers the client is registered to. pub towers: HashMap, /// Queue of unreachable towers - pub unreachable_towers: UnboundedSender, + pub unreachable_towers: UnboundedSender<(TowerId, Locator)>, /// The user secret key. pub user_sk: SecretKey, /// The user identifier. pub user_id: UserId, + /// Optional proxy + pub proxy: Option, } impl WTClient { - pub async fn new(data_dir: PathBuf, unreachable_towers: UnboundedSender) -> Self { + pub async fn new( + data_dir: PathBuf, + unreachable_towers: UnboundedSender<(TowerId, Locator)>, + ) -> Self { // Create data dir if it does not exist fs::create_dir_all(&data_dir).await.unwrap_or_else(|e| { log::error!("Cannot create data dir: {:?}", e); @@ -54,8 +57,10 @@ impl WTClient { let towers = dbm.load_towers(); for (tower_id, tower) in towers.iter() { - if tower.status.is_unreachable() { - unreachable_towers.send(*tower_id).unwrap(); + if tower.status.is_temporary_unreachable() { + for locator in tower.pending_appointments.iter() { + unreachable_towers.send((*tower_id, *locator)).unwrap(); + } } } @@ -67,9 +72,10 @@ impl WTClient { WTClient { towers, unreachable_towers, - dbm: Arc::new(Mutex::new(dbm)), + dbm, user_sk, user_id, + proxy: None, } } @@ -88,8 +94,6 @@ impl WTClient { } else { let previous_receipt = self .dbm - .lock() - .unwrap() .load_registration_receipt(tower_id, self.user_id) .unwrap(); if receipt.available_slots() <= previous_receipt.available_slots() { @@ -99,8 +103,6 @@ impl WTClient { } self.dbm - .lock() - .unwrap() .store_tower_record(tower_id, &tower_net_addr, receipt) .unwrap(); self.towers.insert( @@ -116,9 +118,17 @@ impl WTClient { Ok(()) } + /// Gets the latest registration receipt of a given tower. + pub fn get_registration_receipt( + &self, + tower_id: TowerId, + ) -> Result { + self.dbm.load_registration_receipt(tower_id, self.user_id) + } + /// Loads a tower record from the database. pub fn load_tower_info(&self, tower_id: TowerId) -> Result { - self.dbm.lock().unwrap().load_tower_record(tower_id) + self.dbm.load_tower_record(tower_id) } /// Sets the tower status to any of the `TowerStatus` variants. @@ -147,8 +157,6 @@ impl WTClient { tower.available_slots = available_slots; self.dbm - .lock() - .unwrap() .store_appointment_receipt(tower_id, locator, available_slots, receipt) .unwrap(); } else { @@ -159,14 +167,21 @@ impl WTClient { } } + /// Gets an appointment receipt from the database (if found). + pub fn get_appointment_receipt( + &self, + tower_id: TowerId, + locator: Locator, + ) -> Result { + self.dbm.load_appointment_receipt(tower_id, locator) + } + /// Adds a pending appointment to the tower record. pub fn add_pending_appointment(&mut self, tower_id: TowerId, appointment: &Appointment) { if let Some(tower) = self.towers.get_mut(&tower_id) { tower.pending_appointments.insert(appointment.locator); self.dbm - .lock() - .unwrap() .store_pending_appointment(tower_id, appointment) .unwrap(); } else { @@ -183,8 +198,6 @@ impl WTClient { tower.pending_appointments.remove(&locator); self.dbm - .lock() - .unwrap() .delete_pending_appointment(tower_id, locator) .unwrap(); } else { @@ -201,8 +214,6 @@ impl WTClient { tower.invalid_appointments.insert(appointment.locator); self.dbm - .lock() - .unwrap() .store_invalid_appointment(tower_id, appointment) .unwrap(); } else { @@ -216,16 +227,24 @@ impl WTClient { /// Flags a given tower as misbehaving, storing the misbehaving proof in the database. pub fn flag_misbehaving_tower(&mut self, tower_id: TowerId, proof: MisbehaviorProof) { if let Some(tower) = self.towers.get_mut(&tower_id) { - self.dbm - .lock() - .unwrap() - .store_misbehaving_proof(tower_id, &proof) - .unwrap(); + self.dbm.store_misbehaving_proof(tower_id, &proof).unwrap(); tower.status = TowerStatus::Misbehaving; } else { log::error!("Cannot flag tower. Unknown tower_id: {}", tower_id); } } + + /// Removes a tower from the client (both memory and database). + /// + /// Any data associated to the tower will be deleted (i.e. links to appointments) + pub fn remove_tower(&mut self, tower_id: TowerId) -> Result<(), DBError> { + if self.towers.contains_key(&tower_id) { + self.towers.remove(&tower_id); + self.dbm.remove_tower_record(tower_id) + } else { + Err(DBError::NotFound) + } + } } #[cfg(test)] @@ -474,13 +493,7 @@ mod tests { .pending_appointments .contains(&appointment.locator)); // This bit is tested exhaustively in the DBM. - assert!(!wt_client - .dbm - .lock() - .unwrap() - .appointment_exists(appointment.locator)); - - fs::remove_dir_all(tmp_path).await.unwrap(); + assert!(!wt_client.dbm.appointment_exists(appointment.locator)); } #[tokio::test] @@ -558,23 +571,13 @@ mod tests { .contains(&appointment.locator)); assert!(!wt_client .dbm - .lock() - .unwrap() .load_appointment_locators(tower_id, crate::AppointmentStatus::Pending) .contains(&appointment.locator)); assert!(wt_client .dbm - .lock() - .unwrap() .load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid) .contains(&appointment.locator)); - assert!(wt_client - .dbm - .lock() - .unwrap() - .appointment_exists(appointment.locator)); - - fs::remove_dir_all(tmp_path).await.unwrap(); + assert!(wt_client.dbm.appointment_exists(appointment.locator)); } #[tokio::test] @@ -623,14 +626,10 @@ mod tests { .contains(&appointment.locator)); assert!(!wt_client .dbm - .lock() - .unwrap() .load_appointment_locators(tower_id, crate::AppointmentStatus::Pending) .contains(&appointment.locator)); assert!(wt_client .dbm - .lock() - .unwrap() .load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid) .contains(&appointment.locator)); @@ -649,25 +648,15 @@ mod tests { .contains(&appointment.locator)); assert!(wt_client .dbm - .lock() - .unwrap() .load_appointment_locators(another_tower_id, crate::AppointmentStatus::Pending) .contains(&appointment.locator)); assert!(!wt_client .dbm - .lock() - .unwrap() .load_appointment_locators(another_tower_id, crate::AppointmentStatus::Invalid) .contains(&appointment.locator)); // GENERAL - assert!(wt_client - .dbm - .lock() - .unwrap() - .appointment_exists(appointment.locator)); - - fs::remove_dir_all(tmp_path).await.unwrap(); + assert!(wt_client.dbm.appointment_exists(appointment.locator)); } #[tokio::test] @@ -697,12 +686,147 @@ mod tests { // Check data in memory let tower_summary = wt_client.towers.get(&tower_id); assert!(tower_summary.is_some()); - assert_eq!(tower_summary.unwrap().status, TowerStatus::Misbehaving); + assert!(tower_summary.unwrap().status.is_misbehaving()); // Check data in DB let loaded_info = wt_client.load_tower_info(tower_id).unwrap(); - assert_eq!(loaded_info.status, TowerStatus::Misbehaving); + assert!(loaded_info.status.is_misbehaving()); assert_eq!(loaded_info.misbehaving_proof, Some(proof)); assert!(loaded_info.appointments.contains_key(&appointment.locator)); } + + #[tokio::test] + async fn test_remove_tower() { + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); + let mut wt_client = + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; + + let receipt = get_random_registration_receipt(); + let (tower_sk, tower_pk) = cryptography::get_random_keypair(); + let tower_id = TowerId(tower_pk); + let tower_info = TowerInfo::empty( + "talaia.watch".into(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + + // Add the tower and check it is there + wt_client + .add_update_tower(tower_id, tower_info.net_addr.clone(), &receipt) + .unwrap(); + assert_eq!( + wt_client.towers.get(&tower_id), + Some(&TowerSummary::from(tower_info.clone())) + ); + assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); + + // Remove the tower and check it is not there anymore + wt_client.remove_tower(tower_id).unwrap(); + assert!(matches!( + wt_client.load_tower_info(tower_id), + Err(DBError::NotFound) + )); + assert!(!wt_client.towers.contains_key(&tower_id)); + + // Try again but this time with an associated appointment to check that it also gets removed + wt_client + .add_update_tower(tower_id, tower_info.net_addr, &receipt) + .unwrap(); + + let locator = generate_random_appointment(None).locator; + let registration_receipt = get_random_registration_receipt(); + let appointment_receipt = get_random_appointment_receipt(tower_sk); + + // If we call this on an unknown tower it will simply do nothing + wt_client.add_appointment_receipt( + tower_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt, + ); + assert!(wt_client.dbm.appointment_receipt_exists(locator, tower_id)); + + // Remove and check both the tower and the appointment + wt_client.remove_tower(tower_id).unwrap(); + assert!(matches!( + wt_client.load_tower_info(tower_id), + Err(DBError::NotFound) + )); + assert!(!wt_client.towers.contains_key(&tower_id)); + assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower_id)); + } + + #[tokio::test] + async fn test_remove_tower_shared_appointment() { + // Lets test removing a tower that has associated data shared with another tower. + // For instance, having an appointment that was sent to two towers, and then deleting one of them + // should only remove the link between the tower and the appointment, but not delete the data. + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); + let mut wt_client = + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; + + let receipt = get_random_registration_receipt(); + let (tower1_sk, tower1_pk) = cryptography::get_random_keypair(); + let tower1_id = TowerId(tower1_pk); + let (tower2_sk, tower2_pk) = cryptography::get_random_keypair(); + let tower2_id = TowerId(tower2_pk); + + let tower_info = TowerInfo::empty( + "talaia.watch".into(), + receipt.available_slots(), + receipt.subscription_start(), + receipt.subscription_expiry(), + ); + wt_client + .add_update_tower(tower1_id, tower_info.net_addr.clone(), &receipt) + .unwrap(); + wt_client + .add_update_tower(tower2_id, tower_info.net_addr, &receipt) + .unwrap(); + + let locator = generate_random_appointment(None).locator; + let registration_receipt = get_random_registration_receipt(); + let appointment_receipt_1 = get_random_appointment_receipt(tower1_sk); + let appointment_receipt_2 = get_random_appointment_receipt(tower2_sk); + + wt_client.add_appointment_receipt( + tower1_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt_1, + ); + wt_client.add_appointment_receipt( + tower2_id, + locator, + registration_receipt.available_slots(), + &appointment_receipt_2, + ); + + // Check that the data exists in both towers + assert!(wt_client.dbm.appointment_receipt_exists(locator, tower1_id)); + assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id)); + + // Remove tower1 and check that the appointment receipt can still be found for tower2 + wt_client.remove_tower(tower1_id).unwrap(); + assert!(matches!( + wt_client.load_tower_info(tower1_id), + Err(DBError::NotFound) + )); + + assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower1_id)); + assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id)); + } + + #[tokio::test] + async fn test_remove_inexistent_tower() { + let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); + let mut wt_client = + WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; + + assert!(matches!( + wt_client.remove_tower(get_random_user_id()), + Err(DBError::NotFound) + )); + } } diff --git a/watchtower-plugin/tests/conftest.py b/watchtower-plugin/tests/conftest.py index ef7fac8e..3a97a67a 100644 --- a/watchtower-plugin/tests/conftest.py +++ b/watchtower-plugin/tests/conftest.py @@ -28,8 +28,9 @@ def __init__(self, directory="/tmp/watchtower-test"): def _call(self, method_name, *args): try: - r = subprocess.run(["teos-cli", f"--datadir={self.datadir}/teos", method_name, *args], capture_output=True, - text=True) + r = subprocess.run( + ["teos-cli", f"--datadir={self.datadir}/teos", method_name, *args], capture_output=True, text=True + ) if r.returncode != 0: result = ValueError(f"Unknown method {method_name}") else: @@ -123,3 +124,9 @@ def pytest_runtest_setup(item): pytest.skip("!DEVELOPER: {}".format(mark.args[0])) else: pytest.skip("!DEVELOPER: Requires DEVELOPER=1") + + +@pytest.fixture(scope="function", autouse=True) +def log_name(request): + # Here logging is used, you can use whatever you want to use for logs + logging.info("Starting '{}'".format(request.node.name)) diff --git a/watchtower-plugin/tests/pyproject.toml b/watchtower-plugin/tests/pyproject.toml index 56d2cd67..c81b8b0f 100644 --- a/watchtower-plugin/tests/pyproject.toml +++ b/watchtower-plugin/tests/pyproject.toml @@ -1,12 +1,13 @@ [tool.poetry] name = "tests" -version = "0.1.0" +version = "0.1.2" description = "watchtower-plugin tests" authors = ["Sergi Delgado Segura "] license = "MIT" [tool.poetry.dependencies] python = "^3.9" +black = "^22.6.0" [tool.poetry.dev-dependencies] pytest = "^7.1.2" diff --git a/watchtower-plugin/tests/test.py b/watchtower-plugin/tests/test.py index 8e5030b7..9050bf33 100644 --- a/watchtower-plugin/tests/test.py +++ b/watchtower-plugin/tests/test.py @@ -29,7 +29,7 @@ def test_watchtower(node_factory, bitcoind, teosd): l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": "watchtower-client"}]) # We need to register l2 with the tower - tower_id = teosd.cli.get_tower_info()["tower_id"] + tower_id = teosd.cli.gettowerinfo()["tower_id"] l2.rpc.registertower(tower_id) # Force a new commitment @@ -63,14 +63,14 @@ def test_watchtower(node_factory, bitcoind, teosd): assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded" # Generate blocks until the penalty gets irrevocably resolved - for i in range(100): + for i in range(101): bitcoind.generate_block(1) time.sleep(0.1) if i < 100: assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded" else: # Once the channel gets irrevocably resolved the tower will forget about it - assert l2.rpc.getappointment(tower_id, locator)["status"] == "not_found" + assert l2.rpc.getappointment(tower_id, locator) == {"error": "Appointment not found", "error_code": 36} # Make sure the penalty outputs are in l2's wallet fund_txids = [o["txid"] for o in l2.rpc.listfunds()["outputs"]] @@ -94,7 +94,7 @@ def test_unreachable_watchtower(node_factory, bitcoind, teosd): ) # We need to register l2 with the tower - tower_id = teosd.cli.get_tower_info()["tower_id"] + tower_id = teosd.cli.gettowerinfo()["tower_id"] l2.rpc.registertower(tower_id) # Stop the tower @@ -111,7 +111,6 @@ def test_unreachable_watchtower(node_factory, bitcoind, teosd): time.sleep(1) assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" - assert not l2.rpc.gettowerinfo(tower_id)["pending_appointments"] def test_retry_watchtower(node_factory, bitcoind, teosd): @@ -121,7 +120,7 @@ def test_retry_watchtower(node_factory, bitcoind, teosd): ) # We need to register l2 with the tower - tower_id = teosd.cli.get_tower_info()["tower_id"] + tower_id = teosd.cli.gettowerinfo()["tower_id"] l2.rpc.registertower(tower_id) # Stop the tower @@ -129,23 +128,37 @@ def test_retry_watchtower(node_factory, bitcoind, teosd): # Make a new payment with an unreachable tower l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) + + # The retrier manager waits 1 second before spawning new retriers for unreachable towers, + # so we need to wait a little bit until a retrier is started for our tower. + while l2.rpc.gettowerinfo(tower_id)["status"] == "temporary_unreachable": + time.sleep(1) assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] # Start the tower and retry it teosd.start() - l2.rpc.retrytower(tower_id) - time.sleep(2) + + # Even though we set the max retry time to zero seconds, the retrier manager takes some time (1s) to recognize + # that the tower is unreachable. So manual retries might fail as the tower is marked as temporary unreachable. + while True: + try: + l2.rpc.retrytower(tower_id) + break + except Exception: + time.sleep(1) + + while l2.rpc.gettowerinfo(tower_id)["pending_appointments"]: + time.sleep(1) assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" - assert not l2.rpc.gettowerinfo(tower_id)["pending_appointments"] def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory): l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": "watchtower-client", "allow_broken_log": True}]) # We need to register l2 with the tower - tower_id = teosd.cli.get_tower_info()["tower_id"] + tower_id = teosd.cli.gettowerinfo()["tower_id"] l2.rpc.registertower(tower_id) # Restart overwriting the tower private key @@ -156,3 +169,36 @@ def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory): l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) assert l2.rpc.gettowerinfo(tower_id)["status"] == "misbehaving" assert l2.rpc.gettowerinfo(tower_id)["misbehaving_proof"] + + +def test_get_appointment(node_factory, bitcoind, teosd, directory): + l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": "watchtower-client"}]) + + # We need to register l2 with the tower + tower_id = teosd.cli.gettowerinfo()["tower_id"] + l2.rpc.registertower(tower_id) + + # Force a new commitment + l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) + tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"] + + # Now make sure it is out of date + l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"]) + + # Now l1 cheats + dispute_txid = bitcoind.rpc.sendrawtransaction(tx) + locator = change_endianness(dispute_txid[32:]) + + # Check the appointment before mining a block + appointment = l2.rpc.getappointment(tower_id, locator)["appointment"] + assert "locator" in appointment and "encrypted_blob" in appointment and "to_self_delay" in appointment + + bitcoind.generate_block(1) + time.sleep(1) + + # And after. Now this should be a tracker + tracker = l2.rpc.getappointment(tower_id, locator)["appointment"] + assert "dispute_txid" in tracker and "penalty_txid" in tracker and "penalty_rawtx" in tracker + + # Manually stop l2, otherwise the tower may be stopped before the tower client and we may get some BROKEN logs. + l2.stop()