diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b1b91fe..e80276c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,10 @@ name: Continuous Integration Checks -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: jobs: build: @@ -16,13 +20,15 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust ${{ matrix.toolchain }} toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - override: true - profile: minimal + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Build on Rust ${{ matrix.toolchain }} run: | cargo build ${{ matrix.arguments }} --verbose --color always @@ -34,16 +40,43 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: toolchain: stable - profile: minimal components: rustfmt, clippy + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run rustfmt run: | cargo fmt --verbose --check -- --color always - name: Run clippy run: | - cargo clippy --all-features --all-targets --color always -- --deny warnings \ No newline at end of file + cargo clippy --all-features --all-targets --color always -- --deny warnings + + python-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Run black + uses: psf/black@stable + with: + src: "./watchtower-plugin/tests" + options: "--check -l 120" + + check-flake: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Check Nix flake inputs + uses: DeterminateSystems/flake-checker-action@v12 + - name: Install Nix + uses: cachix/install-nix-action@v31 + - name: Check flake + run: nix flake check + diff --git a/.github/workflows/cln-plugin.yaml b/.github/workflows/cln-plugin.yaml index b31482d..9451239 100644 --- a/.github/workflows/cln-plugin.yaml +++ b/.github/workflows/cln-plugin.yaml @@ -1,47 +1,73 @@ name: CI tests for CLN watchtower-plugin -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: env: - bitcoind_version: 0.20.1 - cln_version: 0.12.1 + bitcoind_version: "27.0" + cln_version: "24.11.1" jobs: + check-cln-cache: + runs-on: ubuntu-latest + outputs: + cache-hit: ${{ steps.cache.outputs.cache-hit }} + steps: + - name: Check CLN cache + id: cache + uses: actions/cache@v4 + with: + path: lightning + key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }} + cache-cln: runs-on: ubuntu-latest + needs: check-cln-cache + if: ${{ needs.check-cln-cache.outputs.cache-hit != 'true' }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' check-latest: true + - uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Create CLN cache - id: cache-cln - uses: actions/cache@v3 - env: - cache-name: cache-cln-dev + uses: actions/cache@v4 with: path: lightning - key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }} + key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }} - name: Compile CLN - env: + 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 + sudo apt-get update && sudo apt-get install -y gettext git clone https://github.com/ElementsProject/lightning.git && cd lightning && git checkout v${{ env.cln_version }} pip install --user poetry && poetry install - ./configure --enable-developer && poetry run make + ./configure && poetry run make -j 8 cln-plugin: needs: cache-cln + if: ${{ always() }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' check-latest: true + - uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt, clippy - name: Install bitcoind run: | wget https://bitcoincore.org/bin/bitcoin-core-${{ env.bitcoind_version }}/bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz @@ -49,24 +75,22 @@ jobs: ln -s $(pwd)/bitcoin-${{ env.bitcoind_version }}/bin/bitcoin* /usr/local/bin - name: Load CLN cache id: cache-cln - uses: actions/cache@v3 - env: - cache-name: cache-cln-dev + uses: actions/cache@v4 with: path: lightning - key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }} + key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }} - name: Link CLN run: | - cd lightning && sudo make install + cd lightning && sudo PATH=$PATH make install - name: Install teos and the plugin run: | - cargo install --locked --path teos + cargo install --locked --path teos cargo install --locked --path watchtower-plugin - name: Add test dependencies run: | cd watchtower-plugin/tests - pip install --user poetry && poetry install + pip install --user poetry && poetry install --no-root - name: Run tests run: | cd watchtower-plugin/tests - DEVELOPER=1 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s + VALGRIND=0 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s diff --git a/.gitignore b/.gitignore index 3b7e0f7..599e08c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target __pycache__ .vscode -.idea \ No newline at end of file +.idea +result diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90fd989..b646bcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,6 +72,35 @@ pub struct Responder { ## Test Coverage Tests should be provided to cover both positive and negative conditions. Tests should cover both the proper execution as well as all the covered error paths. PR with no proper test coverage will not be merged. -## Signing Commits +## Git conventions + +### Commits, titles, and descriptions + +- Changes must be split logically in commits, such that a commit is self-contained +- In general terms, all commits need to pass the test suite. There may be some exceptions to this rule if the change you are working on touches several components of the codebase and it makes more sense to split the change by component (or group of components) +- Commit titles need to be short and explanatory. If we are, for instance, adding an RPC command to the backend, "Adds command X to the backend" will be a good short description, "Add command" or "Fix #123" where #123 is an issue referencing this feature **IS NOT** +- Descriptions can be provided to give more context about what has been fixed and how + +### Pull requests + +- Pull request titles need to be explanatory, in the same way, commits titles were. If a PR includes a single commit, they can share the title, otherwise, a general title of what we are trying to achieve is required. **DO NOT REFERENCE ISSUES IN PULL REQUEST TITLES**, save that for the PR description +- PR descriptions need to guide the reviewer into what has been changed. You can reference issues here. If the PR is a fix of a simple issue, "Fix #123" may suffice, however, if it involves several changes, a proper explanation of both what has been fixed and how is due. These are two good examples of PR descriptions, both long and short: [188](https://github.com/talaia-labs/rust-teos/pull/188), [194](https://github.com/talaia-labs/rust-teos/pull/194) +- **WE DO NOT PILE "fix" COMMITS IN A PULL REQUEST**, that is, if some fixes are requested by reviewers, or something was missing from our original approach, it needs to be squashed. Do **NOT** do this: + + ``` + 886b0ff Adds X functionality to component Y + 801ff5d Fixes the previous commit because Z + 67ac345 Addresses review comments + 7dc7fcd Updates X because G was missing + b60999c Adds missing test + ... + ``` + +- Create a new branch to work on your pull request. **DO NOT** work from the master branch of your fork* +- **DO NOT** merge master into your branch, rebase master instead* + + \* If you're not sure how to handle this, check external documentation on how to manage multiple remotes for the same repository. + +### Signing Commits We require that all commits to be merged into master are signed. You can enable commit signing on GitHub by following [Signing commits](https://help.github.com/en/github/authenticating-to-github/signing-commits). diff --git a/Cargo.lock b/Cargo.lock index 0a9edf9..bbf6e71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.4.3" @@ -13,20 +28,20 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.5", + "getrandom 0.2.15", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -42,17 +57,53 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] -name = "ascii-canvas" -version = "3.0.0" +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ - "term", + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] @@ -65,172 +116,39 @@ dependencies = [ "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" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite 0.2.16", ] [[package]] name = "async-stream-impl" -version = "0.3.2" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] -[[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" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] -[[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" @@ -244,9 +162,54 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes 1.9.0", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "itoa 1.0.14", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite 0.2.16", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes 1.9.0", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] [[package]] name = "backoff" @@ -255,11 +218,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom 0.2.5", + "getrandom 0.2.15", "instant", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.16", "rand 0.8.5", - "tokio 1.20.1", + "tokio 1.43.0", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -268,80 +246,99 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] -name = "base64-compat" -version = "1.0.0" +name = "base64" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" -dependencies = [ - "byteorder", -] +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "basic-cookies" -version = "0.1.4" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" -dependencies = [ - "lalrpop", - "lalrpop-util", - "regex", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" -version = "0.8.1" +version = "0.11.0" 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" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bitcoin" -version = "0.28.1" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ - "base64-compat", + "base58ck", "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", "bitcoin_hashes", + "hex-conservative", + "hex_lit", "secp256k1", "serde", ] [[package]] -name = "bitcoin_hashes" -version = "0.10.0" +name = "bitcoin-internals" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" dependencies = [ "serde", ] [[package]] -name = "bitcoincore-rpc" -version = "0.15.0" +name = "bitcoin-io" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0e67dbf7a9971e7f4276f6089e9e814ce0f624a03216b7d92d00351ae7fb3e" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" dependencies = [ "bitcoincore-rpc-json", "jsonrpc", @@ -352,9 +349,9 @@ dependencies = [ [[package]] name = "bitcoincore-rpc-json" -version = "0.15.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e2ae16202721ba8c3409045681fac790a5ddc791f05731a2df22c0c6bffc0f1" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" dependencies = [ "bitcoin", "serde", @@ -367,6 +364,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + [[package]] name = "block-buffer" version = "0.9.0" @@ -379,9 +382,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -392,50 +395,27 @@ 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" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 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", + "serde", ] [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -445,27 +425,18 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.1.0" +version = "1.9.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" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.0.73" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -481,21 +452,21 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.7.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08493fa7707effc63254c66c6ea908675912493cd67952eda23c09fae2610b1" +checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" dependencies = [ "cfg-if 1.0.0", "cipher", - "cpufeatures", + "cpufeatures 0.1.5", "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.8.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6547abe025f4027edacd9edaa357aded014eecec42a5070d9b885c3c334aba2" +checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" dependencies = [ "aead", "chacha20", @@ -504,21 +475,11 @@ dependencies = [ "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" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "cipher" @@ -537,7 +498,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim", "textwrap", "unicode-width", @@ -546,40 +507,31 @@ dependencies = [ [[package]] name = "cln-plugin" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49be99e6e5ad55d420884b5b2a68aca890bcd1a1540ed6d2892363623a60f538" +checksum = "55eefc811f7d5280586dec7342824a84ab81f1d7e0cdb4cd579c1470e3e236cc" dependencies = [ "anyhow", - "bytes 1.1.0", - "env_logger", + "bytes 1.9.0", "futures", "log", "serde", "serde_json", - "tokio 1.20.1", + "tokio 1.43.0", "tokio-stream", - "tokio-util 0.7.0", + "tokio-util 0.7.13", + "tracing", + "tracing-subscriber", ] [[package]] name = "colored" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 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", + "windows-sys 0.59.0", ] [[package]] @@ -590,9 +542,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -600,40 +552,33 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" dependencies = [ "libc", ] [[package]] -name = "crossbeam-utils" -version = "0.8.10" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "cfg-if 1.0.0", - "once_cell", + "libc", ] -[[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" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -641,60 +586,19 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" 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" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -705,28 +609,18 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.7.0" 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", -] +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "der-parser" -version = "6.0.1" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cddf120f700b411b2b02ebeb7f04dc0b7c8835909a6c2f52bf72ed0dd3433b2" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ - "der-oid-macro", + "asn1-rs", + "displaydoc", "nom", "num-bigint", "num-traits", @@ -734,24 +628,27 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "0.99.17" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.96", ] -[[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" @@ -763,40 +660,36 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.4", "crypto-common", ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dnssec-prover" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi 0.3.9", -] +checksum = "96487aad690d45a83f2b9876828ba856c5430bbb143cb5730d8a5d04a4805179" [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "signature", ] @@ -817,68 +710,35 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.13.0" 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", -] +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if 1.0.0", ] [[package]] -name = "env_logger" -version = "0.10.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.2.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ - "errno-dragonfly", "libc", - "winapi 0.3.9", + "windows-sys 0.59.0", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[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" @@ -893,18 +753,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.7.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" @@ -929,11 +786,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "matches", "percent-encoding", ] @@ -949,7 +805,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fuchsia-zircon-sys", ] @@ -961,9 +817,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -976,9 +832,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -986,15 +842,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1003,53 +859,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.31" 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", -] +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1058,16 +899,16 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.16", "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1086,38 +927,44 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.5" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "globset" -version = "0.4.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" 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", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -1131,8 +978,8 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", - "indexmap", + "http 0.2.12", + "indexmap 1.9.3", "slab", "tokio 0.2.25", "tokio-util 0.3.1", @@ -1142,20 +989,20 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.11" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.1.0", + "bytes 1.9.0", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap", + "http 0.2.12", + "indexmap 2.7.1", "slab", - "tokio 1.20.1", - "tokio-util 0.6.9", + "tokio 1.43.0", + "tokio-util 0.7.13", "tracing", ] @@ -1168,29 +1015,46 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown", + "hashbrown 0.11.2", ] [[package]] name = "headers" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64", - "bitflags", - "bytes 1.1.0", + "base64 0.21.7", + "bytes 1.9.0", "headers-core", - "http", - "httpdate 1.0.2", + "http 0.2.12", + "httpdate 1.0.3", "mime", - "sha-1 0.10.0", + "sha1", ] [[package]] @@ -1199,7 +1063,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http", + "http 0.2.12", ] [[package]] @@ -1211,6 +1075,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1222,12 +1092,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1238,6 +1105,21 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hmac" version = "0.11.0" @@ -1250,22 +1132,33 @@ dependencies = [ [[package]] name = "home" -version = "0.5.3" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "winapi 0.3.9", + "windows-sys 0.59.0", ] [[package]] name = "http" -version = "0.2.6" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.1.0", + "bytes 1.9.0", "fnv", - "itoa 1.0.1", + "itoa 1.0.14", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes 1.9.0", + "fnv", + "itoa 1.0.14", ] [[package]] @@ -1275,25 +1168,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" dependencies = [ "bytes 0.5.6", - "http", + "http 0.2.12", ] [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.1.0", - "http", - "pin-project-lite 0.2.8", + "bytes 1.9.0", + "http 0.2.12", + "pin-project-lite 0.2.16", ] [[package]] name = "httparse" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -1303,43 +1196,9 @@ checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" 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 = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" @@ -1352,7 +1211,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.2.7", - "http", + "http 0.2.12", "http-body 0.3.1", "httparse", "httpdate 0.3.2", @@ -1367,23 +1226,23 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.1.0", + "bytes 1.9.0", "futures-channel", "futures-core", "futures-util", - "h2 0.3.11", - "http", - "http-body 0.4.4", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", - "httpdate 1.0.2", - "itoa 1.0.1", - "pin-project-lite 0.2.8", - "socket2 0.4.4", - "tokio 1.20.1", + "httpdate 1.0.3", + "itoa 1.0.14", + "pin-project-lite 0.2.16", + "socket2 0.5.8", + "tokio 1.43.0", "tower-service", "tracing", "want", @@ -1395,9 +1254,9 @@ 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", + "hyper 0.14.32", + "pin-project-lite 0.2.16", + "tokio 1.43.0", "tokio-io-timeout", ] @@ -1407,53 +1266,181 @@ 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", + "bytes 1.9.0", + "hyper 0.14.32", "native-tls", - "tokio 1.20.1", + "tokio 1.43.0", "tokio-native-tls", ] [[package]] -name = "idna" -version = "0.2.3" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", ] [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "io-lifetimes" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" -dependencies = [ - "libc", - "windows-sys 0.42.0", -] - [[package]] name = "iovec" version = "0.1.4" @@ -1465,54 +1452,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" - -[[package]] -name = "is-terminal" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" -dependencies = [ - "hermit-abi 0.2.6", - "io-lifetimes", - "rustix", - "windows-sys 0.42.0", -] - -[[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", -] +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "itertools" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1525,28 +1473,29 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.58" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "jsonrpc" -version = "0.12.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8423b78fc94d12ef1a4a9d13c348c9a78766dda0cc18817adf0faf77e670c8" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" dependencies = [ - "base64-compat", + "base64 0.13.1", + "minreq", "serde", - "serde_derive", "serde_json", ] @@ -1577,7 +1526,7 @@ dependencies = [ "jsonrpc-server-utils", "log", "net2", - "parking_lot", + "parking_lot 0.11.2", "unicase", ] @@ -1600,9 +1549,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.0" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures 0.2.17", +] [[package]] name = "kernel32-sys" @@ -1614,74 +1566,23 @@ dependencies = [ "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" +version = "1.5.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" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] -name = "libnghttp2-sys" -version = "0.1.7+1.45.0" +name = "libm" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" -dependencies = [ - "cc", - "libc", -] +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libsqlite3-sys" @@ -1694,100 +1595,125 @@ dependencies = [ "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" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d885bf509066af86ae85354c8959028ad6192c22a2657ef8271e94029d30f9d0" +checksum = "b3224b577def19c2bb3dcf2c35a95d94909183204c061746d1245ecc6e889e8e" dependencies = [ + "bech32", "bitcoin", + "dnssec-prover", + "hashbrown 0.13.2", + "libm", + "lightning-invoice", + "lightning-types", + "possiblyrandom", ] [[package]] name = "lightning-block-sync" -version = "0.0.108" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f1ed50f41785af19f5cd1225b668e87ef0d59bb84e6f8ef2542933e6082a2c" +checksum = "baab5bdee174a2047d939a4ca0dc2e1c23caa0f8cab0b4380aed77a20e116f1e" dependencies = [ "bitcoin", "chunked_transfer", - "futures", "lightning", - "serde", "serde_json", ] [[package]] -name = "lightning-net-tokio" -version = "0.0.108" +name = "lightning-invoice" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0170619152c4d6b947d5ed0de427b85691482a293e0cae52d4336a2220a776" +checksum = "d4254e7d05961a3728bc90737c522e7091735ba6f2f71014096d4b3eb4ee5d89" +dependencies = [ + "bech32", + "bitcoin", + "lightning-types", +] + +[[package]] +name = "lightning-net-tokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6a6c93b1e592f1d46bb24233cac4a33b4015c99488ee229927a81d16226e45" dependencies = [ "bitcoin", "lightning", - "tokio 1.20.1", + "tokio 1.43.0", +] + +[[package]] +name = "lightning-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cd84d4e71472035903e43caded8ecc123066ce466329ccd5ae537a8d5488c7" +dependencies = [ + "bitcoin", ] [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.16" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "cfg-if 1.0.0", - "value-bag", + "regex-automata 0.1.10", ] [[package]] -name = "matches" -version = "0.1.9" +name = "matchit" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.4.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -1799,6 +1725,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "minreq" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161" +dependencies = [ + "log", + "serde", + "serde_json", +] + [[package]] name = "mio" version = "0.6.23" @@ -1820,14 +1766,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "windows-sys 0.52.0", ] [[package]] @@ -1843,36 +1788,55 @@ dependencies = [ ] [[package]] -name = "multimap" -version = "0.8.3" +name = "mockito" +version = "0.32.5" 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" +checksum = "406f43768da5a859ce19bb0978fd8dc2167a7d9a52f3935c6a187242e1a4ff9f" dependencies = [ - "buf_redux", - "httparse", + "assert-json-diff", + "colored", + "futures", + "hyper 0.14.32", + "lazy_static", "log", - "mime", - "mime_guess", - "quick-error", "rand 0.8.5", - "safemem", - "tempfile", - "twoway", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio 1.43.0", ] [[package]] -name = "native-tls" -version = "0.2.10" +name = "multer" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes 1.9.0", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1886,108 +1850,125 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.37" +version = "0.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" 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" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] -name = "num-bigint" -version = "0.4.3" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi 0.3.9", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_threads" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] -name = "oid-registry" -version = "0.2.0" +name = "object" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe554cb2393bc784fd678c82c84cc0599c31ceadc7f03a594911f822cb8d1815" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ - "der-parser", + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", ] [[package]] name = "once_cell" -version = "1.9.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.40" +version = "0.10.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" dependencies = [ - "bitflags", + "bitflags 2.8.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -1998,28 +1979,27 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.74" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -2027,10 +2007,10 @@ dependencies = [ ] [[package]] -name = "parking" -version = "2.0.0" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" @@ -2040,81 +2020,90 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi 0.3.9", ] [[package]] -name = "pem" -version = "1.1.0" +name = "parking_lot_core" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ - "base64", + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.5.8", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", ] [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.0" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.7.1", ] -[[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" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -2125,9 +2114,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2137,22 +2126,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.31" 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", -] +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "poly1305" @@ -2160,22 +2136,44 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] [[package]] -name = "ppv-lite86" -version = "0.2.16" +name = "possiblyrandom" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "1b122a615d72104fb3d8b26523fdf9232cd8ee06949fb37e4ce3ff964d15dffd" +dependencies = [ + "getrandom 0.2.15", +] [[package]] -name = "precomputed-hash" -version = "0.1.1" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.96", +] [[package]] name = "proc-macro-error" @@ -2186,7 +2184,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -2203,100 +2201,71 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.8.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" 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", + "bytes 1.9.0", + "prost-derive", ] [[package]] name = "prost-build" -version = "0.9.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.1.0", - "heck", + "bytes 1.9.0", + "heck 0.5.0", "itertools", - "lazy_static", "log", "multimap", + "once_cell", "petgraph", - "prost 0.9.0", + "prettyplease", + "prost", "prost-types", "regex", + "syn 2.0.96", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.8.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" 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", + "syn 2.0.96", ] [[package]] name = "prost-types" -version = "0.9.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "bytes 1.1.0", - "prost 0.9.0", + "prost", ] -[[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" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2335,7 +2304,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2355,7 +2324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2384,11 +2353,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.5", + "getrandom 0.2.15", ] [[package]] @@ -2402,13 +2371,14 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.8.14" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5911d1403f4143c9d56a702069d593e8d0f3fab880a85e103604d0893ea31ba7" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ - "chrono", "pem", "ring", + "rustls-pki-types", + "time", "x509-parser", "yasna", ] @@ -2424,39 +2394,65 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] -name = "redox_users" -version = "0.4.0" +name = "redox_syscall" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "getrandom 0.2.5", - "redox_syscall", + "bitflags 2.8.0", ] [[package]] name = "regex" -version = "1.5.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "remove_dir_all" @@ -2469,32 +2465,35 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", - "bytes 1.1.0", + "base64 0.21.7", + "bytes 1.9.0", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.11", - "http", - "http-body 0.4.4", - "hyper 0.14.18", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "native-tls", + "once_cell", "percent-encoding", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.16", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "tokio 1.20.1", + "sync_wrapper", + "system-configuration", + "tokio 1.43.0", "tokio-native-tls", "tokio-socks", "tower-service", @@ -2507,17 +2506,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if 1.0.0", + "getrandom 0.2.15", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi 0.3.9", + "windows-sys 0.52.0", ] [[package]] @@ -2526,7 +2525,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2536,10 +2535,16 @@ dependencies = [ ] [[package]] -name = "rustc_version" -version = "0.4.0" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -2555,107 +2560,127 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.6" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.8.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.19.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ - "base64", "log", "ring", - "sct", - "webpki", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "rustversion" -version = "1.0.7" +name = "rustls-pemfile" +version = "1.0.4" 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" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "base64 0.21.7", ] [[package]] -name = "scoped-tls" -version = "1.0.0" +name = "rustls-pemfile" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "rustls-pki-types" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] -name = "sct" -version = "0.6.1" +name = "rustls-webpki" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] [[package]] -name = "secp256k1" -version = "0.22.1" +name = "rustversion" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", "secp256k1-sys", "serde", ] [[package]] name = "secp256k1-sys" -version = "0.5.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" dependencies = [ "cc", ] [[package]] name = "security-framework" -version = "2.6.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -2664,9 +2689,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2674,52 +2699,43 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ - "indexmap", - "itoa 1.0.1", + "indexmap 2.7.1", + "itoa 1.0.14", + "memchr", "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" @@ -2727,33 +2743,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.1", + "itoa 1.0.14", "ryu", "serde", ] [[package]] -name = "sha-1" -version = "0.9.8" +name = "sha1" +version = "0.10.6" 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" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.3", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -2764,7 +2767,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -2782,41 +2785,46 @@ dependencies = [ ] [[package]] -name = "signal-hook" -version = "0.3.14" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "libc", - "signal-hook-registry", + "lazy_static", ] [[package]] -name = "signal-hook-registry" -version = "1.4.0" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "signature" -version = "1.5.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "similar" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "simple_logger" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75a9723083573ace81ad0cdfc50b858aa3c366c48636edb4109d73122a0c0ea" +checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7" dependencies = [ "atty", "colored", @@ -2825,34 +2833,20 @@ dependencies = [ "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" +version = "0.4.9" 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" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "async-channel", - "futures-core", - "futures-io", + "autocfg", ] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -2867,32 +2861,25 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "winapi 0.3.9", + "windows-sys 0.52.0", ] [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "string_cache" -version = "0.8.3" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" -dependencies = [ - "lazy_static", - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" @@ -2917,24 +2904,24 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2942,15 +2929,52 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", - "syn", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -2965,22 +2989,23 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if 1.0.0", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] name = "teos" version = "0.2.0" dependencies = [ + "base64 0.22.1", "bitcoin", "bitcoincore-rpc", "hex", @@ -2990,7 +3015,7 @@ dependencies = [ "lightning-block-sync", "lightning-net-tokio", "log", - "prost 0.9.0", + "prost", "rand 0.8.5", "rcgen", "rusqlite", @@ -3000,10 +3025,10 @@ dependencies = [ "structopt", "tempdir", "teos-common", - "tokio 1.20.1", + "tokio 1.43.0", "tokio-stream", "toml", - "tonic 0.6.2", + "tonic", "tonic-build", "torut", "triggered", @@ -3018,35 +3043,15 @@ dependencies = [ "chacha20poly1305", "hex", "lightning", - "prost 0.9.0", + "prost", "rand 0.8.5", "rusqlite", "serde", "serde_json", - "tonic 0.6.2", + "tonic", "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 = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -3058,66 +3063,77 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", ] [[package]] name = "time" -version = "0.3.7" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ - "itoa 1.0.1", + "deranged", + "itoa 1.0.14", "libc", + "num-conv", "num_threads", + "powerfmt", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "time-macros" -version = "0.2.3" +version = "0.2.19" 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" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ - "crunchy", + "num-conv", + "time-core", ] [[package]] -name = "tinyvec" -version = "1.5.1" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[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" @@ -3138,22 +3154,20 @@ dependencies = [ [[package]] name = "tokio" -version = "1.20.1" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ - "autocfg", - "bytes 1.1.0", + "backtrace", + "bytes 1.9.0", "libc", - "memchr", - "mio 0.8.4", - "num_cpus", - "once_cell", - "pin-project-lite 0.2.8", + "mio 1.0.3", + "parking_lot 0.12.3", + "pin-project-lite 0.2.16", "signal-hook-registry", - "socket2 0.4.4", + "socket2 0.5.8", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.52.0", ] [[package]] @@ -3162,75 +3176,74 @@ 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", + "pin-project-lite 0.2.16", + "tokio 1.43.0", ] [[package]] name = "tokio-macros" -version = "1.7.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] name = "tokio-native-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.20.1", + "tokio 1.43.0", ] [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", - "tokio 1.20.1", - "webpki", + "rustls-pki-types", + "tokio 1.43.0", ] [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", "thiserror", - "tokio 1.20.1", + "tokio 1.43.0", ] [[package]] name = "tokio-stream" -version = "0.1.8" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", - "pin-project-lite 0.2.8", - "tokio 1.20.1", + "pin-project-lite 0.2.16", + "tokio 1.43.0", ] [[package]] name = "tokio-tungstenite" -version = "0.15.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "pin-project", - "tokio 1.20.1", + "tokio 1.43.0", "tungstenite", ] @@ -3250,115 +3263,67 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ - "bytes 1.1.0", + "bytes 1.9.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", + "pin-project-lite 0.2.16", + "tokio 1.43.0", ] [[package]] name = "toml" -version = "0.5.8" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "tonic" -version = "0.5.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796c5e1cd49905e65dd8e700d4cb1dffcbfdb4fc9d017de08c1a537afd83627c" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" 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", + "axum", + "base64 0.21.7", + "bytes 1.9.0", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.8.0", - "prost-derive 0.8.0", - "tokio 1.20.1", + "prost", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "tokio 1.43.0", "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" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" dependencies = [ + "prettyplease", "proc-macro2", "prost-build", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -3368,7 +3333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" dependencies = [ "base32", - "base64", + "base64 0.13.1", "derive_more", "ed25519-dalek", "hex", @@ -3378,24 +3343,24 @@ dependencies = [ "serde_derive", "sha2", "sha3", - "tokio 1.20.1", + "tokio 1.43.0", ] [[package]] name = "tower" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.16", "rand 0.8.5", "slab", - "tokio 1.20.1", - "tokio-util 0.7.0", + "tokio 1.43.0", + "tokio-util 0.7.13", "tower-layer", "tower-service", "tracing", @@ -3403,47 +3368,47 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.31" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.16", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" -version = "0.1.19" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.22" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ - "lazy_static", + "once_cell", + "valuable", ] [[package]] @@ -3456,6 +3421,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "triggered" version = "0.1.2" @@ -3464,97 +3458,64 @@ checksum = "ce148eae0d1a376c1b94ae651fc3261d9cb8294788b962b7382066376503a2d1" [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.14.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ - "base64", "byteorder", - "bytes 1.1.0", - "http", + "bytes 1.9.0", + "data-encoding", + "http 1.2.0", "httparse", "log", "rand 0.8.5", - "sha-1 0.9.8", + "sha1", "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" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.6.0" +version = "2.8.1" 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" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.16" 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", -] +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.14" 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" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "universal-hash" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" dependencies = [ "generic-array", "subtle", @@ -3562,19 +3523,18 @@ dependencies = [ [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.2.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -3585,14 +3545,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "value-bag" -version = "1.0.0-alpha.8" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" -dependencies = [ - "ctor", - "version_check", -] +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -3608,52 +3576,44 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" 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" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] [[package]] name = "warp" -version = "0.3.2" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ - "bytes 1.1.0", + "bytes 1.9.0", "futures-channel", "futures-util", "headers", - "http", - "hyper 0.14.18", + "http 0.2.12", + "hyper 0.14.32", "log", "mime", "mime_guess", - "multipart", + "multer", "percent-encoding", "pin-project", "scoped-tls", "serde", "serde_json", "serde_urlencoded", - "tokio 1.20.1", - "tokio-stream", + "tokio 1.43.0", "tokio-tungstenite", - "tokio-util 0.6.9", + "tokio-util 0.7.13", "tower-service", "tracing", ] @@ -3664,12 +3624,6 @@ 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" @@ -3677,47 +3631,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.81" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if 1.0.0", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", - "lazy_static", "log", "proc-macro2", "quote", - "syn", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if 1.0.0", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3725,22 +3690,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "watchtower-plugin" @@ -3751,58 +3719,28 @@ dependencies = [ "cln-plugin", "hex", "home", - "httpmock", "log", + "mockito", "reqwest", "rusqlite", "serde", "serde_json", "tempdir", "teos-common", - "tokio 1.20.1", - "tonic 0.5.2", + "tokio 1.43.0", + "tonic", ] [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 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" @@ -3831,15 +3769,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3848,113 +3777,183 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -3967,12 +3966,11 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.12.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc90836a84cb72e6934137b1504d0cae304ef5d83904beb0c8d773bbfe256ed" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ - "base64", - "chrono", + "asn1-rs", "data-encoding", "der-parser", "lazy_static", @@ -3981,34 +3979,122 @@ dependencies = [ "ring", "rusticata-macros", "thiserror", + "time", ] [[package]] name = "yasna" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "chrono", + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", ] [[package]] name = "zeroize" -version = "1.3.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.96", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] diff --git a/Cargo.toml b/Cargo.toml index 3a62379..a070916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] +resolver = "2" members = [ "teos", "teos-common", "watchtower-plugin" -] \ No newline at end of file +] diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 4efa204..3ea5e11 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -6,7 +6,7 @@ - `bitcoind` ### Minimum Supported Rust Version (MSRV) -FIXME: Define MSRV +Refer to [toolchain](./rust-toolchain.toml) ### Installing Rust Refer to [rust-lang.org](https://www.rust-lang.org/tools/install). diff --git a/README.md b/README.md index 4213e2d..0336b48 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -**THIS IS CURRENTLY WIP** - # The Eye of Satoshi (rust-teos) The Eye of Satoshi is a Lightning watchtower compliant with [BOLT13](https://github.com/sr-gi/bolt13), written in Rust. +[![discord](https://img.shields.io/discord/991334710611550208?logo=discord&style=plastic)](https://discord.gg/EyVbrNMDUP) +[![build](https://img.shields.io/github/actions/workflow/status/talaia-labs/rust-teos/build.yaml?logo=github&style=plastic)](https://github.com/talaia-labs/rust-teos/actions/workflows/build.yaml) +[![release](https://img.shields.io/github/v/release/talaia-labs/rust-teos?style=plastic)](https://github.com/talaia-labs/rust-teos/releases/latest) + + `rust-teos` consists of two main crates: - `teos`: including the tower's main functionality (server-side) and a CLI. Compiling this crate will generate two binaries: `teosd` and `teos-cli`. @@ -20,6 +23,8 @@ Refer to [INSTALL.md](INSTALL.md) Make sure `bitcoind` is running before running `teosd` (it will fail at startup if it cannot connect to `bitcoind`). [Here](DEPENDENCIES.md#installing-bitcoind) you can find a sample bitcoin.conf. +Please see [Docker instructions](docker/README.md) for instructions on how to set up `teosd` in Docker. + ### Starting the tower daemon â™– Once installed, you can start the tower by running: diff --git a/coffee.yml b/coffee.yml new file mode 100644 index 0000000..143466e --- /dev/null +++ b/coffee.yml @@ -0,0 +1,10 @@ +--- +plugin: + name: rust-teos + version: 0.2.0 + lang: rust + install: | + cargo build --release --locked --package watchtower-plugin + cp target/release/watchtower-client . + cargo clean + main: watchtower-client diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..1333ea8 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,49 @@ +# Use the rust image as the base image for the build stage +FROM rust:latest AS builder + +# Copy the rust-teos source code +COPY . /tmp/rust-teos + +# Install the dependencies required for building rust-teos +RUN apt-get update\ + && apt-get -y --no-install-recommends install libffi-dev libssl-dev musl-tools pkg-config + +RUN cd /tmp/rust-teos \ + && rustup target add x86_64-unknown-linux-musl \ + # Rustfmt is needed to format the grpc stubs generated by tonic + && rustup component add rustfmt \ + # Cross compile with musl as the target, so teosd can run on alpine + && RUSTFLAGS='-C target-feature=+crt-static' cargo build --manifest-path=teos/Cargo.toml --locked --release --target x86_64-unknown-linux-musl + +# Use a new stage with a smaller base image to reduce image size +FROM alpine:latest + +RUN apk update && apk upgrade + +# UID and GID for the teosd user +ENV TEOS_UID=1001 TEOS_GID=1001 + +# Copy the teos binaries from the build stage to the new stage +COPY --from=builder \ + /tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teosd \ + /tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teos-cli /usr/local/bin/ + +# Copy the entrypoint script to the container +COPY docker/entrypoint.sh /entrypoint.sh + +# Set the entrypoint script as executable and add running user +RUN chmod +x /entrypoint.sh \ + && addgroup -g ${TEOS_GID} -S teos \ + && adduser -S -G teos -u ${TEOS_UID} teos + +# Expose the default port used by teosd +EXPOSE 9814/tcp + +# Switch user so that we don't run stuff as root +USER teos + +# Create the teos data directory +RUN mkdir /home/teos/.teos + +# Start teosd when the container starts +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..6c4289a --- /dev/null +++ b/docker/README.md @@ -0,0 +1,113 @@ +## Running `teosd` in a docker container +A `teos` image can be built from the Dockerfile located in `docker`. You can create the image by running: + + cd rust-teos + docker build -f docker/Dockerfile -t teos . + +Then we can create a container by running: + + docker run -it teos + +One way to feed `teos` custom config options is to set environment variables: + + docker run -it -e teos + +Notice that the ENV variables are optional, if unset the corresponding default setting is used. The following ENVs are available: + +``` +- API_BIND= +- API_PORT= +- RPC_BIND= +- RPC_PORT= +- BTC_NETWORK= +- BTC_RPC_CONNECT= +- BTC_RPC_PORT= +- BTC_RPC_USER= +- BTC_RPC_PASSWORD= +# The following options can be set turned on by setting them to "true" +- DEBUG= +- DEPS_DEBUG= +- OVERWRITE_KEY= +- FORCE_UPDATE= +``` + +### Volume persistence + +You may also want to run docker with a volume, so you can have data persistence in `teosd` databases and keys. +If so, run: + + docker volume create teos-data + +And add the the mount parameter to `docker run`: + + -v teos-data:/home/teos/.teos + +If you are running `teosd` and `bitcoind` in the same machine, continue reading for how to create the container based on your OS. + +### `bitcoind` running on the same machine (UNIX) +The easiest way to run both together in the same machine using UNIX is to set the container to use the host network. + +For example, if both `teosd` and `bitcoind` are running on default settings, run: + +``` +docker run \ + --network=host \ + --name teos \ + -v teos-data:/home/teos/.teos \ + -e BTC_RPC_USER= \ + -e BTC_RPC_PASSWORD= \ + -it teos +``` + +Notice that you may still need to set your RPC authentication details, since, hopefully, your credentials won't match the `teosd` defaults. + +### `bitcoind` running on the same machine (OSX or Windows) + +Docker for OSX and Windows does not allow to use the host network (nor to use the `docker0` bridge interface). To work around this +you can use the special `host.docker.internal` domain: + +``` +docker run \ + -p 9814:9814 \ + -p 8814:8814 \ + --name teos \ + -v teos-data:/home/teos/.teos \ + -e BTC_RPC_CONNECT=host.docker.internal \ + -e BTC_RPC_USER= \ + -e BTC_RPC_PASSWORD= \ + -e API_BIND=0.0.0.0 \ + -e RPC_BIND=0.0.0.0 \ + -it teos +``` + +Notice that we also needed to add `API_BIND=0.0.0.0` and `RPC_BIND=0.0.0.0` to bind the API to all interfaces of the container. +Otherwise it will bind to `localhost` and we won't be able to send requests to the tower from the host. + +### Interacting with a TEOS instance + +Once our `teos` instance is running in the container, we can interact with it using `teos-cli`. We have two main ways of doing so: + +1) You can open a shell to the Docker instance by calling: + +`docker exec -it sh` + +Then you can use the `teos-cli` binary from inside the container as you would use it from your host machine. + +2) Using `teos-cli` remotely (assuming you have it installed in the source machine) and pointing to the container. To do so, you will need to copy over the necessary credentials to the host machine. To do so, you can follow the instructions in [the main README](https://github.com/talaia-labs/rust-teos/blob/master/README.md#running-teos-cli-remotely). + +### Plugging in Tor + +You may have noticed, in the above section where the environment variables are covered, that the Tor options are nowhere to be found. That's because these instructions assume that users will likely be setting up Tor in another container. + +On the machine where you have Tor running, you can follow [these instructions](https://community.torproject.org/onion-services/setup/) for setting up a hidden service manually. + +For instance, if you're running `teosd` in a Docker container on the same machine as where Tor is running, you can create a hidden service from the host machine to hide the IP of the `teosd` API (listening on port 9814 for example). If you're using Linux, you can do so by editing your `torrc` file on the host machine with the below option: + +``` +HiddenServiceDir /var/lib/tor/teosd # Path for Linux. This may differ depending on your OS. +HiddenServicePort 9814 127.0.0.1:9814 +``` + +Then restart Tor. + +If all works correctly, the hidden service public key will be located in the `HiddenServiceDir` you set above, in the file called `hostname`. diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..f8975af --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +# Define the start command +START_COMMAND="teosd" + +# Set the API bind address +if [[ ! -z ${API_BIND} ]]; then + START_COMMAND="$START_COMMAND --apibind $API_BIND" +fi + +# Set the API port +if [[ ! -z ${API_PORT} ]]; then + START_COMMAND="$START_COMMAND --apiport $API_PORT" +fi + +# Set the RPC bind address +if [[ ! -z ${RPC_BIND} ]]; then + START_COMMAND="$START_COMMAND --rpcbind $RPC_BIND" +fi + +# Set the RPC port +if [[ ! -z ${RPC_PORT} ]]; then + START_COMMAND="$START_COMMAND --rpcport $RPC_PORT" +fi + +# Set the Bitcoin network +if [[ ! -z ${BTC_NETWORK} ]]; then + START_COMMAND="$START_COMMAND --btcnetwork $BTC_NETWORK" +fi + +# Set the Bitcoin RPC credentials +if [[ ! -z ${BTC_RPC_USER} ]]; then + START_COMMAND="$START_COMMAND --btcrpcuser $BTC_RPC_USER" +fi + +if [[ ! -z ${BTC_RPC_PASSWORD} ]]; then + START_COMMAND="$START_COMMAND --btcrpcpassword $BTC_RPC_PASSWORD" +fi + +# Set the Bitcoin RPC connection details +if [[ ! -z ${BTC_RPC_CONNECT} ]]; then + START_COMMAND="$START_COMMAND --btcrpcconnect $BTC_RPC_CONNECT" +fi + +if [[ ! -z ${BTC_RPC_PORT} ]]; then + START_COMMAND="$START_COMMAND --btcrpcport $BTC_RPC_PORT" +fi + +if [ "${DEBUG}" == "true" ]; then + START_COMMAND="$START_COMMAND --debug" +fi + +if [ "${DEPS_DEBUG}" == "true" ]; then + START_COMMAND="$START_COMMAND --depsdebug" +fi + +if [ "${OVERWRITE_KEY}" == "true" ]; then + START_COMMAND="$START_COMMAND --overwritekey" +fi + +if [ "${FORCE_UPDATE}" == "true" ]; then + START_COMMAND="$START_COMMAND --forceupdate" +fi + +# Start the TEOS daemon +$START_COMMAND diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ca135b2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,116 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1758758545, + "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", + "owner": "ipetkov", + "repo": "crane", + "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1758782550, + "narHash": "sha256-olCvyP5r6+HQTl2EUudtjlA5UammsBpkzAl0l9+utZc=", + "owner": "nix-community", + "repo": "fenix", + "rev": "32f4e350c03cc5762be811e9c700e8696cd13c02", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1758589230, + "narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1758620797, + "narHash": "sha256-Ly4rHgrixFMBnkbMursVt74mxnntnE6yVdF5QellJ+A=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "905641f3520230ad6ef421bcf5da9c6b49f2479b", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a272a74 --- /dev/null +++ b/flake.nix @@ -0,0 +1,114 @@ +{ + description = "Build teos (The Eye of Satoshi) server and plugin"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + + crane.url = "github:ipetkov/crane"; + + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + crane, + fenix, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + inherit (pkgs) lib; + + craneLib = (crane.mkLib pkgs).overrideToolchain fenix.packages.${system}.stable.minimalToolchain; + + env = { + PROTOC = "${pkgs.protobuf}/bin/protoc"; + PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.openssl.out ]; + }; + commonArgs = { + inherit env; + strictDeps = true; + + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.rustfmt # needed for tonic build + pkgs.cacert + pkgs.openssl.dev + ]; + + buildInputs = + [ ] + ++ lib.optionals pkgs.stdenv.isDarwin [ + # Additional darwin specific inputs can be set here + pkgs.libiconv + ]; + }; + + fileSetForCrate = + crate: + lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./Cargo.toml + ./Cargo.lock + ./teos-common + ./teos + ./watchtower-plugin + crate + ]; + }; + + plugin = craneLib.buildPackage ( + commonArgs + // { + pname = "watchtower-plugin"; + cargoExtraArgs = "-p watchtower-plugin"; + src = fileSetForCrate ./watchtower-plugin; + inherit (craneLib.crateNameFromCargoToml { cargoToml = ./watchtower-plugin/Cargo.toml; }) version; + } + ); + teos = craneLib.buildPackage ( + commonArgs + // { + pname = "teos"; + cargoExtraArgs = "-p teos"; + src = fileSetForCrate ./teos; + inherit (craneLib.crateNameFromCargoToml { cargoToml = ./teos/Cargo.toml; }) version; + } + ); + in + { + packages = { + inherit plugin teos; + default = teos; + }; + + apps = { + plugin = flake-utils.lib.mkApp { drv = plugin; }; + teos = flake-utils.lib.mkApp { drv = teos; }; + }; + + formatter = pkgs.nixfmt-tree; + + checks = { + inherit teos plugin; + }; + + devShells.default = craneLib.devShell { + inherit env; + packages = commonArgs.buildInputs ++ commonArgs.nativeBuildInputs; + }; + } + ); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..8a95a07 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +[toolchain] +channel = "1.81.0" +components = [ + "rustfmt", + "clippy", +] diff --git a/teos-common/Cargo.toml b/teos-common/Cargo.toml index be0cb48..14a45d4 100644 --- a/teos-common/Cargo.toml +++ b/teos-common/Cargo.toml @@ -2,26 +2,26 @@ name = "teos-common" version = "0.2.0" authors = ["Sergi Delgado Segura "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] # General hex = { version = "0.4.3", features = [ "serde" ] } -prost = "0.9" +prost = "0.12" rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } serde = "1.0.130" serde_json = "1.0" -tonic = "0.6" +tonic = "0.11" # Crypto rand = "0.8.4" chacha20poly1305 = "0.8.0" # Bitcoin and Lightning -bitcoin = { version = "0.28.0", features = [ "use-serde" ] } -lightning = "0.0.108" +bitcoin = { version = "0.32.0", features = [ "serde" ] } +lightning = "0.1.0" [build-dependencies] -tonic-build = "0.6" \ No newline at end of file +tonic-build = "0.11" diff --git a/teos-common/src/cryptography.rs b/teos-common/src/cryptography.rs index 91e53ae..5dee477 100644 --- a/teos-common/src/cryptography.rs +++ b/teos-common/src/cryptography.rs @@ -1,10 +1,9 @@ //! Cryptography module, used in the interaction between users and towers. -use rand::distributions::Uniform; -use rand::Rng; - use chacha20poly1305::aead::{Aead, NewAead}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use rand::distributions::Uniform; +use rand::Rng; use bitcoin::consensus; use bitcoin::hashes::{sha256, Hash}; @@ -20,7 +19,7 @@ pub enum DecryptingError { } /// Shadows [message_signing::sign]. -pub fn sign(msg: &[u8], sk: &SecretKey) -> Result { +pub fn sign(msg: &[u8], sk: &SecretKey) -> String { message_signing::sign(msg, sk) } @@ -47,8 +46,8 @@ pub fn encrypt( ) -> Result, chacha20poly1305::aead::Error> { // Defaults is [0; 12] let nonce = Nonce::default(); - let _k = sha256::Hash::hash(secret); - let key = Key::from_slice(&_k); + let k = sha256::Hash::hash(secret.as_byte_array()); + let key = Key::from_slice(k.as_byte_array()); let cypher = ChaCha20Poly1305::new(key); cypher.encrypt(&nonce, consensus::serialize(message).as_ref()) @@ -64,8 +63,8 @@ pub fn encrypt( pub fn decrypt(encrypted_blob: &[u8], secret: &Txid) -> Result { // Defaults is [0; 12] let nonce = Nonce::default(); - let _k = sha256::Hash::hash(secret); - let key = Key::from_slice(&_k); + let k = sha256::Hash::hash(secret.as_byte_array()); + let key = Key::from_slice(k.as_byte_array()); let cypher = ChaCha20Poly1305::new(key); @@ -81,17 +80,13 @@ pub fn decrypt(encrypted_blob: &[u8], secret: &Txid) -> Result Vec { let mut rng = rand::thread_rng(); let uniform_u8 = Uniform::new(u8::MIN, u8::MAX); - let v: Vec = (&mut rng).sample_iter(uniform_u8).take(size).collect(); - - v + (&mut rng).sample_iter(uniform_u8).take(size).collect() } /// Gets a key pair generated in a pseudorandom way. pub fn get_random_keypair() -> (SecretKey, PublicKey) { - let raw_sk = get_random_bytes(32); - loop { - if let Ok(sk) = SecretKey::from_slice(&raw_sk) { + if let Ok(sk) = SecretKey::from_slice(&get_random_bytes(32)) { return (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)); } } @@ -99,6 +94,8 @@ pub fn get_random_keypair() -> (SecretKey, PublicKey) { #[cfg(test)] mod tests { + use std::str::FromStr; + use super::*; use bitcoin::consensus; use bitcoin::hashes::hex::FromHex; @@ -112,8 +109,8 @@ mod tests { let expected_enc_blob = Vec::from_hex(ENC_BLOB).unwrap(); let tx_bytes = Vec::from_hex(HEX_TX).unwrap(); - let tx = consensus::deserialize(&tx_bytes).unwrap(); - let txid = Txid::from_hex(HEX_TXID).unwrap(); + let tx: Transaction = consensus::deserialize(&tx_bytes).unwrap(); + let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap(); assert_eq!(encrypt(&tx, &txid).unwrap(), expected_enc_blob); } @@ -122,7 +119,7 @@ mod tests { let expected_tx = consensus::deserialize(&Vec::from_hex(HEX_TX).unwrap()).unwrap(); let encrypted_blob = Vec::from_hex(ENC_BLOB).unwrap(); - let txid = Txid::from_hex(HEX_TXID).unwrap(); + let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap(); assert_eq!(decrypt(&encrypted_blob, &txid).unwrap(), expected_tx); } } diff --git a/teos-common/src/net/http.rs b/teos-common/src/net/http.rs index 86aa3df..f982bd9 100644 --- a/teos-common/src/net/http.rs +++ b/teos-common/src/net/http.rs @@ -3,6 +3,7 @@ pub enum Endpoint { AddAppointment, GetAppointment, GetSubscriptionInfo, + Ping, } impl std::fmt::Display for Endpoint { @@ -15,6 +16,7 @@ impl std::fmt::Display for Endpoint { Endpoint::AddAppointment => "add_appointment", Endpoint::GetAppointment => "get_appointment", Endpoint::GetSubscriptionInfo => "get_subscription_info", + Endpoint::Ping => "ping", } ) } diff --git a/teos-common/src/receipts.rs b/teos-common/src/receipts.rs index 6e806c2..5dc0e00 100644 --- a/teos-common/src/receipts.rs +++ b/teos-common/src/receipts.rs @@ -9,11 +9,11 @@ use crate::{cryptography, UserId}; /// Proof that a user has registered with a tower. This serves two purposes: /// /// - First, the user is able to prove that the tower agreed on providing a service. If a tower refuses to accept appointments -/// from a user (claiming the subscription has expired) but the expiry time has still not passed and the tower cannot -/// provide the relevant appointments signed by the user, it means it is cheating. +/// from a user (claiming the subscription has expired) but the expiry time has still not passed and the tower cannot +/// provide the relevant appointments signed by the user, it means it is cheating. /// - Second, it serves as proof, alongside an appointment receipt, that an appointment was not fulfilled. A registration receipt -/// specifies a subscription period (`subscription_start` - `subscription_expiry`) and the appointment a `start_block` so inclusion -/// can be proved. +/// specifies a subscription period (`subscription_start` - `subscription_expiry`) and the appointment a `start_block` so inclusion +/// can be proved. /// /// TODO: / DISCUSS: In order to minimize the amount of receipts the user has to store, the tower could batch subscription receipts /// as long as the user info is still known. That is, if a user has a subscription with range (S, E) and the user renews the subscription @@ -92,8 +92,7 @@ impl RegistrationReceipt { } pub fn sign(&mut self, sk: &SecretKey) { - // TODO: Check if there's any case where this can actually fail. Don't unwrap if so. - self.signature = Some(cryptography::sign(&self.to_vec(), sk).unwrap()); + self.signature = Some(cryptography::sign(&self.to_vec(), sk)); } pub fn verify(&self, id: &UserId) -> bool { @@ -153,8 +152,7 @@ impl AppointmentReceipt { } pub fn sign(&mut self, sk: &SecretKey) { - // TODO: Check if there's any case where this can actually fail. Don't unwrap if so. - self.signature = Some(cryptography::sign(&self.to_vec(), sk).unwrap()); + self.signature = Some(cryptography::sign(&self.to_vec(), sk)); } pub fn verify(&self, id: &UserId) -> bool { diff --git a/teos-common/src/test_utils.rs b/teos-common/src/test_utils.rs index 9eaed7a..4bd5cb9 100644 --- a/teos-common/src/test_utils.rs +++ b/teos-common/src/test_utils.rs @@ -1,14 +1,14 @@ use std::convert::TryInto; +use bitcoin::script::PushBytesBuf; use hex::FromHex; use rand::distributions::Standard; use rand::prelude::Distribution; use rand::Rng; -use bitcoin::consensus; use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; -use bitcoin::Txid; +use bitcoin::{consensus, Amount, ScriptBuf, Transaction, TxOut, Txid}; use crate::appointment::{Appointment, Locator}; use crate::cryptography; @@ -32,6 +32,12 @@ pub fn get_random_user_id() -> UserId { UserId(pk) } +pub fn get_random_locator() -> Locator { + let mut rng = rand::thread_rng(); + + Locator::from_slice(&rng.gen::<[u8; 16]>()).unwrap() +} + pub fn generate_random_appointment(dispute_txid: Option<&Txid>) -> Appointment { let dispute_txid = match dispute_txid { Some(l) => *l, @@ -42,7 +48,18 @@ pub fn generate_random_appointment(dispute_txid: Option<&Txid>) -> Appointment { }; let tx_bytes = Vec::from_hex(TX_HEX).unwrap(); - let penalty_tx = consensus::deserialize(&tx_bytes).unwrap(); + let mut penalty_tx: Transaction = consensus::deserialize(&tx_bytes).unwrap(); + let size = get_random_int::() % 81; + let mut push_bytes_buf = PushBytesBuf::new(); + PushBytesBuf::extend_from_slice(&mut push_bytes_buf, &cryptography::get_random_bytes(size)) + .unwrap(); + let script_pubkey = ScriptBuf::new_op_return(push_bytes_buf); + + // Append a random-sized OP_RETURN to make each transcation random in size. + penalty_tx.output.push(TxOut { + value: Amount::from_sat(0), + script_pubkey, + }); let mut raw_locator: [u8; 16] = cryptography::get_random_bytes(16).try_into().unwrap(); raw_locator.copy_from_slice(&dispute_txid[..16]); diff --git a/teos/Cargo.toml b/teos/Cargo.toml index b062b46..287b0aa 100644 --- a/teos/Cargo.toml +++ b/teos/Cargo.toml @@ -3,7 +3,7 @@ name = "teos" version = "0.2.0" authors = ["Sergi Delgado Segura "] license = "MIT" -edition = "2018" +edition = "2021" default-run="teosd" [[bin]] @@ -19,32 +19,33 @@ path = "src/main.rs" hex = { version = "0.4.3", features = [ "serde" ] } home = "0.5.3" log = "0.4" -prost = "0.9" -rcgen = { version = "0.8", features = ["pem", "x509-parser"] } +prost = "0.12" +rcgen = { version = "0.13.1", features = ["pem", "x509-parser"] } rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } serde = "1.0.130" serde_json = "1.0" simple_logger = "2.1.0" structopt = "0.3" toml = "0.5" -tonic = { version = "0.6", features = [ "tls", "transport" ] } +tonic = { version = "0.11", features = [ "tls", "transport" ] } tokio = { version = "1.5", features = [ "rt-multi-thread" ] } triggered = "0.1.2" -warp = "0.3.2" +warp = "0.3.5" torut = "0.2.1" +base64 = "0.22.1" # Bitcoin and Lightning -bitcoin = { version = "0.28.0", features = [ "base64" ] } -bitcoincore-rpc = "0.15.0" -lightning = "0.0.108" -lightning-net-tokio = "0.0.108" -lightning-block-sync = { version = "0.0.108", features = [ "rpc-client" ] } +bitcoin = { version = "0.32.0" } +bitcoincore-rpc = "0.19.0" +lightning = "0.1.0" +lightning-net-tokio = "0.1.0" +lightning-block-sync = { version = "0.1.0", features = [ "rpc-client" ] } # Local teos-common = { path = "../teos-common" } [build-dependencies] -tonic-build = "0.6" +tonic-build = "0.11" [dev-dependencies] jsonrpc-http-server = "17.1.0" diff --git a/teos/src/api/http.rs b/teos/src/api/http.rs index 385c5d7..949c08b 100644 --- a/teos/src/api/http.rs +++ b/teos/src/api/http.rs @@ -217,6 +217,14 @@ async fn get_subscription_info( Ok(reply::with_status(body, status)) } +async fn ping(addr: Option) -> Result { + log::debug!( + "Received a ping request from {}", + addr.map_or("an unknown address".to_owned(), |a| a.to_string()) + ); + Ok(reply::reply()) +} + fn router( grpc_conn: PublicTowerServicesClient, ) -> impl Filter + Clone { @@ -251,10 +259,16 @@ fn router( .and(with_grpc(grpc_conn)) .and_then(get_subscription_info); + let ping = warp::get() + .and(warp::path(Endpoint::Ping.to_string())) + .and(warp::addr::remote()) + .and_then(ping); + register .or(add_appointment) .or(get_appointment) .or(get_subscription_info) + .or(ping) .recover(handle_rejection) } @@ -290,12 +304,12 @@ async fn handle_rejection(err: Rejection) -> Result { pub async fn serve( http_bind: SocketAddr, - grpc_bind: String, + grpc_bind: SocketAddr, service_ready: Trigger, shutdown_signal: Listener, ) { let grpc_conn = loop { - match PublicTowerServicesClient::connect(grpc_bind.clone()).await { + match PublicTowerServicesClient::connect(format!("http://{grpc_bind}")).await { Ok(conn) => break conn, Err(_) => { log::error!("Cannot connect to the gRPC server. Retrying shortly"); @@ -615,7 +629,7 @@ mod tests_failures { .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::NOT_FOUND); + assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); } #[tokio::test] @@ -646,8 +660,11 @@ mod tests_methods { }; use super::*; - use crate::extended_appointment::UUID; - use crate::test_utils::{generate_dummy_appointment, ApiConfig, DURATION, SLOTS}; + use crate::responder::{ConfirmationStatus, TransactionTracker}; + use crate::test_utils::{ + generate_dummy_appointment, get_random_tx, ApiConfig, DURATION, SLOTS, + }; + use crate::watcher::Breach; use teos_common::test_utils::get_random_user_id; use teos_common::{cryptography, UserId}; @@ -750,7 +767,7 @@ mod tests_methods { // Then try to add an appointment let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); let response = request_to_api::< common_msgs::AddAppointmentRequest, @@ -776,7 +793,7 @@ mod tests_methods { 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(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); assert_eq!( check_api_error( @@ -816,14 +833,20 @@ mod tests_methods { .await .unwrap(); - // Add the appointment to the Responder so it counts as triggered - let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + // Add the appointment to the Responder as a tracker so it counts as triggered + let dispute_tx = get_random_tx(); + let tracker = TransactionTracker::new( + Breach::new(dispute_tx.clone(), get_random_tx()), + UserId(user_pk), + ConfirmationStatus::ConfirmedIn(100), + ); internal_api .get_watcher() - .add_random_tracker_to_responder(UUID::new(appointment.locator, UserId(user_pk))); + .add_dummy_tracker_to_responder(&tracker); // Try to add it via the http API + let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); assert_eq!( check_api_error( Endpoint::AddAppointment, @@ -852,7 +875,7 @@ mod tests_methods { .await; let (user_sk, _) = cryptography::get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); assert_eq!( check_api_error( @@ -892,7 +915,7 @@ mod tests_methods { // Add an appointment let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); request_to_api::( Endpoint::AddAppointment, @@ -916,8 +939,7 @@ mod tests_methods { signature: cryptography::sign( format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, - ) - .unwrap(), + ), }, server_addr, ) @@ -947,7 +969,6 @@ mod tests_methods { format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, ) - .unwrap() })), server_addr, ) @@ -990,7 +1011,6 @@ mod tests_methods { format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, ) - .unwrap() })), server_addr, ) @@ -1025,7 +1045,6 @@ mod tests_methods { format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, ) - .unwrap() })), server_addr, ) @@ -1063,8 +1082,7 @@ mod tests_methods { >( Endpoint::GetSubscriptionInfo, common_msgs::GetSubscriptionInfoRequest { - signature: cryptography::sign("get subscription info".as_bytes(), &user_sk) - .unwrap(), + signature: cryptography::sign("get subscription info".as_bytes(), &user_sk), }, server_addr, ) @@ -1088,7 +1106,6 @@ mod tests_methods { Endpoint::GetSubscriptionInfo, RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest { signature: cryptography::sign("get subscription info".as_bytes(), &user_sk) - .unwrap(), })), server_addr, ) @@ -1116,7 +1133,6 @@ mod tests_methods { Endpoint::GetSubscriptionInfo, RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest { signature: cryptography::sign("get subscription info".as_bytes(), &user_sk) - .unwrap(), })), server_addr, ) diff --git a/teos/src/api/internal.rs b/teos/src/api/internal.rs index c6fa355..8a1e1ea 100644 --- a/teos/src/api/internal.rs +++ b/teos/src/api/internal.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Condvar, Mutex}; use tonic::{Code, Request, Response, Status}; use triggered::Trigger; +use crate::extended_appointment::UUID; use crate::protos as msgs; use crate::protos::private_tower_services_server::PrivateTowerServices; use crate::protos::public_tower_services_server::PublicTowerServices; @@ -386,10 +387,14 @@ impl PrivateTowerServices for Arc { })?; match self.watcher.get_user_info(user_id) { - Some(info) => Ok(Response::new(msgs::GetUserResponse { + Some((info, locators)) => Ok(Response::new(msgs::GetUserResponse { available_slots: info.available_slots, subscription_expiry: info.subscription_expiry, - appointments: info.appointments.keys().map(|uuid| uuid.to_vec()).collect(), + // TODO: Should make it return locators and make `get_appointments` queryable using the (user_id, locator) pair for consistency. + appointments: locators + .into_iter() + .map(|locator| UUID::new(locator, user_id).to_vec()) + .collect(), })), None => Err(Status::new(Code::NotFound, "User not found")), } @@ -429,11 +434,10 @@ mod tests_private_api { use bitcoin::hashes::Hash; use bitcoin::Txid; - use crate::extended_appointment::UUID; use crate::responder::{ConfirmationStatus, TransactionTracker}; use crate::test_utils::{ - create_api, generate_dummy_appointment, generate_uuid, get_random_tx, DURATION, SLOTS, - START_HEIGHT, + create_api, generate_dummy_appointment, generate_dummy_appointment_with_user, + get_random_tx, DURATION, SLOTS, START_HEIGHT, }; use crate::watcher::Breach; @@ -462,7 +466,7 @@ mod tests_private_api { internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk); internal_api .watcher .add_appointment(appointment.clone(), user_signature) @@ -486,9 +490,7 @@ mod tests_private_api { let (internal_api, _s) = create_api().await; // Add data to the Responser so we can retrieve it later on - internal_api - .watcher - .add_random_tracker_to_responder(generate_uuid()); + internal_api.watcher.add_random_tracker_to_responder(); let response = internal_api .get_all_appointments(Request::new(())) @@ -507,7 +509,7 @@ mod tests_private_api { async fn test_get_appointments() { let (internal_api, _s) = create_api().await; - let locator = Locator::new(get_random_tx().txid()).to_vec(); + let locator = Locator::new(get_random_tx().compute_txid()).to_vec(); let response = internal_api .get_appointments(Request::new(msgs::GetAppointmentsRequest { locator })) .await @@ -523,7 +525,7 @@ mod tests_private_api { for i in 0..3 { // Create a dispute tx to be used for creating different dummy appointments with the same locator. - let dispute_txid = get_random_tx().txid(); + let dispute_txid = get_random_tx().compute_txid(); // The number of different appointments to create for this dispute tx. let appointments_to_create = 4 * i + 7; @@ -533,7 +535,7 @@ mod tests_private_api { let (user_sk, user_pk) = get_random_keypair(); internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(Some(&dispute_txid)).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); internal_api .watcher .add_appointment(appointment, signature) @@ -588,10 +590,10 @@ mod tests_private_api { ); internal_api .watcher - .add_dummy_tracker_to_responder(generate_uuid(), &tracker); + .add_dummy_tracker_to_responder(&tracker); } - let locator = Locator::new(dispute_tx.txid()); + let locator = Locator::new(dispute_tx.compute_txid()); // Query for the current locator and assert it retrieves correct trackers. let response = internal_api @@ -646,7 +648,7 @@ mod tests_private_api { // Add data to the Watcher for _ in 0..2 { let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk); internal_api .watcher .add_appointment(appointment.clone(), user_signature) @@ -655,9 +657,7 @@ mod tests_private_api { // And the Responder for _ in 0..3 { - internal_api - .watcher - .add_random_tracker_to_responder(generate_uuid()); + internal_api.watcher.add_random_tracker_to_responder(); } let response = internal_api @@ -730,12 +730,11 @@ mod tests_private_api { assert!(response.appointments.is_empty()); // Add an appointment and check back - let appointment = generate_dummy_appointment(None).inner; - let uuid = UUID::new(appointment.locator, user_id); - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + let user_signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk); internal_api .watcher - .add_appointment(appointment.clone(), user_signature) + .add_appointment(appointment.inner, user_signature) .unwrap(); let response = internal_api @@ -786,10 +785,12 @@ mod tests_private_api { mod tests_public_api { use super::*; - use crate::extended_appointment::UUID; + use crate::responder::{ConfirmationStatus, TransactionTracker}; use crate::test_utils::{ - create_api, create_api_with_config, generate_dummy_appointment, ApiConfig, DURATION, SLOTS, + create_api, create_api_with_config, generate_dummy_appointment, get_random_tx, ApiConfig, + DURATION, SLOTS, }; + use crate::watcher::Breach; use teos_common::cryptography::{self, get_random_keypair}; #[tokio::test] @@ -900,12 +901,12 @@ mod tests_public_api { internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); let response = internal_api .add_appointment(Request::new(common_msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature: user_signature.clone(), + signature, })) .await .unwrap() @@ -925,12 +926,12 @@ mod tests_public_api { let (user_sk, _) = get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); match internal_api .add_appointment(Request::new(common_msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature: user_signature.clone(), + signature, })) .await { @@ -954,12 +955,12 @@ mod tests_public_api { internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); match internal_api .add_appointment(Request::new(common_msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature: user_signature.clone(), + signature, })) .await { @@ -983,12 +984,12 @@ mod tests_public_api { internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); match internal_api .add_appointment(Request::new(common_msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature: user_signature.clone(), + signature, })) .await { @@ -1008,16 +1009,24 @@ mod tests_public_api { let user_id = UserId(user_pk); internal_api.watcher.register(user_id).unwrap(); - let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + // Add a tracker to the responder to simulate it being triggered. + let dispute_tx = get_random_tx(); + let tracker = TransactionTracker::new( + Breach::new(dispute_tx.clone(), get_random_tx()), + user_id, + ConfirmationStatus::ConfirmedIn(100), + ); internal_api - .watcher - .add_random_tracker_to_responder(UUID::new(appointment.locator, user_id)); + .get_watcher() + .add_dummy_tracker_to_responder(&tracker); + // Try to add it again using the API. + let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); match internal_api .add_appointment(Request::new(common_msgs::AddAppointmentRequest { - appointment: Some(appointment.clone().into()), - signature: user_signature.clone(), + appointment: Some(appointment.into()), + signature, })) .await { @@ -1038,12 +1047,12 @@ mod tests_public_api { let (user_sk, _) = get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); match internal_api .add_appointment(Request::new(common_msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature: user_signature.clone(), + signature, })) .await { @@ -1065,7 +1074,7 @@ mod tests_public_api { // Add the appointment let appointment = generate_dummy_appointment(None).inner; - let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk); internal_api .watcher .add_appointment(appointment.clone(), user_signature) @@ -1076,7 +1085,7 @@ mod tests_public_api { let response = internal_api .get_appointment(Request::new(common_msgs::GetAppointmentRequest { locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await .unwrap() @@ -1104,7 +1113,7 @@ mod tests_public_api { match internal_api .get_appointment(Request::new(common_msgs::GetAppointmentRequest { locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { @@ -1131,7 +1140,7 @@ mod tests_public_api { match internal_api .get_appointment(Request::new(common_msgs::GetAppointmentRequest { locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { @@ -1159,7 +1168,7 @@ mod tests_public_api { match internal_api .get_appointment(Request::new(common_msgs::GetAppointmentRequest { locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { @@ -1182,7 +1191,7 @@ mod tests_public_api { match internal_api .get_appointment(Request::new(common_msgs::GetAppointmentRequest { locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { @@ -1206,7 +1215,7 @@ mod tests_public_api { let message = "get subscription info".to_string(); let response = internal_api .get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest { - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await .unwrap() @@ -1229,7 +1238,7 @@ mod tests_public_api { let message = "get subscription info".to_string(); match internal_api .get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest { - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { @@ -1253,7 +1262,7 @@ mod tests_public_api { let message = "get subscription info".to_string(); match internal_api .get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest { - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { @@ -1274,7 +1283,7 @@ mod tests_public_api { let message = "get subscription info".to_string(); match internal_api .get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest { - signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), + signature: cryptography::sign(message.as_bytes(), &user_sk), })) .await { diff --git a/teos/src/bitcoin_cli.rs b/teos/src/bitcoin_cli.rs index 80b805c..4b73bde 100644 --- a/teos/src/bitcoin_cli.rs +++ b/teos/src/bitcoin_cli.rs @@ -9,19 +9,19 @@ * at your option. */ +use base64::{engine::general_purpose::URL_SAFE as BASE64, Engine}; use std::convert::TryInto; use std::io::{Error, ErrorKind}; use std::sync::Arc; use tokio::sync::Mutex; -use bitcoin::base64; use bitcoin::hash_types::{BlockHash, Txid}; -use bitcoin::hashes::hex::ToHex; -use bitcoin::{Block, Transaction}; +use bitcoin::Transaction; +use bitcoincore_rpc::{Auth, RawTx}; use lightning::util::ser::Writeable; use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; use lightning_block_sync::rpc::RpcClient; -use lightning_block_sync::{AsyncBlockSourceResult, BlockHeaderData, BlockSource}; +use lightning_block_sync::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource}; /// A simple implementation of a bitcoind client (`bitcoin-cli`) with the minimal functionality required by the tower. pub struct BitcoindClient<'a> { @@ -32,9 +32,9 @@ pub struct BitcoindClient<'a> { /// The port to connect to. port: u16, /// The RPC user `bitcoind` is configured with. - rpc_user: &'a str, + rpc_user: String, /// The RPC password for the given user. - rpc_password: &'a str, + rpc_password: String, } impl BlockSource for &BitcoindClient<'_> { @@ -51,7 +51,10 @@ impl BlockSource for &BitcoindClient<'_> { } /// Gets a block given its hash. - fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> { + fn get_block<'a>( + &'a self, + header_hash: &'a BlockHash, + ) -> AsyncBlockSourceResult<'a, BlockData> { Box::pin(async move { let rpc = self.bitcoind_rpc_client.lock().await; rpc.get_block(header_hash).await @@ -74,13 +77,34 @@ impl<'a> BitcoindClient<'a> { pub async fn new( host: &'a str, port: u16, - rpc_user: &'a str, - rpc_password: &'a str, + auth: Auth, 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}")); - let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint)?; + let (rpc_user, rpc_password) = { + let (user, pass) = auth.get_user_pass().map_err(|e| { + Error::new( + ErrorKind::InvalidInput, + format!("Cannot read cookie file. {}", e), + ) + })?; + if user.is_none() { + Err(Error::new( + ErrorKind::InvalidInput, + "Empty btc_rpc_user parsed from rpc_cookie".to_string(), + )) + } else if pass.is_none() { + Err(Error::new( + ErrorKind::InvalidInput, + "Empty btc_rpc_password parsed from rpc_cookie", + )) + } else { + Ok((user.unwrap(), pass.unwrap())) + } + }?; + + let rpc_credentials = BASE64.encode(format!("{}:{}", rpc_user, rpc_password)); + let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint); let client = Self { bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)), @@ -105,9 +129,10 @@ impl<'a> BitcoindClient<'a> { } /// Gets a fresh RPC client. - pub fn get_new_rpc_client(&self) -> std::io::Result { + pub fn get_new_rpc_client(&self) -> RpcClient { let http_endpoint = HttpEndpoint::for_host(self.host.to_owned()).with_port(self.port); - let rpc_credentials = base64::encode(&format!("{}:{}", self.rpc_user, self.rpc_password)); + let rpc_credentials = BASE64.encode(format!("{}:{}", self.rpc_user, self.rpc_password)); + RpcClient::new(&rpc_credentials, http_endpoint) } @@ -124,7 +149,7 @@ impl<'a> BitcoindClient<'a> { pub async fn send_raw_transaction(&self, raw_tx: &Transaction) -> Result { let rpc = self.bitcoind_rpc_client.lock().await; - let raw_tx_json = serde_json::json!(raw_tx.encode().to_hex()); + let raw_tx_json = serde_json::json!(raw_tx.encode().raw_hex()); rpc.call_method::("sendrawtransaction", &[raw_tx_json]) .await } @@ -133,7 +158,7 @@ impl<'a> BitcoindClient<'a> { pub async fn get_raw_transaction(&self, txid: &Txid) -> Result { let rpc = self.bitcoind_rpc_client.lock().await; - let txid_hex = serde_json::json!(txid.encode().to_hex()); + let txid_hex = serde_json::json!(txid.encode().raw_hex()); rpc.call_method::("getrawtransaction", &[txid_hex]) .await } diff --git a/teos/src/carrier.rs b/teos/src/carrier.rs index 15c9d83..75d0fe6 100644 --- a/teos/src/carrier.rs +++ b/teos/src/carrier.rs @@ -80,17 +80,17 @@ impl Carrier { pub(crate) fn send_transaction(&mut self, tx: &Transaction) -> ConfirmationStatus { self.hang_until_bitcoind_reachable(); - if let Some(receipt) = self.issued_receipts.get(&tx.txid()) { - log::info!("Transaction already sent: {}", tx.txid()); + if let Some(receipt) = self.issued_receipts.get(&tx.compute_txid()) { + log::info!("Transaction already sent: {}", tx.compute_txid()); return *receipt; } - log::info!("Pushing transaction to the network: {}", tx.txid()); + log::info!("Pushing transaction to the network: {}", tx.compute_txid()); let receipt = match self.bitcoin_cli.send_raw_transaction(tx) { Ok(_) => { // Here the transaction could, potentially, have been in mempool before the current height. // This shouldn't really matter though. - log::info!("Transaction successfully delivered: {}", tx.txid()); + log::info!("Transaction successfully delivered: {}", tx.compute_txid()); ConfirmationStatus::InMempoolSince(self.block_height) } Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code { @@ -106,7 +106,7 @@ impl Carrier { rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN => { log::info!( "Transaction was confirmed long ago, not keeping track of it: {}", - tx.txid() + tx.compute_txid() ); // Given we are not using txindex, if a transaction bounces we cannot get its confirmation count. However, [send_transaction] is guarded by @@ -117,7 +117,7 @@ impl Carrier { 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, // who receives them from the Watcher, who checks that the tx can be properly deserialized. - log::info!("Transaction cannot be deserialized: {}", tx.txid()); + log::info!("Transaction cannot be deserialized: {}", tx.compute_txid()); ConfirmationStatus::Rejected(rpc_errors::RPC_DESERIALIZATION_ERROR) } _ => { @@ -139,7 +139,7 @@ impl Carrier { } }; - self.issued_receipts.insert(tx.txid(), receipt); + self.issued_receipts.insert(tx.compute_txid(), receipt); receipt } @@ -184,6 +184,7 @@ impl Carrier { #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; use std::thread; use crate::test_utils::{get_random_tx, start_server, BitcoindMock, MockOptions, START_HEIGHT}; @@ -217,7 +218,7 @@ mod tests { // Lets add some dummy data into the cache for i in 0..10 { carrier.issued_receipts.insert( - get_random_tx().txid(), + get_random_tx().compute_txid(), ConfirmationStatus::ConfirmedIn(start_height - i), ); } @@ -243,7 +244,7 @@ mod tests { assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height)); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); } #[test] @@ -261,7 +262,7 @@ mod tests { assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height)); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); } #[test] @@ -284,7 +285,7 @@ mod tests { ); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); } #[test] @@ -306,7 +307,7 @@ mod tests { ); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); } #[test] @@ -326,7 +327,7 @@ mod tests { assert_eq!(r, ConfirmationStatus::IrrevocablyResolved); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); } #[test] @@ -348,7 +349,7 @@ mod tests { ); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); } #[test] @@ -389,7 +390,7 @@ mod tests { start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_hex(TXID_HEX).unwrap(); + let txid = Txid::from_str(TXID_HEX).unwrap(); assert!(carrier.in_mempool(&txid)); } @@ -402,7 +403,7 @@ mod tests { start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_hex(TXID_HEX).unwrap(); + let txid = Txid::from_str(TXID_HEX).unwrap(); assert!(!carrier.in_mempool(&txid)); } @@ -417,7 +418,7 @@ mod tests { start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_hex(TXID_HEX).unwrap(); + let txid = Txid::from_str(TXID_HEX).unwrap(); assert!(!carrier.in_mempool(&txid)); } @@ -431,7 +432,7 @@ mod tests { start_server(bitcoind_mock.server); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_hex(TXID_HEX).unwrap(); + let txid = Txid::from_str(TXID_HEX).unwrap(); assert!(!carrier.in_mempool(&txid)); } @@ -444,7 +445,7 @@ mod tests { let start_height = START_HEIGHT as u32; let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable.clone(), start_height); - let txid = Txid::from_hex(TXID_HEX).unwrap(); + let txid = Txid::from_str(TXID_HEX).unwrap(); let delay = std::time::Duration::new(3, 0); thread::spawn(move || { diff --git a/teos/src/chain_monitor.rs b/teos/src/chain_monitor.rs index 0119267..50a690b 100644 --- a/teos/src/chain_monitor.rs +++ b/teos/src/chain_monitor.rs @@ -26,7 +26,7 @@ where { /// A bitcoin client to poll best tips from. spv_client: SpvClient<'a, P, C, L>, - /// The lat known block header by the [ChainMonitor]. + /// The last known block header by the [ChainMonitor]. last_known_block_header: ValidatedBlockHeader, /// A [DBM] (database manager) instance. Used to persist block data into disk. dbm: Arc>, @@ -135,8 +135,8 @@ mod tests { use std::iter::FromIterator; use std::thread; - use bitcoin::network::constants::Network; use bitcoin::BlockHash; + use bitcoin::Network; use lightning_block_sync::{poll::ChainPoller, SpvClient, UnboundedCache}; use crate::test_utils::{Blockchain, START_HEIGHT}; @@ -158,7 +158,7 @@ mod tests { impl chain::Listen for DummyListener { fn filtered_block_connected( &self, - header: &bitcoin::BlockHeader, + header: &bitcoin::block::Header, _: &chain::transaction::TransactionData, _: u32, ) { @@ -167,7 +167,7 @@ mod tests { .insert(header.block_hash()); } - fn block_disconnected(&self, header: &bitcoin::BlockHeader, _: u32) { + fn block_disconnected(&self, header: &bitcoin::block::Header, _: u32) { self.disconnected_blocks .borrow_mut() .insert(header.block_hash()); @@ -264,10 +264,7 @@ mod tests { // If a new (worse, just one) block gets mined, nothing gets connected nor disconnected cm.poll_best_tip().await; assert_eq!(cm.last_known_block_header, best_tip); - assert!(matches!( - cm.dbm.lock().unwrap().load_last_known_block(), - Err { .. } - )); + assert!(cm.dbm.lock().unwrap().load_last_known_block().is_none()); assert!(listener.connected_blocks.borrow().is_empty()); assert!(listener.disconnected_blocks.borrow().is_empty()); } diff --git a/teos/src/cli.rs b/teos/src/cli.rs index 5c20f00..93ca300 100644 --- a/teos/src/cli.rs +++ b/teos/src/cli.rs @@ -13,6 +13,12 @@ use teos::protos::private_tower_services_client::PrivateTowerServicesClient; use teos_common::appointment::Locator; use teos_common::UserId; +/// Prints the cli error to standard error and exits the process +fn handle_error(error: T) { + eprintln!("{}", error); + std::process::exit(1); +} + #[tokio::main] async fn main() { let opt = Opt::from_args(); @@ -27,7 +33,7 @@ async fn main() { let command = opt.command.clone(); // Load conf (from file or defaults) and patch it with the command line parameters received (if any) - let mut conf = config::from_file::(path.join("teos.toml")); + let mut conf = config::from_file::(&path.join("teos.toml")); conf.patch_with_options(opt); let key = fs::read(&path.join("client-key.pem")) @@ -47,7 +53,7 @@ async fn main() { .ca_certificate(ca_cert) .identity(Identity::from_pem(certificate, key)); - let channel = Channel::from_shared(format!("http://{}:{}", conf.rpc_bind, conf.rpc_port)) + let channel = Channel::from_shared(format!("https://{}:{}", conf.rpc_bind, conf.rpc_port)) .expect("Cannot create channel from endpoint") .tls_config(tls) .unwrap_or_else(|e| { @@ -56,8 +62,8 @@ async fn main() { }) .connect() .await - .unwrap_or_else(|e| { - eprintln!("Could not connect to tower: {e:?}"); + .unwrap_or_else(|_| { + eprintln!("Could not connect to tower. Is teosd running?"); std::process::exit(1); }); @@ -80,10 +86,10 @@ async fn main() { Ok(appointments) => { println!("{}", pretty_json(&appointments.into_inner()).unwrap()) } - Err(status) => println!("{}", status.message()), + Err(status) => handle_error(status.message()), } } - Err(e) => println!("{e}"), + Err(e) => handle_error(e), }; } Command::GetTowerInfo => { @@ -106,10 +112,10 @@ async fn main() { Ok(response) => { println!("{}", pretty_json(&response.into_inner()).unwrap()) } - Err(status) => println!("{}", status.message()), + Err(status) => handle_error(status.message()), } } - Err(e) => println!("{e}"), + Err(e) => handle_error(e), }; } Command::Stop => { diff --git a/teos/src/conf_template.toml b/teos/src/conf_template.toml index 9540cb4..6193b35 100644 --- a/teos/src/conf_template.toml +++ b/teos/src/conf_template.toml @@ -12,8 +12,10 @@ rpc_port = 8814 # bitcoind btc_network = "mainnet" btc_rpc_user = "CSW" +## Notice only user+password **OR** cookie is allowed as rpc auth, any other combination would be rejected btc_rpc_password = "NotSatoshi" btc_rpc_connect = "localhost" +btc_rpc_cookie = "~/.bitcoin/.cookie" btc_rpc_port = 8332 # Flags diff --git a/teos/src/config.rs b/teos/src/config.rs index 1669b25..f1db3de 100644 --- a/teos/src/config.rs +++ b/teos/src/config.rs @@ -1,6 +1,6 @@ //! Logic related to the tower configuration and command line parameter parsing. -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::path::PathBuf; use structopt::StructOpt; @@ -16,15 +16,12 @@ pub fn data_dir_absolute_path(data_dir: String) -> PathBuf { } } -pub fn from_file(path: PathBuf) -> T { +pub fn from_file(path: &PathBuf) -> T { match std::fs::read(path) { - Ok(file_content) => toml::from_slice::(&file_content).map_or_else( - |e| { - eprintln!("Couldn't parse config file: {e}"); - T::default() - }, - |config| config, - ), + Ok(file_content) => toml::from_slice::(&file_content).unwrap_or_else(|e| { + eprintln!("Couldn't parse config file: {e}"); + T::default() + }), Err(_) => T::default(), } } @@ -41,6 +38,14 @@ impl std::fmt::Display for ConfigError { impl std::error::Error for ConfigError {} +#[derive(PartialEq)] +pub enum AuthMethod { + UserPass, + CookieFile, + Multiple, + Invalid, +} + /// Holds all the command line options. #[derive(StructOpt, Debug, Clone)] #[structopt(rename_all = "lowercase")] @@ -66,14 +71,18 @@ pub struct Opt { #[structopt(long)] pub btc_network: Option, - /// bitcoind rpcuser [default: user] + /// bitcoind rpcuser #[structopt(long)] pub btc_rpc_user: Option, - /// bitcoind rpcpassword [default: passwd] + /// bitcoind rpcpassword #[structopt(long)] pub btc_rpc_password: Option, + /// bitcoind rpccookie + #[structopt(long)] + pub btc_rpc_cookie: Option, + /// bitcoind rpcconnect [default: localhost] #[structopt(long)] pub btc_rpc_connect: Option, @@ -102,6 +111,11 @@ pub struct Opt { #[structopt(long)] pub tor_support: bool, + /// Forces the tower to run even if the underlying chain has gone too far out of sync. This can only happen + /// if the node is being run in pruned mode. + #[structopt(long)] + pub force_update: bool, + /// Tor control port [default: 9051] #[structopt(long)] pub tor_control_port: Option, @@ -117,7 +131,7 @@ pub struct Opt { /// - Defaults /// - Configuration file /// - Command line options -#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(default)] pub struct Config { // API @@ -131,6 +145,7 @@ pub struct Config { // Bitcoind pub btc_network: String, pub btc_rpc_user: String, + pub btc_rpc_cookie: String, pub btc_rpc_password: String, pub btc_rpc_connect: String, pub btc_rpc_port: u16, @@ -139,6 +154,7 @@ pub struct Config { pub debug: bool, pub deps_debug: bool, pub overwrite_key: bool, + pub force_update: bool, // General pub subscription_slots: u32, @@ -158,6 +174,24 @@ pub struct Config { } impl Config { + /// The only combinations of valid authentication methods are: + /// - User **AND** password + /// - **OR** Cookie file + // + /// Any other combination will be rejected + pub fn get_auth_method(&self) -> AuthMethod { + match ( + self.btc_rpc_user.is_empty(), + self.btc_rpc_password.is_empty(), + self.btc_rpc_cookie.is_empty(), + ) { + (false, false, true) => AuthMethod::UserPass, + (true, true, false) => AuthMethod::CookieFile, + (true, true, true) => AuthMethod::Invalid, + _ => AuthMethod::Multiple, + } + } + /// Patches the configuration options with the command line options. pub fn patch_with_options(&mut self, options: Opt) { if options.api_bind.is_some() { @@ -181,6 +215,9 @@ impl Config { if options.btc_rpc_password.is_some() { self.btc_rpc_password = options.btc_rpc_password.unwrap(); } + if options.btc_rpc_cookie.is_some() { + self.btc_rpc_cookie = options.btc_rpc_cookie.unwrap(); + } if options.btc_rpc_connect.is_some() { self.btc_rpc_connect = options.btc_rpc_connect.unwrap(); } @@ -198,6 +235,7 @@ impl Config { self.debug |= options.debug; self.deps_debug |= options.deps_debug; self.overwrite_key = options.overwrite_key; + self.force_update = options.force_update; } /// Verifies that [Config] is properly built. @@ -209,11 +247,14 @@ impl Config { /// This will also assign the default `btc_rpc_port` depending on the network if it has not /// been overwritten at this point. pub fn verify(&mut self) -> Result<(), ConfigError> { - if self.btc_rpc_user == String::new() { - return Err(ConfigError("btc_rpc_user must be set".to_owned())); - } - if self.btc_rpc_password == String::new() { - return Err(ConfigError("btc_rpc_password must be set".to_owned())); + let auth_method = self.get_auth_method(); + if auth_method == AuthMethod::Invalid { + return Err(ConfigError("No valid bitcoind auth provided. Set either both btc_rpc_user/btc_rpc_password or btc_rpc_cookie".to_owned())); + } else if auth_method == AuthMethod::Multiple { + return Err(ConfigError( + "Multiple bitcoind auth provided. Pick a single one (either btc_rpc_user/btc_rpc_password or btc_rpc_cookie)" + .to_owned(), + )); } // Normalize the network option to the ones used by bitcoind. @@ -242,6 +283,27 @@ impl Config { pub fn is_default(&self) -> bool { self == &Config::default() } + + /// Logs non-default options. + pub fn log_non_default_options(&self) { + let json_default_config = serde_json::json!(&Config::default()); + let json_config = serde_json::json!(&self); + let sensitive_args = ["btc_rpc_user", "btc_rpc_password"]; + + for (key, value) in json_config.as_object().unwrap().iter() { + if *value != json_default_config[key] { + log::info!( + "Custom config arg: {}: {}", + key, + if sensitive_args.contains(&key.as_str()) { + "****".to_owned() + } else { + value.to_string() + } + ); + } + } + } } impl Default for Config { @@ -263,12 +325,14 @@ impl Default for Config { btc_network: "mainnet".into(), btc_rpc_user: String::new(), btc_rpc_password: String::new(), + btc_rpc_cookie: String::new(), btc_rpc_connect: "localhost".into(), btc_rpc_port: 0, debug: false, deps_debug: false, overwrite_key: false, + force_update: false, subscription_slots: 10000, subscription_duration: 4320, expiry_delta: 6, @@ -297,6 +361,7 @@ mod tests { btc_network: None, btc_rpc_user: None, btc_rpc_password: None, + btc_rpc_cookie: None, btc_rpc_connect: None, btc_rpc_port: None, data_dir: String::from("~/.teos"), @@ -304,6 +369,7 @@ mod tests { debug: false, deps_debug: false, overwrite_key: false, + force_update: false, } } } @@ -323,7 +389,7 @@ mod tests { assert_eq!(config.api_bind, expected_value); // Check the rest of fields are equal. The easiest is to just the field back and compare with a clone - config.api_bind = config_clone.api_bind.clone(); + config.api_bind.clone_from(&config_clone.api_bind); assert_eq!(config, config_clone); } @@ -333,7 +399,7 @@ mod tests { // required to be updated by the user. let mut config = Config::default(); assert!( - matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_rpc_user must be set")) + matches!(config.verify(), Err(ConfigError(e)) if e.contains("No valid bitcoind auth provided")) ); } diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index af27214..7322628 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -1,7 +1,7 @@ //! Logic related to the tower database manager (DBM), component in charge of persisting data on disk. //! -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::iter::FromIterator; use std::path::PathBuf; use std::str::FromStr; @@ -14,16 +14,15 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; use bitcoin::BlockHash; -use teos_common::appointment::{compute_appointment_slots, Appointment, Locator}; -use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; +use teos_common::appointment::{Appointment, Locator}; use teos_common::dbm::{DatabaseConnection, DatabaseManager, Error}; use teos_common::UserId; use crate::extended_appointment::{ExtendedAppointment, UUID}; use crate::gatekeeper::UserInfo; -use crate::responder::{ConfirmationStatus, TransactionTracker}; +use crate::responder::{ConfirmationStatus, PenaltySummary, TransactionTracker}; -const TABLES: [&str; 5] = [ +const TABLES: [&str; 6] = [ "CREATE TABLE IF NOT EXISTS users ( user_id INT PRIMARY KEY, available_slots INT NOT NULL, @@ -59,6 +58,9 @@ const TABLES: [&str; 5] = [ "CREATE TABLE IF NOT EXISTS keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, key INT NOT NULL +)", + "CREATE INDEX IF NOT EXISTS locators_index ON appointments ( + locator )", ]; @@ -139,27 +141,21 @@ impl DBM { } } - /// Loads the associated appointments ([Appointment]) of a given user ([UserInfo]). - pub(crate) fn load_user_appointments(&self, user_id: UserId) -> HashMap { + /// Loads the associated locators ([Locator]) of a given user ([UserId]). + pub(crate) fn load_user_locators(&self, user_id: UserId) -> Vec { let mut stmt = self .connection - .prepare("SELECT UUID, encrypted_blob FROM appointments WHERE user_id=(?)") + .prepare("SELECT locator FROM appointments WHERE user_id=(?)") .unwrap(); - let mut rows = stmt.query([user_id.to_vec()]).unwrap(); - let mut appointments = HashMap::new(); - while let Ok(Some(inner_row)) = rows.next() { - let raw_uuid: Vec = inner_row.get(0).unwrap(); - let uuid = UUID::from_slice(&raw_uuid[0..20]).unwrap(); - let e_blob: Vec = inner_row.get(1).unwrap(); - - appointments.insert( - uuid, - compute_appointment_slots(e_blob.len(), ENCRYPTED_BLOB_MAX_SIZE), - ); - } - - appointments + stmt.query_map([user_id.to_vec()], |row| { + let raw_locator: Vec = row.get(0).unwrap(); + let locator = Locator::from_slice(&raw_locator).unwrap(); + Ok(locator) + }) + .unwrap() + .map(|res| res.unwrap()) + .collect() } /// Loads all users from the database. @@ -178,22 +174,14 @@ impl DBM { let start = row.get(2).unwrap(); let expiry = row.get(3).unwrap(); - users.insert( - user_id, - UserInfo::with_appointments( - slots, - start, - expiry, - self.load_user_appointments(user_id), - ), - ); + users.insert(user_id, UserInfo::new(slots, start, expiry)); } users } /// Removes some users from the database in batch. - pub(crate) fn batch_remove_users(&mut self, users: &HashSet) -> usize { + pub(crate) fn batch_remove_users(&mut self, users: &[UserId]) -> usize { let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; let tx = self.connection.transaction().unwrap(); let iter = users @@ -219,6 +207,24 @@ impl DBM { (users.len() as f64 / limit as f64).ceil() as usize } + /// Get the number of stored appointments. + pub(crate) fn get_appointments_count(&self) -> usize { + let mut stmt = self + .connection + .prepare("SELECT COUNT(*) FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL") + .unwrap(); + stmt.query_row([], |row| row.get(0)).unwrap() + } + + /// Get the number of stored trackers. + pub(crate) fn get_trackers_count(&self) -> usize { + let mut stmt = self + .connection + .prepare("SELECT COUNT(*) FROM trackers") + .unwrap(); + stmt.query_row([], |row| row.get(0)).unwrap() + } + /// Stores an [Appointment] into the database. pub(crate) fn store_appointment( &self, @@ -250,7 +256,11 @@ impl DBM { } /// Updates an existing [Appointment] in the database. - pub(crate) fn update_appointment(&self, uuid: UUID, appointment: &ExtendedAppointment) { + pub(crate) fn update_appointment( + &self, + uuid: UUID, + appointment: &ExtendedAppointment, + ) -> Result<(), Error> { // DISCUSS: Check what fields we'd like to make updatable. e_blob and signature are the obvious, to_self_delay and start_block may not be necessary (or even risky) let query = "UPDATE appointments SET encrypted_blob=(?1), to_self_delay=(?2), user_signature=(?3), start_block=(?4) WHERE UUID=(?5)"; @@ -266,15 +276,17 @@ impl DBM { ) { Ok(_) => { log::debug!("Appointment successfully updated: {uuid}"); + Ok(()) } - Err(_) => { - log::error!("Appointment not found, data cannot be updated: {uuid}"); + Err(e) => { + log::error!("Appointment not found, data cannot be updated: {uuid}. Error: {e:?}"); + Err(e) } } } /// Loads an [Appointment] from the database. - pub(crate) fn load_appointment(&self, uuid: UUID) -> Result { + pub(crate) fn load_appointment(&self, uuid: UUID) -> Option { let key = uuid.to_vec(); let mut stmt = self .connection @@ -302,7 +314,16 @@ impl DBM { start_block, )) }) - .map_err(|_| Error::NotFound) + .ok() + } + + /// Check if an appointment with `uuid` exists. + pub(crate) fn appointment_exists(&self, uuid: UUID) -> bool { + self.connection + .prepare("SELECT UUID FROM appointments WHERE UUID=(?)") + .unwrap() + .exists([uuid.to_vec()]) + .unwrap() } /// Loads appointments from the database. If a locator is given, this method loads only the appointments @@ -352,6 +373,32 @@ impl DBM { appointments } + /// Gets the length of an appointment (the length of `appointment.encrypted_blob`). + pub(crate) fn get_appointment_length(&self, uuid: UUID) -> Option { + let mut stmt = self + .connection + .prepare("SELECT length(encrypted_blob) FROM appointments WHERE UUID=(?)") + .unwrap(); + + stmt.query_row([uuid.to_vec()], |row| row.get(0)).ok() + } + + /// Gets the [`UserId`] of the owner of the appointment along with the appointment + /// length (same as [DBM::get_appointment_length]) for `uuid`. + pub(crate) fn get_appointment_user_and_length(&self, uuid: UUID) -> Option<(UserId, usize)> { + let mut stmt = self + .connection + .prepare("SELECT user_id, length(encrypted_blob) FROM appointments WHERE UUID=(?)") + .unwrap(); + + stmt.query_row([uuid.to_vec()], |row| { + let raw_userid: Vec = row.get(0).unwrap(); + let length = row.get(1).unwrap(); + Ok((UserId::from_slice(&raw_userid).unwrap(), length)) + }) + .ok() + } + /// Removes an [Appointment] from the database. pub(crate) fn remove_appointment(&self, uuid: UUID) { let query = "DELETE FROM appointments WHERE UUID=(?)"; @@ -365,11 +412,12 @@ impl DBM { } } - /// Removes some appointments from the database in batch and updates the associated users giving back - /// the freed appointment slots + /// Removes some appointments from the database in batch and updates the associated users + /// (giving back freed appointment slots) in one transaction so that the deletion and the + /// update is atomic. pub(crate) fn batch_remove_appointments( &mut self, - appointments: &HashSet, + appointments: &[UUID], updated_users: &HashMap, ) -> usize { let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; @@ -405,18 +453,49 @@ impl DBM { (appointments.len() as f64 / limit as f64).ceil() as usize } - /// Loads the locator associated to a given UUID - pub(crate) fn load_locator(&self, uuid: UUID) -> Result { + /// Loads the [`UUID`]s of appointments triggered by `locator`. + pub(crate) fn load_uuids(&self, locator: Locator) -> Vec { let mut stmt = self .connection - .prepare("SELECT locator FROM appointments WHERE UUID=(?)") + .prepare("SELECT UUID from appointments WHERE locator=(?)") .unwrap(); - stmt.query_row([uuid.to_vec()], |row| { - let raw_locator: Vec = row.get(0).unwrap(); - Ok(Locator::from_slice(&raw_locator).unwrap()) + stmt.query_map([locator.to_vec()], |row| { + let raw_uuid: Vec = row.get(0).unwrap(); + let uuid = UUID::from_slice(&raw_uuid).unwrap(); + Ok(uuid) }) - .map_err(|_| Error::NotFound) + .unwrap() + .map(|uuid_res| uuid_res.unwrap()) + .collect() + } + + /// Filters the given set of [`Locator`]s by including only the ones which trigger any of our stored appointments. + pub(crate) fn batch_check_locators_exist(&self, locators: Vec<&Locator>) -> Vec { + let mut registered_locators = Vec::new(); + let locators: Vec> = locators.iter().map(|l| l.to_vec()).collect(); + let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; + + for chunk in locators.chunks(limit) { + let query = "SELECT locator FROM appointments WHERE locator IN ".to_owned(); + let placeholders = format!("(?{})", (", ?").repeat(chunk.len() - 1)); + + let mut stmt = self + .connection + .prepare(&format!("{query}{placeholders}")) + .unwrap(); + let known_locators = stmt + .query_map(params_from_iter(chunk), |row| { + let raw_locator: Vec = row.get(0).unwrap(); + let locator = Locator::from_slice(&raw_locator).unwrap(); + Ok(locator) + }) + .unwrap() + .map(|locator_res| locator_res.unwrap()); + registered_locators.extend(known_locators); + } + + registered_locators } /// Stores a [TransactionTracker] into the database. @@ -450,8 +529,31 @@ impl DBM { } } + /// Updates the tracker status in the database. + /// + /// The only updatable fields are `height` and `confirmed`. + pub(crate) fn update_tracker_status( + &self, + uuid: UUID, + status: &ConfirmationStatus, + ) -> Result<(), Error> { + let (height, confirmed) = status.to_db_data().ok_or(Error::MissingField)?; + + let query = "UPDATE trackers SET height=(?1), confirmed=(?2) WHERE UUID=(?3)"; + match self.update_data(query, params![height, confirmed, uuid.to_vec(),]) { + Ok(x) => { + log::debug!("Tracker successfully updated: {uuid}"); + Ok(x) + } + Err(e) => { + log::error!("Couldn't update tracker: {uuid}. Error: {e:?}"); + Err(e) + } + } + } + /// Loads a [TransactionTracker] from the database. - pub(crate) fn load_tracker(&self, uuid: UUID) -> Result { + pub(crate) fn load_tracker(&self, uuid: UUID) -> Option { let key = uuid.to_vec(); let mut stmt = self .connection.prepare( @@ -478,7 +580,16 @@ impl DBM { user_id, }) }) - .map_err(|_| Error::NotFound) + .ok() + } + + /// Check if a tracker with `uuid` exists. + pub(crate) fn tracker_exists(&self, uuid: UUID) -> bool { + self.connection + .prepare("SELECT UUID FROM trackers WHERE UUID=(?)") + .unwrap() + .exists([uuid.to_vec()]) + .unwrap() } /// Loads trackers from the database. If a locator is given, this method loads only the trackers @@ -530,14 +641,74 @@ impl DBM { trackers } + /// Loads trackers with the given confirmation status. + /// + /// Note that for [`ConfirmationStatus::InMempoolSince(height)`] variant, this pulls trackers + /// with `h <= height` and not just `h = height`. + pub(crate) fn load_trackers_with_confirmation_status( + &self, + status: ConfirmationStatus, + ) -> Result, Error> { + let (height, confirmed) = status.to_db_data().ok_or(Error::MissingField)?; + let sql = format!( + "SELECT UUID FROM trackers WHERE confirmed=(?1) AND height{}(?2)", + if confirmed { "=" } else { "<=" } + ); + let mut stmt = self.connection.prepare(&sql).unwrap(); + + Ok(stmt + .query_map(params![confirmed, height], |row| { + let raw_uuid: Vec = row.get(0).unwrap(); + let uuid = UUID::from_slice(&raw_uuid).unwrap(); + Ok(uuid) + }) + .unwrap() + .map(|uuid_res| uuid_res.unwrap()) + .collect()) + } + + /// Loads the transaction IDs of all the penalties and their status from the database. + pub(crate) fn load_penalties_summaries(&self) -> HashMap { + let mut summaries = HashMap::new(); + + let mut stmt = self + .connection + .prepare( + "SELECT t.UUID, t.penalty_tx, t.height, t.confirmed + FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID", + ) + .unwrap(); + let mut rows = stmt.query([]).unwrap(); + + while let Ok(Some(row)) = rows.next() { + let raw_uuid: Vec = row.get(0).unwrap(); + let raw_penalty_tx: Vec = row.get(1).unwrap(); + let height: u32 = row.get(2).unwrap(); + let confirmed: bool = row.get(3).unwrap(); + + // DISCUSS: Should we store the txids to avoid pulling raw txs and deserializing then hashing them. + let penalty_txid = consensus::deserialize::(&raw_penalty_tx) + .unwrap() + .compute_txid(); + summaries.insert( + UUID::from_slice(&raw_uuid).unwrap(), + PenaltySummary::new( + penalty_txid, + ConfirmationStatus::from_db_data(height, confirmed), + ), + ); + } + summaries + } + /// Stores the last known block into the database. pub(crate) fn store_last_known_block(&self, block_hash: &BlockHash) -> Result<(), Error> { let query = "INSERT OR REPLACE INTO last_known_block (id, block_hash) VALUES (0, ?)"; - self.store_data(query, params![block_hash.to_vec()]) + self.store_data(query, params![block_hash.to_byte_array().to_vec()]) } /// Loads the last known block from the database. - pub fn load_last_known_block(&self) -> Result { + pub fn load_last_known_block(&self) -> Option { let mut stmt = self .connection .prepare("SELECT block_hash FROM last_known_block WHERE id=0") @@ -547,7 +718,7 @@ impl DBM { let raw_hash: Vec = row.get(0).unwrap(); Ok(BlockHash::from_slice(&raw_hash).unwrap()) }) - .map_err(|_| Error::NotFound) + .ok() } /// Stores the tower secret key into the database. @@ -562,7 +733,7 @@ impl DBM { /// /// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed, /// but they are not accessible from the API either. - pub fn load_tower_key(&self) -> Result { + pub fn load_tower_key(&self) -> Option { let mut stmt = self .connection .prepare( @@ -574,18 +745,20 @@ impl DBM { let sk: String = row.get(0).unwrap(); Ok(SecretKey::from_str(&sk).unwrap()) }) - .map_err(|_| Error::NotFound) + .ok() } } #[cfg(test)] mod tests { use super::*; + use std::collections::HashSet; use std::iter::FromIterator; use teos_common::cryptography::{get_random_bytes, get_random_keypair}; - use teos_common::test_utils::get_random_user_id; + use teos_common::test_utils::{get_random_locator, get_random_user_id}; + use crate::rpc_errors; use crate::test_utils::{ generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, get_random_tracker, get_random_tx, AVAILABLE_SLOTS, SUBSCRIPTION_EXPIRY, @@ -602,30 +775,22 @@ mod tests { Ok(dbm) } - pub(crate) fn load_user(&self, user_id: UserId) -> Result { + pub(crate) fn load_user(&self, user_id: UserId) -> Option { let key = user_id.to_vec(); let mut stmt = self .connection .prepare( - "SELECT user_id, available_slots, subscription_start, subscription_expiry + "SELECT available_slots, subscription_start, subscription_expiry FROM users WHERE user_id=(?)", ) .unwrap(); - let user = stmt - .query_row([&key], |row| { - let slots = row.get(1).unwrap(); - let start = row.get(2).unwrap(); - let expiry = row.get(3).unwrap(); - Ok(UserInfo::with_appointments( - slots, - start, - expiry, - self.load_user_appointments(user_id), - )) - }) - .map_err(|_| Error::NotFound)?; - - Ok(user) + stmt.query_row([&key], |row| { + let slots = row.get(0).unwrap(); + let start = row.get(1).unwrap(); + let expiry = row.get(2).unwrap(); + Ok(UserInfo::new(slots, start, expiry)) + }) + .ok() } } @@ -654,33 +819,12 @@ mod tests { )); } - #[test] - fn test_store_load_user_with_appointments() { - let dbm = DBM::in_memory().unwrap(); - - let user_id = get_random_user_id(); - let mut user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - - dbm.store_user(user_id, &user).unwrap(); - - // Add some appointments to the user - for _ in 0..10 { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); - user.appointments.insert(uuid, 1); - } - - // Check both loading the whole user info or only the associated appointments - assert_eq!(dbm.load_user(user_id).unwrap(), user); - assert_eq!(dbm.load_user_appointments(user_id), user.appointments); - } - #[test] fn test_load_nonexistent_user() { let dbm = DBM::in_memory().unwrap(); let user_id = get_random_user_id(); - assert!(matches!(dbm.load_user(user_id), Err(Error::NotFound))); + assert!(dbm.load_user(user_id).is_none()); } #[test] @@ -698,6 +842,30 @@ mod tests { assert_eq!(dbm.load_user(user_id).unwrap(), user); } + #[test] + fn test_load_user_locators() { + let dbm = DBM::in_memory().unwrap(); + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).unwrap(); + + let mut locators = HashSet::new(); + + // Add some appointments to the user + for _ in 0..10 { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + locators.insert(appointment.locator()); + } + + assert_eq!(dbm.load_user(user_id).unwrap(), user); + assert_eq!( + HashSet::from_iter(dbm.load_user_locators(user_id)), + locators + ); + } + #[test] fn test_load_all_users() { let dbm = DBM::in_memory().unwrap(); @@ -710,19 +878,8 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - users.insert(user_id, user.clone()); + users.insert(user_id, user); dbm.store_user(user_id, &user).unwrap(); - - // Add appointments to some of the users - if i % 2 == 0 { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); - users - .get_mut(&user_id) - .unwrap() - .appointments - .insert(uuid, 1); - } } assert_eq!(dbm.load_all_users(), users); @@ -738,7 +895,7 @@ mod tests { dbm.connection .set_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER, limit); - let mut to_be_deleted = HashSet::new(); + let mut to_be_deleted = Vec::new(); let mut rest = HashSet::new(); for i in 1..100 { let user_id = get_random_user_id(); @@ -746,7 +903,7 @@ mod tests { dbm.store_user(user_id, &user).unwrap(); if i % 2 == 0 { - to_be_deleted.insert(user_id); + to_be_deleted.push(user_id); } else { rest.insert(user_id); } @@ -778,12 +935,9 @@ mod tests { Ok { .. } )); - dbm.batch_remove_users(&HashSet::from_iter(vec![appointment.user_id])); - assert!(matches!( - dbm.load_user(appointment.user_id), - Err(Error::NotFound) - )); - assert!(matches!(dbm.load_appointment(uuid), Err(Error::NotFound))); + dbm.batch_remove_users(&[appointment.user_id]); + assert!(dbm.load_user(appointment.user_id).is_none()); + assert!(dbm.load_appointment(uuid).is_none()); // Appointment + Tracker dbm.store_user(appointment.user_id, &info).unwrap(); @@ -793,24 +947,52 @@ mod tests { )); assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); - dbm.batch_remove_users(&HashSet::from_iter(vec![appointment.user_id])); - assert!(matches!( - dbm.load_user(appointment.user_id), - Err(Error::NotFound) - )); - assert!(matches!(dbm.load_appointment(uuid), Err(Error::NotFound))); - assert!(matches!(dbm.load_tracker(uuid), Err(Error::NotFound))); + dbm.batch_remove_users(&[appointment.user_id]); + assert!(dbm.load_user(appointment.user_id).is_none()); + assert!(dbm.load_appointment(uuid).is_none()); + assert!(dbm.load_tracker(uuid).is_none()); } #[test] fn test_batch_remove_nonexistent_users() { let mut dbm = DBM::in_memory().unwrap(); - let users = (0..10).map(|_| get_random_user_id()).collect(); + let users = (0..10).map(|_| get_random_user_id()).collect::>(); // Test it does not fail even if the user does not exist (it will log though) dbm.batch_remove_users(&users); } + #[test] + fn test_get_appointments_trackers_count() { + let dbm = DBM::in_memory().unwrap(); + let n_users = 100; + let n_app_per_user = 4; + let n_trk_per_user = 6; + + for _ in 0..n_users { + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).unwrap(); + + // These are un-triggered appointments. + for _ in 0..n_app_per_user { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + } + + // And these are triggered ones (trackers). + for _ in 0..n_trk_per_user { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)); + dbm.store_tracker(uuid, &tracker).unwrap(); + } + } + + assert_eq!(dbm.get_appointments_count(), n_users * n_app_per_user); + assert_eq!(dbm.get_trackers_count(), n_users * n_trk_per_user); + } + #[test] fn test_store_load_appointment() { let dbm = DBM::in_memory().unwrap(); @@ -846,7 +1028,7 @@ mod tests { dbm.store_appointment(uuid, &appointment), Err(Error::MissingForeignKey) )); - assert!(matches!(dbm.load_tracker(uuid), Err(Error::NotFound))); + assert!((dbm.load_tracker(uuid).is_none())); } #[test] @@ -854,7 +1036,23 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let uuid = generate_uuid(); - assert!(matches!(dbm.load_appointment(uuid), Err(Error::NotFound))); + assert!(dbm.load_appointment(uuid).is_none()); + } + + #[test] + fn test_appointment_exists() { + let dbm = DBM::in_memory().unwrap(); + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + assert!(!dbm.appointment_exists(uuid)); + + dbm.store_user(user_id, &user).unwrap(); + dbm.store_appointment(uuid, &appointment).unwrap(); + + assert!(dbm.appointment_exists(uuid)); } #[test] @@ -880,7 +1078,8 @@ mod tests { another_modified_appointment.user_id = get_random_user_id(); // Check how only the modifiable fields have been updated - dbm.update_appointment(uuid, &another_modified_appointment); + dbm.update_appointment(uuid, &another_modified_appointment) + .unwrap(); assert_eq!(dbm.load_appointment(uuid).unwrap(), modified_appointment); assert_ne!( dbm.load_appointment(uuid).unwrap(), @@ -931,7 +1130,7 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let mut appointments = HashMap::new(); let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.txid(); + let dispute_txid = dispute_tx.compute_txid(); let locator = Locator::new(dispute_txid); for i in 1..11 { @@ -979,6 +1178,44 @@ mod tests { assert_eq!(dbm.load_appointments(Some(locator)), appointments); } + #[test] + fn test_get_appointment_length() { + let dbm = DBM::in_memory().unwrap(); + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + dbm.store_user(user_id, &user).unwrap(); + dbm.store_appointment(uuid, &appointment).unwrap(); + + assert_eq!( + dbm.get_appointment_length(uuid).unwrap(), + appointment.inner.encrypted_blob.len() + ); + assert!(dbm.get_appointment_length(generate_uuid()).is_none()); + } + + #[test] + fn test_get_appointment_user_and_length() { + let dbm = DBM::in_memory().unwrap(); + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + dbm.store_user(user_id, &user).unwrap(); + dbm.store_appointment(uuid, &appointment).unwrap(); + + assert_eq!( + dbm.get_appointment_user_and_length(uuid).unwrap(), + (user_id, appointment.encrypted_blob().len()) + ); + assert!(dbm + .get_appointment_user_and_length(generate_uuid()) + .is_none()); + } + #[test] fn test_batch_remove_appointments() { let mut dbm = DBM::in_memory().unwrap(); @@ -999,13 +1236,13 @@ mod tests { let mut rest = HashSet::new(); for i in 1..6 { - let mut to_be_deleted = HashSet::new(); + let mut to_be_deleted = Vec::new(); for j in 0..limit * 2 * i { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); dbm.store_appointment(uuid, &appointment).unwrap(); if j % 2 == 0 { - to_be_deleted.insert(uuid); + to_be_deleted.push(uuid); } else { rest.insert(uuid); } @@ -1014,7 +1251,7 @@ mod tests { // When the appointment are deleted, the user will get back slots based on the deleted data. // Here we can just make a number up to make sure it matches. user.available_slots = i as u32; - let updated_users = HashMap::from_iter([(user_id, user.clone())]); + let updated_users = HashMap::from_iter([(user_id, user)]); // Check that the db transaction had i queries on it assert_eq!( @@ -1049,11 +1286,8 @@ mod tests { Ok { .. } )); - dbm.batch_remove_appointments( - &HashSet::from_iter(vec![uuid]), - &HashMap::from_iter([(appointment.user_id, info.clone())]), - ); - assert!(matches!(dbm.load_appointment(uuid), Err(Error::NotFound))); + dbm.batch_remove_appointments(&[uuid], &HashMap::from_iter([(appointment.user_id, info)])); + assert!(dbm.load_appointment(uuid).is_none()); // Appointment + Tracker assert!(matches!( @@ -1062,48 +1296,96 @@ mod tests { )); assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); - dbm.batch_remove_appointments( - &HashSet::from_iter(vec![uuid]), - &HashMap::from_iter([(appointment.user_id, info)]), - ); - assert!(matches!(dbm.load_appointment(uuid), Err(Error::NotFound))); - assert!(matches!(dbm.load_tracker(uuid), Err(Error::NotFound))); + dbm.batch_remove_appointments(&[uuid], &HashMap::from_iter([(appointment.user_id, info)])); + assert!(dbm.load_appointment(uuid).is_none()); + assert!(dbm.load_tracker(uuid).is_none()); } #[test] fn test_batch_remove_nonexistent_appointments() { let mut dbm = DBM::in_memory().unwrap(); - let appointments = (0..10).map(|_| generate_uuid()).collect(); + let appointments = (0..10).map(|_| generate_uuid()).collect::>(); // Test it does not fail even if the user does not exist (it will log though) dbm.batch_remove_appointments(&appointments, &HashMap::new()); } + #[test] - fn test_load_locator() { + fn test_load_uuids() { let dbm = DBM::in_memory().unwrap(); - // In order to add an appointment we need the associated user to be present - let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + let dispute_tx = get_random_tx(); + let dispute_txid = dispute_tx.compute_txid(); + let mut uuids = HashSet::new(); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + // Add ten appointments triggered by the same locator. + for _ in 0..10 { + let user_id = get_random_user_id(); + dbm.store_user(user_id, &user).unwrap(); - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } - )); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + dbm.store_appointment(uuid, &appointment).unwrap(); - // We should be able to load the locator now the appointment exists - assert_eq!(dbm.load_locator(uuid).unwrap(), appointment.locator()); + uuids.insert(uuid); + } + + // Add ten more appointments triggered by different locators. + for _ in 0..10 { + let user_id = get_random_user_id(); + dbm.store_user(user_id, &user).unwrap(); + + let dispute_txid = get_random_tx().compute_txid(); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + dbm.store_appointment(uuid, &appointment).unwrap(); + } + + assert_eq!( + HashSet::from_iter(dbm.load_uuids(Locator::new(dispute_txid))), + uuids + ); } #[test] - fn test_load_nonexistent_locator() { + fn test_batch_check_locators_exist() { let dbm = DBM::in_memory().unwrap(); + // Generate `n_app` appointments which we will store in the DB. + let n_app = 100; + let appointments: Vec<_> = (0..n_app) + .map(|_| generate_dummy_appointment(None)) + .collect(); - let (uuid, _) = generate_dummy_appointment_with_user(get_random_user_id(), None); - assert!(matches!(dbm.load_locator(uuid), Err(Error::NotFound))); + // Register all the users beforehand. + for user_id in appointments.iter().map(|a| a.user_id) { + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).unwrap(); + } + + // Store all the `n_app` appointments. + for appointment in appointments.iter() { + dbm.store_appointment(appointment.uuid(), appointment) + .unwrap(); + } + + // Select `n_app / 5` locators as if they appeared in a new block. + let known_locators: HashSet<_> = appointments + .iter() + .take(n_app / 5) + .map(|a| a.locator()) + .collect(); + // And extra `n_app / 5` unknown locators. + let unknown_locators: HashSet<_> = (0..n_app / 5).map(|_| get_random_locator()).collect(); + let all_locators = known_locators + .iter() + .chain(unknown_locators.iter()) + .collect(); + + assert_eq!( + HashSet::from_iter(dbm.batch_check_locators_exist(all_locators)), + known_locators + ); } #[test] @@ -1163,12 +1445,44 @@ mod tests { )); } + #[test] + fn test_update_tracker_status() { + let dbm = DBM::in_memory().unwrap(); + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); + dbm.store_tracker(uuid, &tracker).unwrap(); + + // Update the status and check if it's actually updated. + dbm.update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(100)) + .unwrap(); + assert_eq!( + dbm.load_tracker(uuid).unwrap().status, + ConfirmationStatus::ConfirmedIn(100) + ); + + // Rejected status doesn't have a persistent DB representation. + assert!(matches!( + dbm.update_tracker_status( + uuid, + &ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_REJECTED) + ), + Err(Error::MissingField) + )); + } + #[test] fn test_load_nonexistent_tracker() { let dbm = DBM::in_memory().unwrap(); let uuid = generate_uuid(); - assert!(matches!(dbm.load_tracker(uuid), Err(Error::NotFound))); + assert!(dbm.load_tracker(uuid).is_none()); } #[test] @@ -1202,7 +1516,7 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let mut trackers = HashMap::new(); let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.txid(); + let dispute_txid = dispute_tx.compute_txid(); let locator = Locator::new(dispute_txid); let status = ConfirmationStatus::InMempoolSince(42); @@ -1233,6 +1547,168 @@ mod tests { assert_eq!(dbm.load_trackers(Some(locator)), trackers); } + #[test] + fn test_load_trackers_with_confirmation_status_in_mempool() { + let dbm = DBM::in_memory().unwrap(); + let n_trackers = 100; + let mut tracker_statuses = HashMap::new(); + + // Store a bunch of trackers. + for i in 0..n_trackers { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + + // Some trackers confirmed and some aren't. + let status = if i % 2 == 0 { + ConfirmationStatus::InMempoolSince(i) + } else { + ConfirmationStatus::ConfirmedIn(i) + }; + + let tracker = get_random_tracker(user_id, status); + dbm.store_tracker(uuid, &tracker).unwrap(); + tracker_statuses.insert(uuid, status); + } + + for i in 0..n_trackers + 10 { + let in_mempool_since_i: HashSet = tracker_statuses + .iter() + .filter_map(|(&uuid, &status)| { + if let ConfirmationStatus::InMempoolSince(x) = status { + // If a tracker was in mempool since x, then it's also in mempool since x + 1, x + 2, etc... + return (x <= i).then_some(uuid); + } + None + }) + .collect(); + assert_eq!( + HashSet::from_iter( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::InMempoolSince( + i + )) + .unwrap() + ), + in_mempool_since_i, + ); + } + } + + #[test] + fn test_load_trackers_with_confirmation_status_confirmed() { + let dbm = DBM::in_memory().unwrap(); + let n_blocks = 100; + let n_trackers = 30; + let mut tracker_statuses = HashMap::new(); + + // Loop over a bunch of blocks. + for i in 0..n_blocks { + // Store a bunch of trackers in each block. + for j in 0..n_trackers { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + + // Some trackers confirmed and some aren't. + let status = if j % 2 == 0 { + ConfirmationStatus::InMempoolSince(i) + } else { + ConfirmationStatus::ConfirmedIn(i) + }; + + let tracker = get_random_tracker(user_id, status); + dbm.store_tracker(uuid, &tracker).unwrap(); + tracker_statuses.insert(uuid, status); + } + } + + for i in 0..n_blocks + 10 { + let confirmed_in_i: HashSet = tracker_statuses + .iter() + .filter_map(|(&uuid, &status)| { + if let ConfirmationStatus::ConfirmedIn(x) = status { + return (x == i).then_some(uuid); + } + None + }) + .collect(); + assert_eq!( + HashSet::from_iter( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(i)) + .unwrap() + ), + confirmed_in_i, + ); + } + } + + #[test] + fn test_load_trackers_with_confirmation_status_bad_status() { + let dbm = DBM::in_memory().unwrap(); + + assert!(matches!( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::Rejected( + rpc_errors::RPC_VERIFY_REJECTED + )), + Err(Error::MissingField) + )); + + assert!(matches!( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::IrrevocablyResolved), + Err(Error::MissingField) + )); + } + + #[test] + fn test_load_penalties_summaries() { + let dbm = DBM::in_memory().unwrap(); + let n_trackers = 100; + let mut penalties_summaries = HashMap::new(); + + for i in 0..n_trackers { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).unwrap(); + + let status = if i % 2 == 0 { + ConfirmationStatus::InMempoolSince(i) + } else { + ConfirmationStatus::ConfirmedIn(i) + }; + + let tracker = get_random_tracker(user_id, status); + dbm.store_tracker(uuid, &tracker).unwrap(); + + penalties_summaries.insert( + uuid, + PenaltySummary::new(tracker.penalty_tx.compute_txid(), status), + ); + } + + assert_eq!(dbm.load_penalties_summaries(), penalties_summaries); + } + #[test] fn test_store_load_last_known_block() { let dbm = DBM::in_memory().unwrap(); @@ -1251,14 +1727,14 @@ mod tests { fn test_store_load_nonexistent_last_known_block() { let dbm = DBM::in_memory().unwrap(); - assert!(matches!(dbm.load_last_known_block(), Err(Error::NotFound))); + assert!(dbm.load_last_known_block().is_none()); } #[test] fn test_store_load_tower_key() { let dbm = DBM::in_memory().unwrap(); - assert!(matches!(dbm.load_tower_key(), Err(Error::NotFound))); + assert!(dbm.load_tower_key().is_none()); for _ in 0..7 { let sk = get_random_keypair().0; dbm.store_tower_key(&sk).unwrap(); diff --git a/teos/src/extended_appointment.rs b/teos/src/extended_appointment.rs index 88920de..6545a7f 100644 --- a/teos/src/extended_appointment.rs +++ b/teos/src/extended_appointment.rs @@ -23,7 +23,7 @@ impl UUID { pub fn new(locator: Locator, user_id: UserId) -> Self { let mut uuid_data = locator.to_vec(); uuid_data.extend(user_id.0.serialize()); - UUID(ripemd160::Hash::hash(&uuid_data).into_inner()) + UUID(ripemd160::Hash::hash(&uuid_data).to_byte_array()) } /// Serializes the [UUID] returning its byte representation. @@ -46,8 +46,6 @@ impl std::fmt::Display for UUID { /// An extended version of the appointment hold by the tower. /// /// The [Appointment] is extended in terms of data, that is, it provides further information only relevant to the tower. -/// Notice [ExtendedAppointment]s are not kept in memory but persisted on disk. The [Watcher](crate::watcher::Watcher) -/// keeps [AppointmentSummary] instead. #[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct ExtendedAppointment { /// The underlying appointment extended by [ExtendedAppointment]. @@ -60,18 +58,6 @@ pub(crate) struct ExtendedAppointment { pub start_block: u32, } -/// A summary of an appointment. -/// -/// Contains the minimal amount of data the [Watcher](crate::watcher::Watcher) needs to keep in memory in order to -/// watch for breaches. -#[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) struct AppointmentSummary { - /// The [Appointment] locator. - pub locator: Locator, - /// The user this [Appointment] belongs to. - pub user_id: UserId, -} - impl ExtendedAppointment { /// Create a new [ExtendedAppointment]. pub fn new( @@ -103,12 +89,8 @@ impl ExtendedAppointment { self.inner.to_self_delay } - /// Computes the summary of the [ExtendedAppointment]. - pub fn get_summary(&self) -> AppointmentSummary { - AppointmentSummary { - locator: self.locator(), - user_id: self.user_id, - } + pub fn uuid(&self) -> UUID { + UUID::new(self.inner.locator, self.user_id) } } @@ -116,22 +98,14 @@ impl ExtendedAppointment { mod tests { use super::*; - use teos_common::appointment::Appointment; - use teos_common::cryptography::get_random_bytes; - use teos_common::test_utils::get_random_user_id; + use crate::test_utils::generate_uuid; #[test] - fn test_get_summary() { - let locator = Locator::from_slice(&get_random_bytes(16)).unwrap(); - let user_id = get_random_user_id(); - let signature = String::new(); - - let a = Appointment::new(locator, get_random_bytes(32), 42); - let e = ExtendedAppointment::new(a, user_id, signature, 21); - - let s = e.get_summary(); - - assert_eq!(e.locator(), s.locator); - assert_eq!(e.user_id, s.user_id); + fn test_uuid_ser_deser() { + let original_uuid = generate_uuid(); + assert_eq!( + UUID::from_slice(&original_uuid.to_vec()).unwrap(), + original_uuid + ); } } diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index 7ae8a23..99e7452 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -1,13 +1,11 @@ //! Logic related to the Gatekeeper, the component in charge of managing access to the tower resources. -use std::collections::{HashMap, HashSet}; -use std::iter::FromIterator; +use lightning::chain; +use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -use lightning::chain; - -use teos_common::appointment::compute_appointment_slots; +use teos_common::appointment::{compute_appointment_slots, Locator}; use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; use teos_common::cryptography; use teos_common::receipts::RegistrationReceipt; @@ -17,7 +15,7 @@ use crate::dbm::DBM; use crate::extended_appointment::{ExtendedAppointment, UUID}; /// Data regarding a user subscription with the tower. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct UserInfo { /// Number of appointment slots available for a given user. pub(crate) available_slots: u32, @@ -25,8 +23,6 @@ pub(crate) struct UserInfo { pub(crate) subscription_start: u32, /// Block height where the user subscription expires. pub(crate) subscription_expiry: u32, - /// Map of appointment ids and the how many slots they take from the subscription. - pub(crate) appointments: HashMap, } impl UserInfo { @@ -36,22 +32,6 @@ impl UserInfo { available_slots, subscription_start, subscription_expiry, - appointments: HashMap::new(), - } - } - - /// Creates a new [UserInfo] instance with some associated appointments. - pub fn with_appointments( - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - appointments: HashMap, - ) -> Self { - UserInfo { - available_slots, - subscription_start, - subscription_expiry, - appointments, } } } @@ -134,8 +114,9 @@ impl Gatekeeper { } /// Gets the data held by the tower about a given user. - pub(crate) fn get_user_info(&self, user_id: UserId) -> Option { - self.registered_users.lock().unwrap().get(&user_id).cloned() + pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec)> { + let info = self.registered_users.lock().unwrap().get(&user_id).cloned(); + info.map(|info| (info, self.dbm.lock().unwrap().load_user_locators(user_id))) } /// Authenticates a user. @@ -219,7 +200,13 @@ impl Gatekeeper { // For updates, the difference between the existing appointment size and the update is computed. let mut registered_users = self.registered_users.lock().unwrap(); let user_info = registered_users.get_mut(&user_id).unwrap(); - let used_slots = user_info.appointments.get(&uuid).map_or(0, |x| *x); + let used_blob_size = self + .dbm + .lock() + .unwrap() + .get_appointment_length(uuid) + .unwrap_or(0); + let used_slots = compute_appointment_slots(used_blob_size, ENCRYPTED_BLOB_MAX_SIZE); let required_slots = compute_appointment_slots(appointment.encrypted_blob().len(), ENCRYPTED_BLOB_MAX_SIZE); @@ -228,7 +215,6 @@ impl Gatekeeper { if diff <= user_info.available_slots as i64 { // Filling / freeing slots depending on whether this is an update or not, and if it is bigger or smaller // than the old appointment - user_info.appointments.insert(uuid, required_slots); user_info.available_slots = (user_info.available_slots as i64 - diff) as u32; self.dbm.lock().unwrap().update_user(user_id, user_info); @@ -258,56 +244,50 @@ impl Gatekeeper { /// Gets a map of outdated users. Outdated users are those whose subscription has expired and the renewal grace period /// has already passed ([expiry_delta](Self::expiry_delta)). - pub(crate) fn get_outdated_users(&self, block_height: u32) -> HashMap> { - let registered_users = self.registered_users.lock().unwrap().clone(); - registered_users - .into_iter() - .filter(|(_, info)| block_height == info.subscription_expiry + self.expiry_delta) - .map(|(id, info)| (id, info.appointments.keys().cloned().collect())) + pub(crate) fn get_outdated_users(&self, block_height: u32) -> Vec { + self.registered_users + .lock() + .unwrap() + .iter() + // NOTE: Ideally there won't be a user with `block_height > subscription_expiry + expiry_delta`, but + // this might happen if we skip a couple of block connections due to a force update. + .filter(|(_, info)| block_height >= info.subscription_expiry + self.expiry_delta) + .map(|(user_id, _)| *user_id) .collect() } - /// Gets a set of outdated user ids. - pub(crate) fn get_outdated_user_ids(&self, block_height: u32) -> HashSet { - self.get_outdated_users(block_height) - .keys() - .cloned() - .collect() - } - - /// Get a map of outdated appointments (from any user). - pub(crate) fn get_outdated_appointments(&self, block_height: u32) -> HashSet { - HashSet::from_iter( - self.get_outdated_users(block_height) - .into_values() - .flatten(), - ) - } - - /// Deletes a collection of appointments from the users' subscriptions (from memory only) - /// and updates the available_slots count for the given user. + /// Deletes these appointments from the database and updates the user's information. /// - /// Notice appointments are only de-linked from users, but not actually removed. This is because the [Gatekeeper] - /// does not actually hold any [ExtendedAppointment](crate::extended_appointment::ExtendedAppointment) data, - /// just references to them. - pub(crate) fn delete_appointments_from_memory( - &self, - appointments: &HashMap, - ) -> HashMap { - let mut updated_users = HashMap::new(); - let mut registered_users = self.registered_users.lock().unwrap(); + /// If `refund` is set, the appointments owners will get their slots refunded back. + /// + /// DISCUSS: When `refund` is `false` we don't give back the slots to the user for the deleted appointments. + /// This is to discourage misbehavior (sending bad appointments, either non-decryptable or rejected by the network). + pub(crate) fn delete_appointments(&self, appointments: Vec, refund: bool) { + let mut dbm = self.dbm.lock().unwrap(); - for (uuid, user_id) in appointments { - // Remove the appointment from the appointment list and update the available slots - if let Some(user_info) = registered_users.get_mut(user_id) { - if let Some(x) = user_info.appointments.remove(uuid) { - user_info.available_slots += x; - } - updated_users.insert(*user_id, user_info.clone()); - }; + let updated_users = if refund { + let mut updated_users = HashMap::new(); + let mut registered_users = self.registered_users.lock().unwrap(); + // Give back the consumed slots to each user. + for uuid in appointments.iter() { + let (user_id, blob_size) = dbm.get_appointment_user_and_length(*uuid).unwrap(); + registered_users.get_mut(&user_id).unwrap().available_slots += + compute_appointment_slots(blob_size, ENCRYPTED_BLOB_MAX_SIZE); + updated_users.insert(user_id, registered_users[&user_id]); + } + updated_users + } else { + // No updated users. + HashMap::new() + }; + + // An optimization for the case when only one appointment is being deleted without refunding. + // This avoids creating a DB transaction for a single query. + if appointments.len() == 1 && updated_users.is_empty() { + dbm.remove_appointment(appointments[0]) + } else { + dbm.batch_remove_appointments(&appointments, &updated_users); } - - updated_users } } @@ -317,19 +297,24 @@ impl chain::Listen for Gatekeeper { /// This is mainly used to keep track of time and expire / outdate subscriptions when needed. fn filtered_block_connected( &self, - header: &bitcoin::BlockHeader, + header: &bitcoin::block::Header, _: &chain::transaction::TransactionData, height: u32, ) { log::info!("New block received: {}", header.block_hash()); // Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired. - let outdated_users = self.get_outdated_user_ids(height); + let outdated_users = self.get_outdated_users(height); if !outdated_users.is_empty() { - self.registered_users - .lock() - .unwrap() - .retain(|id, _| !outdated_users.contains(id)); + // Remove the outdated users from memory first. + { + let mut registered_users = self.registered_users.lock().unwrap(); + // Removing each outdated user in a loop is more efficient than retaining non-outdated users + // because retaining would loop over all the available users which is always more than the outdated ones. + for outdated_user in outdated_users.iter() { + registered_users.remove(outdated_user); + } + } self.dbm.lock().unwrap().batch_remove_users(&outdated_users); } @@ -339,7 +324,7 @@ impl chain::Listen for Gatekeeper { } /// Handles reorgs in the [Gatekeeper]. Simply updates the last_known_block_height. - fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) { + fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); // There's nothing to be done here but updating the last known block self.last_known_block_height @@ -351,14 +336,13 @@ impl chain::Listen for Gatekeeper { mod tests { use super::*; - use crate::test_utils::{ - generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, Blockchain, - }; + use crate::test_utils::{generate_dummy_appointment_with_user, get_random_tracker, Blockchain}; use lightning::chain::Listen; use teos_common::cryptography::{get_random_bytes, get_random_keypair}; - use teos_common::dbm::Error as DBError; use teos_common::test_utils::get_random_user_id; + use crate::responder::ConfirmationStatus; + const SLOTS: u32 = 21; const DURATION: u32 = 500; const EXPIRY_DELTA: u32 = 42; @@ -381,21 +365,11 @@ mod tests { &self.registered_users } - pub(crate) fn add_outdated_user( - &self, - user_id: UserId, - outdates_at: u32, - appointments: Option>, - ) { + pub(crate) fn add_outdated_user(&self, user_id: UserId, outdates_at: u32) { self.add_update_user(user_id).unwrap(); let mut registered_users = self.registered_users.lock().unwrap(); - let mut user = registered_users.get_mut(&user_id).unwrap(); + let user = registered_users.get_mut(&user_id).unwrap(); user.subscription_expiry = outdates_at - self.expiry_delta; - if let Some(uuids) = appointments { - for uuid in uuids.iter() { - user.appointments.insert(*uuid, 1); - } - } } } @@ -461,7 +435,7 @@ mod tests { // Let's now provide data generated by an actual user, still the user is unknown let (user_sk, user_pk) = get_random_keypair(); - let signature = cryptography::sign(message, &user_sk).unwrap(); + let signature = cryptography::sign(message, &user_sk); assert_eq!( gatekeeper.authenticate_user(message, &signature), Err(AuthenticationFailure("User not found.")) @@ -568,25 +542,32 @@ mod tests { let available_slots = gatekeeper .add_update_appointment(user_id, uuid, &appointment) .unwrap(); + // Simulate the watcher adding the appointment in the database. + gatekeeper + .dbm + .lock() + .unwrap() + .store_appointment(uuid, &appointment) + .unwrap(); - assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid)); + let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + assert!(user_locators.contains(&appointment.locator())); assert_eq!(slots_before, available_slots + 1); - // Slots should have been updated in the database too. Notice the appointment won't be there yet - // given the Watcher is responsible for adding it, and it will do so after calling this method + // Slots should have been updated in the database too. let mut loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); assert_eq!(loaded_user.available_slots, available_slots); - // Adding the exact same appointment should leave the slots count unchanged + // Adding the exact same appointment should leave the slots count unchanged. + // We don't really need to update the appointment in the DB since it's the very same appointment. let mut updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) .unwrap(); - assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid)); + + let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots); + loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); @@ -596,10 +577,18 @@ mod tests { updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &bigger_appointment) .unwrap(); - assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid)); + // Simulate the watcher updating the appointment in the database. + gatekeeper + .dbm + .lock() + .unwrap() + .update_appointment(uuid, &bigger_appointment) + .unwrap(); + + let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots - 1); + loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); @@ -607,26 +596,43 @@ mod tests { updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) .unwrap(); - assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid)); + // Simulate the watcher updating the appointment in the database. + gatekeeper + .dbm + .lock() + .unwrap() + .update_appointment(uuid, &appointment) + .unwrap(); + + let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots); + loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); // Adding an appointment with a different uuid should not count as an update - let new_uuid = generate_uuid(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); updated_slot_count = gatekeeper - .add_update_appointment(user_id, new_uuid, &appointment) + .add_update_appointment(user_id, uuid, &appointment) .unwrap(); - assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] - .appointments - .contains_key(&new_uuid)); + // Simulate the watcher adding the appointment in the database. + gatekeeper + .dbm + .lock() + .unwrap() + .store_appointment(uuid, &appointment) + .unwrap(); + + let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots - 1); + loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); // Finally, trying to add an appointment when the user has no enough slots should fail + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); gatekeeper .registered_users .lock() @@ -635,9 +641,10 @@ mod tests { .unwrap() .available_slots = 0; assert!(matches!( - gatekeeper.add_update_appointment(user_id, generate_uuid(), &appointment), + gatekeeper.add_update_appointment(user_id, uuid, &appointment), Err(NotEnoughSlots) )); + // The entry in the database should remain unchanged in this case loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); @@ -683,144 +690,175 @@ mod tests { // Initially, there are not outdated users, so querying any block height should return an empty map for i in 0..start_height { - assert_eq!(gatekeeper.get_outdated_users(i).len(), 0); + assert_eq!(gatekeeper.get_outdated_users(i), vec![]); } // Adding a user whose subscription is outdated should return an entry let user_id = get_random_user_id(); gatekeeper.add_update_user(user_id).unwrap(); - // Add also an appointment so we can check the returned data - let appointment = generate_dummy_appointment(None); - let uuid = generate_uuid(); - gatekeeper - .add_update_appointment(user_id, uuid, &appointment) - .unwrap(); - // Check that data is not yet outdated - assert_eq!(gatekeeper.get_outdated_users(start_height).len(), 0); + assert_eq!(gatekeeper.get_outdated_users(start_height), vec![]); // Add an outdated user and check again - gatekeeper.add_outdated_user(user_id, start_height, None); - let outdated_users = gatekeeper.get_outdated_users(start_height); - assert_eq!(outdated_users.len(), 1); - assert_eq!(outdated_users[&user_id], HashSet::from_iter([uuid])); + gatekeeper.add_outdated_user(user_id, start_height); + assert_eq!(gatekeeper.get_outdated_users(start_height), vec![user_id]); } #[test] - fn test_get_outdated_appointments() { - let start_height = START_HEIGHT as u32 + EXPIRY_DELTA; - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(start_height as usize)); - - // get_outdated_appointments returns a list of appointments that were outdated at a given block height, indistinguishably of their user. - - // If there are no outdated users, there cannot be outdated appointments - for i in 0..start_height { - assert_eq!(gatekeeper.get_outdated_appointments(i).len(), 0); - } - - // Adding data about different users and appointments should return a flattened list of appointments - let user1_id = get_random_user_id(); - let user2_id = get_random_user_id(); - let uuid1 = generate_uuid(); - let uuid2 = generate_uuid(); - - // Manually set the user expiry for the test - for (user_id, uuid) in [(user1_id, uuid1), (user2_id, uuid2)] { - gatekeeper.add_outdated_user(user_id, start_height, Some(Vec::from_iter([uuid]))); - } - - let outdated_appointments = gatekeeper.get_outdated_appointments(start_height); - assert_eq!(outdated_appointments.len(), 2); - assert!(outdated_appointments.contains(&uuid1)); - assert!(outdated_appointments.contains(&uuid2)); - } - - #[test] - fn test_delete_appointments_from_memory() { + fn test_delete_appointments_without_refund() { let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let n_users = 100; + let n_apps = 10; + let mut uuids_to_delete = Vec::new(); + let mut rest = Vec::new(); + let mut trackers = Vec::new(); + let mut users_info = HashMap::new(); - // delete_appointments will remove a list of appointments from the Gatekeeper (as long as they exist) - let mut all_appointments = HashMap::new(); - let mut to_be_deleted = HashMap::new(); - let mut rest = HashMap::new(); - for i in 1..11 { + for _ in 0..n_users { let user_id = get_random_user_id(); - let uuid = generate_uuid(); - all_appointments.insert(uuid, user_id); - - if i % 2 == 0 { - to_be_deleted.insert(uuid, user_id); - } else { - rest.insert(uuid, user_id); - } - } - - // Calling the method with unknown data should work but do nothing - assert!(gatekeeper.registered_users.lock().unwrap().is_empty()); - assert!(gatekeeper - .delete_appointments_from_memory(&all_appointments) - .is_empty()); - - // If there's matching data in the gatekeeper it should be deleted - for (uuid, user_id) in to_be_deleted.iter() { - gatekeeper.add_update_user(*user_id).unwrap(); - gatekeeper - .add_update_appointment(*user_id, *uuid, &generate_dummy_appointment(None)) - .unwrap(); - } - - // Check before deleting - assert_eq!(gatekeeper.registered_users.lock().unwrap().len(), 5); - for (uuid, user_id) in to_be_deleted.iter() { - assert!(gatekeeper.registered_users.lock().unwrap()[user_id] - .appointments - .contains_key(uuid)); - - // The slot count should be decreased now too (both in memory and in the database) - assert_ne!( - gatekeeper.registered_users.lock().unwrap()[user_id].available_slots, - gatekeeper.subscription_slots - ); - assert_ne!( + gatekeeper.add_update_user(user_id).unwrap(); + for i in 0..n_apps { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + gatekeeper + .add_update_appointment(user_id, uuid, &appointment) + .unwrap(); + // Add the appointment to the database. This is normally done by the Watcher. gatekeeper .dbm .lock() .unwrap() - .load_user(*user_id) + .store_appointment(uuid, &appointment) + .unwrap(); + if i % 2 == 0 { + uuids_to_delete.push(uuid); + } else { + rest.push(uuid); + } + // Also trigger some of these appointments as trackers. + if i % 5 == 0 { + gatekeeper + .dbm + .lock() + .unwrap() + .store_tracker( + uuid, + &get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)), + ) + .unwrap(); + trackers.push(uuid); + } + } + users_info.insert(user_id, gatekeeper.get_user_info(user_id).unwrap().0); + } + + // Delete these appointments without refunding their owners. + gatekeeper.delete_appointments(uuids_to_delete.clone(), false); + + for uuid in uuids_to_delete.clone() { + assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + } + for uuid in rest { + assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + } + for uuid in trackers { + if uuids_to_delete.contains(&uuid) { + // The tracker should be deleted as well. + assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + } else { + assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + } + } + + for (user_id, user_info_before_deletion) in users_info { + // Since `refund` was false, the users' slots should not have changed after deleting appointments. + let (user_info_after_deletion, _) = gatekeeper.get_user_info(user_id).unwrap(); + assert_eq!(user_info_after_deletion, user_info_before_deletion); + } + } + + #[test] + fn test_delete_appointments_with_refund() { + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let n_users = 100; + let n_apps = 10; + let mut uuids_to_delete = Vec::new(); + let mut rest = Vec::new(); + let mut trackers = Vec::new(); + let mut users_remaining_slots = HashMap::new(); + + for _ in 0..n_users { + let user_id = get_random_user_id(); + gatekeeper.add_update_user(user_id).unwrap(); + let mut user_remaining_slots = + gatekeeper.get_user_info(user_id).unwrap().0.available_slots; + for i in 0..n_apps { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + gatekeeper + .add_update_appointment(user_id, uuid, &appointment) + .unwrap(); + // Add the appointment to the database. This is normally done by the Watcher. + gatekeeper + .dbm + .lock() .unwrap() - .available_slots, - gatekeeper.subscription_slots - ); - } - for (_, user_id) in rest.iter() { - assert!(!gatekeeper - .registered_users - .lock() - .unwrap() - .contains_key(user_id)); + .store_appointment(uuid, &appointment) + .unwrap(); + if i % 2 == 0 { + // We don't reduce the remaining slots for the appointments which are + // going to delete since we will refund their owners. + uuids_to_delete.push(uuid); + } else { + rest.push(uuid); + user_remaining_slots -= compute_appointment_slots( + appointment.encrypted_blob().len(), + ENCRYPTED_BLOB_MAX_SIZE, + ); + } + // Also trigger some of these appointments as trackers. + if i % 5 == 0 { + gatekeeper + .dbm + .lock() + .unwrap() + .store_tracker( + uuid, + &get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)), + ) + .unwrap(); + trackers.push(uuid); + } + } + users_remaining_slots.insert(user_id, user_remaining_slots); } - // And after - gatekeeper.delete_appointments_from_memory(&all_appointments); - for (uuid, user_id) in to_be_deleted.iter() { - assert!(!gatekeeper.registered_users.lock().unwrap()[user_id] - .appointments - .contains_key(uuid)); + // Delete these appointments and refund their owners their slots back. + gatekeeper.delete_appointments(uuids_to_delete.clone(), true); - // The slot count is back to default + for uuid in uuids_to_delete.clone() { + assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + } + for uuid in rest { + assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + } + for uuid in trackers { + if uuids_to_delete.contains(&uuid) { + // The tracker should be deleted as well. + assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + } else { + assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + } + } + + for (user_id, correct_remaining_slots) in users_remaining_slots { + let remaining_slots_from_db = + gatekeeper.get_user_info(user_id).unwrap().0.available_slots; + assert_eq!(remaining_slots_from_db, correct_remaining_slots); assert_eq!( - gatekeeper.registered_users.lock().unwrap()[user_id].available_slots, - gatekeeper.subscription_slots + gatekeeper.registered_users.lock().unwrap()[&user_id].available_slots, + correct_remaining_slots ); } - for (_, user_id) in rest.iter() { - assert!(!gatekeeper - .registered_users - .lock() - .unwrap() - .contains_key(user_id)); - } } #[test] @@ -836,7 +874,7 @@ mod tests { let user3_id = get_random_user_id(); for user_id in &[user1_id, user2_id, user3_id] { - gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1, None) + gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1) } // Connect a new block. Outdated users are deleted @@ -849,10 +887,7 @@ mod tests { .lock() .unwrap() .contains_key(user_id)); - assert!(matches!( - gatekeeper.dbm.lock().unwrap().load_user(*user_id), - Err(DBError::NotFound) - )); + assert!(gatekeeper.dbm.lock().unwrap().load_user(*user_id).is_none()); } // Check that the last_known_block_header has been properly updated diff --git a/teos/src/main.rs b/teos/src/main.rs index 97a882c..15c1591 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -3,27 +3,26 @@ use simple_logger::SimpleLogger; use std::fs; use std::io::ErrorKind; use std::ops::{Deref, DerefMut}; -use std::str::FromStr; use std::sync::{Arc, Condvar, Mutex}; use structopt::StructOpt; use tokio::task; use tonic::transport::{Certificate, Server, ServerTlsConfig}; -use bitcoin::network::constants::Network; +use bitcoin::network::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; -use bitcoincore_rpc::{Auth, Client}; +use bitcoincore_rpc::{Auth, Client, RpcApi}; use lightning_block_sync::init::validate_best_block_header; use lightning_block_sync::poll::{ ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader, }; -use lightning_block_sync::{BlockSource, SpvClient, UnboundedCache}; +use lightning_block_sync::{BlockSource, BlockSourceError, SpvClient, UnboundedCache}; use teos::api::internal::InternalAPI; use teos::api::{http, tor::TorAPI}; use teos::bitcoin_cli::BitcoindClient; use teos::carrier::Carrier; use teos::chain_monitor::ChainMonitor; -use teos::config::{self, Config, Opt}; +use teos::config::{self, AuthMethod, Config, Opt}; use teos::dbm::DBM; use teos::gatekeeper::Gatekeeper; use teos::protos as msgs; @@ -41,22 +40,20 @@ async fn get_last_n_blocks( poller: &mut ChainPoller, mut last_known_block: ValidatedBlockHeader, n: usize, -) -> Vec +) -> Result, BlockSourceError> where B: DerefMut + Sized + Send + Sync, T: BlockSource, { 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 - .look_up_previous_header(&last_known_block) - .await - .unwrap(); + log::debug!("Fetching block #{}", last_known_block.height); + let block = poller.fetch_block(&last_known_block).await?; + last_known_block = poller.look_up_previous_header(&last_known_block).await?; last_n_blocks.push(block); } - last_n_blocks + Ok(last_n_blocks) } fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) { @@ -69,7 +66,7 @@ fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) { async fn main() { let opt = Opt::from_args(); let path = config::data_dir_absolute_path(opt.data_dir.clone()); - + let conf_file_path = path.join("teos.toml"); // Create data dir if it does not exist fs::create_dir_all(&path).unwrap_or_else(|e| { eprintln!("Cannot create data dir: {e:?}"); @@ -77,7 +74,7 @@ async fn main() { }); // Load conf (from file or defaults) and patch it with the command line parameters received (if any) - let mut conf = config::from_file::(path.join("teos.toml")); + let mut conf = config::from_file::(&conf_file_path); let is_default = conf.is_default(); conf.patch_with_options(opt); conf.verify().unwrap_or_else(|e| { @@ -103,18 +100,27 @@ async fn main() { .init() .unwrap(); - if is_default { - log::info!("Loading default configuration") - } else { - log::info!("Loading configuration from file") - } - // Create network dir let path_network = path.join(conf.btc_network.clone()); fs::create_dir_all(&path_network).unwrap_or_else(|e| { eprintln!("Cannot create network dir: {e:?}"); std::process::exit(1); }); + + // Log default data dir + log::info!("Default data directory: {:?}", &path); + + // Log datadir path + log::info!("Using data directory: {:?}", &path_network); + + // Log config file path based on whether the config file is found or not + if is_default { + log::info!("Config file: {:?} (not found, skipping)", &conf_file_path); + } else { + log::info!("Config file: {:?}", &conf_file_path); + conf.log_non_default_options(); + } + let dbm = Arc::new(Mutex::new( DBM::new(path_network.join("teos_db.sql3")).unwrap(), )); @@ -126,24 +132,29 @@ async fn main() { if conf.overwrite_key { log::info!("Overwriting tower keys"); create_new_tower_keypair(&locked_db) + } else if let Some(sk) = locked_db.load_tower_key() { + (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)) } else { - match locked_db.load_tower_key() { - Ok(sk) => (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)), - Err(_) => { - log::info!("Tower keys not found. Creating a fresh set"); - create_new_tower_keypair(&locked_db) - } - } + log::info!("Tower keys not found. Creating a fresh set"); + create_new_tower_keypair(&locked_db) } }; log::info!("tower_id: {tower_pk}"); + let btc_rpc_auth = match conf.get_auth_method() { + AuthMethod::CookieFile => { + Auth::CookieFile(config::data_dir_absolute_path(conf.btc_rpc_cookie)) + } + AuthMethod::UserPass => Auth::UserPass(conf.btc_rpc_user, conf.btc_rpc_password), + // Notice an invalid conf would have failed on `Config::verify()` + _ => unreachable!("A verified conf will only have one of these two auth methods"), + }; + // Initialize our bitcoind client let (bitcoin_cli, bitcoind_reachable) = match BitcoindClient::new( &conf.btc_rpc_connect, conf.btc_rpc_port, - &conf.btc_rpc_user, - &conf.btc_rpc_password, + btc_rpc_auth.clone(), &conf.btc_network, ) .await @@ -172,22 +183,61 @@ async fn main() { let rpc = Arc::new( Client::new( &format!("{schema}{}:{}", conf.btc_rpc_connect, conf.btc_rpc_port), - Auth::UserPass(conf.btc_rpc_user.clone(), conf.btc_rpc_password.clone()), + btc_rpc_auth, ) .unwrap(), ); let mut derefed = bitcoin_cli.deref(); // Load last known block from DB if found. Poll it from Bitcoind otherwise. let last_known_block = dbm.lock().unwrap().load_last_known_block(); - let tip = if let Ok(block_hash) = last_known_block { - derefed + let tip = if let Some(block_hash) = last_known_block { + let mut last_known_header = derefed .get_header(&block_hash, None) .await .unwrap() .validate(block_hash) - .unwrap() + .unwrap(); + + log::info!( + "Last known block: {} (height: {})", + last_known_header.header.block_hash(), + last_known_header.height + ); + + // If we are running in pruned mode some data may be missing (if we happen to have been offline for a while) + if let Some(prune_height) = rpc.get_blockchain_info().unwrap().prune_height { + if last_known_header.height - IRREVOCABLY_RESOLVED + 1 < prune_height as u32 { + log::warn!( + "Cannot load blocks in the range {}-{}. Chain has gone too far out of sync", + last_known_header.height - IRREVOCABLY_RESOLVED + 1, + last_known_header.height + ); + if conf.force_update { + log::info!("Forcing a backend update"); + // We want to grab the first IRREVOCABLY_RESOLVED we know about for the initial cache + // So we can perform transitions from there onwards. + let target_height = prune_height + IRREVOCABLY_RESOLVED as u64; + let target_hash = rpc.get_block_hash(target_height).unwrap(); + last_known_header = derefed + .get_header( + &rpc.get_block_hash(target_height).unwrap(), + Some(target_height as u32), + ) + .await + .unwrap() + .validate(target_hash) + .unwrap(); + } else { + log::error!( + "The underlying chain has gone too far out of sync. The tower block cache cannot be initialized. Run with --forceupdate to force update. THIS WILL, POTENTIALLY, MAKE THE TOWER MISS SOME OF ITS APPOINTMENTS" + ); + std::process::exit(1); + } + } + } + last_known_header } else { - validate_best_block_header(&mut derefed).await.unwrap() + validate_best_block_header(&derefed).await.unwrap() }; // DISCUSS: This is not really required (and only triggered in regtest). This is only in place so the caches can be @@ -202,17 +252,11 @@ async fn main() { std::process::exit(1); } - log::info!("Last known block: {}", tip.header.block_hash()); - - // 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; + log::info!( + "Current chain tip: {} (height: {})", + tip.header.block_hash(), + tip.height + ); // Build components let gatekeeper = Arc::new(Gatekeeper::new( @@ -223,23 +267,38 @@ async fn main() { 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[0..6], - tip.height, - tower_sk, - TowerId(tower_pk), - dbm.clone(), - )); + let mut poller = ChainPoller::new( + &mut derefed, + Network::from_core_arg(&conf.btc_network).unwrap(), + ); + let (responder, watcher) = { + let last_n_blocks = get_last_n_blocks(&mut poller, tip, IRREVOCABLY_RESOLVED as usize) + .await.unwrap_or_else(|e| { + // I'm pretty sure this can only happen if we are pulling blocks from the target to the prune height, and by the time we get to + // the end at least one has been pruned. + log::error!("Couldn't load the latest {IRREVOCABLY_RESOLVED} blocks. Please try again (Error: {})", e.into_inner()); + std::process::exit(1); + } + ); + + let responder = Arc::new(Responder::new( + &last_n_blocks, + tip.height, + Carrier::new(rpc, bitcoind_reachable.clone(), tip.height), + gatekeeper.clone(), + dbm.clone(), + )); + let watcher = Arc::new(Watcher::new( + gatekeeper.clone(), + responder.clone(), + &last_n_blocks[0..6], + tip.height, + tower_sk, + TowerId(tower_pk), + dbm.clone(), + )); + (responder, watcher) + }; if watcher.is_fresh() & responder.is_fresh() & gatekeeper.is_fresh() { log::info!("Fresh bootstrap"); @@ -248,14 +307,14 @@ async fn main() { } let (shutdown_trigger, shutdown_signal_rpc_api) = triggered::trigger(); - let shutdown_signal_internal_rpc_api = shutdown_signal_rpc_api.clone(); + let shutdown_signal_internal_api = shutdown_signal_rpc_api.clone(); let shutdown_signal_http = shutdown_signal_rpc_api.clone(); let shutdown_signal_cm = shutdown_signal_rpc_api.clone(); let shutdown_signal_tor = shutdown_signal_rpc_api.clone(); // The ordering here actually matters. Listeners are called by order, and we want the gatekeeper to be called - // last, so both the Watcher and the Responder can query the necessary data from it during data deletion. - let listener = &(watcher.clone(), &(responder, gatekeeper)); + // first so it updates the users' states and both the Watcher and the Responder operate only on registered users. + let listener = &(gatekeeper, &(watcher.clone(), responder)); let cache = &mut UnboundedCache::new(); let spv_client = SpvClient::new(tip, poller, cache, listener); let mut chain_monitor = ChainMonitor::new( @@ -300,24 +359,20 @@ async fn main() { None }; - let rpc_api = Arc::new(InternalAPI::new( + let internal_api = Arc::new(InternalAPI::new( watcher, addresses, bitcoind_reachable.clone(), shutdown_trigger, )); - let internal_rpc_api = rpc_api.clone(); + let internal_api_cloned = internal_api.clone(); let rpc_api_addr = format!("{}:{}", conf.rpc_bind, conf.rpc_port) .parse() .unwrap(); - let internal_rpc_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port) + let internal_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port) .parse() .unwrap(); - let internal_rpc_api_uri = format!( - "http://{}:{}", - conf.internal_api_bind, conf.internal_api_port - ); // Generate mtls certificates to data directory so the admin can securely connect // to the server to perform administrative tasks. @@ -335,7 +390,7 @@ async fn main() { Server::builder() .tls_config(tls) .expect("couldn't configure tls") - .add_service(PrivateTowerServicesServer::new(rpc_api)) + .add_service(PrivateTowerServicesServer::new(internal_api)) .serve_with_shutdown(rpc_api_addr, shutdown_signal_rpc_api) .await .unwrap(); @@ -343,8 +398,8 @@ async fn main() { let public_api_task = task::spawn(async move { Server::builder() - .add_service(PublicTowerServicesServer::new(internal_rpc_api)) - .serve_with_shutdown(internal_rpc_api_addr, shutdown_signal_internal_rpc_api) + .add_service(PublicTowerServicesServer::new(internal_api_cloned)) + .serve_with_shutdown(internal_api_addr, shutdown_signal_internal_api) .await .unwrap(); }); @@ -352,7 +407,7 @@ async fn main() { let (http_service_ready, ready_signal_http) = triggered::trigger(); let http_api_task = task::spawn(http::serve( http_api_addr, - internal_rpc_api_uri, + internal_api_addr, http_service_ready, shutdown_signal_http, )); diff --git a/teos/src/responder.rs b/teos/src/responder.rs index 0a0a1cd..f816aac 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -1,11 +1,12 @@ //! Logic related to the Responder, the components in charge of making sure breaches get properly punished. -use std::collections::{HashMap, HashSet}; -use std::iter::FromIterator; +use std::collections::HashSet; use std::sync::{Arc, Mutex}; +use bitcoin::hashes::Hash; use bitcoin::{consensus, BlockHash}; -use bitcoin::{BlockHeader, Transaction, Txid}; +use bitcoin::{Transaction, Txid}; + use lightning::chain; use lightning_block_sync::poll::ValidatedBlock; @@ -16,7 +17,7 @@ use teos_common::UserId; use crate::carrier::Carrier; use crate::dbm::DBM; use crate::extended_appointment::UUID; -use crate::gatekeeper::{Gatekeeper, UserInfo}; +use crate::gatekeeper::Gatekeeper; use crate::tx_index::TxIndex; use crate::watcher::Breach; @@ -30,14 +31,6 @@ pub enum ConfirmationStatus { InMempoolSince(u32), IrrevocablyResolved, Rejected(i32), - ReorgedOut, -} - -/// Reason why the tracker is deleted. Used for logging purposes. -enum DeletionReason { - Outdated, - Rejected, - Completed, } impl ConfirmationStatus { @@ -72,17 +65,6 @@ impl ConfirmationStatus { } } -/// Minimal data required in memory to keep track of transaction trackers. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct TrackerSummary { - /// Identifier of the user who arranged the appointment. - user_id: UserId, - /// Transaction id the [Responder] is keeping track of. - penalty_txid: Txid, - /// The confirmation status of a given tracker. - status: ConfirmationStatus, -} - /// Structure to keep track of triggered appointments. /// /// It is analogous to [ExtendedAppointment](crate::extended_appointment::ExtendedAppointment) for the [`Watcher`](crate::watcher::Watcher). @@ -108,27 +90,44 @@ impl TransactionTracker { user_id, } } - - /// Computes the [TrackerSummary] of the [TransactionTracker]. - pub fn get_summary(&self) -> TrackerSummary { - TrackerSummary { - user_id: self.user_id, - penalty_txid: self.penalty_tx.txid(), - status: self.status, - } - } } impl From for common_msgs::Tracker { fn from(t: TransactionTracker) -> Self { common_msgs::Tracker { - dispute_txid: t.dispute_tx.txid().to_vec(), - penalty_txid: t.penalty_tx.txid().to_vec(), + dispute_txid: t + .dispute_tx + .compute_txid() + .to_raw_hash() + .to_byte_array() + .to_vec(), + penalty_txid: t + .penalty_tx + .compute_txid() + .to_raw_hash() + .to_byte_array() + .to_vec(), penalty_rawtx: consensus::serialize(&t.penalty_tx), } } } +/// A struct that packages the summary of a tracker's penalty transaction. +#[derive(Debug, PartialEq)] +pub(crate) struct PenaltySummary { + pub penalty_txid: Txid, + pub status: ConfirmationStatus, +} + +impl PenaltySummary { + pub fn new(penalty_txid: Txid, status: ConfirmationStatus) -> Self { + PenaltySummary { + penalty_txid, + status, + } + } +} + /// Component in charge of keeping track of triggered appointments. /// /// The [Responder] receives data from the [Watcher](crate::watcher::Watcher) in form of a [Breach]. @@ -136,11 +135,6 @@ impl From for common_msgs::Tracker { /// The [Transaction] is then monitored to make sure it makes it to a block and it gets [irrevocably resolved](https://github.com/lightning/bolts/blob/master/05-onchain.md#general-nomenclature). #[derive(Debug)] pub struct Responder { - /// A map holding a summary of every tracker ([TransactionTracker]) hold by the [Responder], identified by [UUID]. - /// The identifiers match those used by the [Watcher](crate::watcher::Watcher). - 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. @@ -149,6 +143,8 @@ pub struct Responder { gatekeeper: Arc, /// A [DBM] (database manager) instance. Used to persist tracker data into disk. dbm: Arc>, + /// A list of all the reorged trackers that might need to be republished after reorg resolution. + reorged_trackers: Mutex>, } impl Responder { @@ -160,37 +156,28 @@ impl Responder { gatekeeper: Arc, dbm: Arc>, ) -> Self { - let mut trackers = HashMap::new(); - let mut tx_tracker_map: HashMap> = HashMap::new(); - - for (uuid, tracker) in dbm.lock().unwrap().load_trackers(None) { - trackers.insert(uuid, tracker.get_summary()); - - if let Some(map) = tx_tracker_map.get_mut(&tracker.penalty_tx.txid()) { - map.insert(uuid); - } else { - tx_tracker_map.insert(tracker.penalty_tx.txid(), HashSet::from_iter(vec![uuid])); - } - } - 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, + reorged_trackers: Mutex::new(HashSet::new()), } } /// Returns whether the [Responder] has been created from scratch (fresh) or from backed-up data. pub fn is_fresh(&self) -> bool { - self.trackers.lock().unwrap().is_empty() + self.get_trackers_count() == 0 } - /// Gets the total number of trackers in the responder. + /// Gets the total number of trackers in the [Responder]. pub(crate) fn get_trackers_count(&self) -> usize { - self.trackers.lock().unwrap().len() + self.dbm.lock().unwrap().get_trackers_count() + } + + /// Checks whether the [Responder] has gone through a reorg and some transactions should to be resent. + fn coming_from_reorg(&self) -> bool { + !self.reorged_trackers.lock().unwrap().is_empty() } /// Data entry point for the [Responder]. Handles a [Breach] provided by the [Watcher](crate::watcher::Watcher). @@ -203,21 +190,15 @@ impl Responder { breach: Breach, user_id: UserId, ) -> ConfirmationStatus { - // Do not add already added trackers. This can only happen if handle_breach is called twice with the same data, which can only happen - // if Watcher::block_connected is interrupted during execution and called back during bootstrap. - if let Some(tracker) = self.trackers.lock().unwrap().get(&uuid) { - return tracker.status; - } - 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()) { + let status = if let Some(block_hash) = tx_index.get(&breach.penalty_tx.compute_txid()) { + ConfirmationStatus::ConfirmedIn(tx_index.get_height(block_hash).unwrap() as u32) + } else if carrier.in_mempool(&breach.penalty_tx.compute_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) }; @@ -244,271 +225,177 @@ impl Responder { user_id: UserId, status: ConfirmationStatus, ) { - let tracker = TransactionTracker::new(breach, user_id, status); - - self.trackers + if self + .dbm .lock() .unwrap() - .insert(uuid, tracker.get_summary()); - - let mut tx_tracker_map = self.tx_tracker_map.lock().unwrap(); - if let Some(map) = tx_tracker_map.get_mut(&tracker.penalty_tx.txid()) { - map.insert(uuid); + .store_tracker(uuid, &TransactionTracker::new(breach, user_id, status)) + .is_ok() + { + log::info!("New tracker added (uuid={uuid})"); } else { - tx_tracker_map.insert(tracker.penalty_tx.txid(), HashSet::from_iter(vec![uuid])); + log::error!( + "Failed to store tracker in database (uuid={uuid}). It might be already stored." + ); } - - self.dbm - .lock() - .unwrap() - .store_tracker(uuid, &tracker) - .unwrap(); - log::info!("New tracker added (uuid={uuid})"); } /// Checks whether a given tracker can be found in the [Responder]. pub(crate) fn has_tracker(&self, uuid: UUID) -> bool { - // has_tracker should return true as long as the given tracker is hold by the Responder. - // If the tracker is partially kept, the function will log and the return will be false. - // This may point out that some partial data deletion is happening, which must be fixed. - self.trackers - .lock() - .unwrap() - .get(&uuid) - .map_or(false, |tracker| { - self.tx_tracker_map - .lock() - .unwrap() - .get(&tracker.penalty_txid) - .map_or( - { - log::debug!( - "Partially found Tracker. Some data may have not been properly deleted" - ); - false - }, - |_| true, - ) - }) - } - - /// Gets a tracker from the [Responder] if found. [None] otherwise. - /// - /// The [TransactionTracker] is queried to the [DBM]. - pub(crate) fn get_tracker(&self, uuid: UUID) -> Option { - if self.trackers.lock().unwrap().contains_key(&uuid) { - self.dbm.lock().unwrap().load_tracker(uuid).ok() - } else { - None - } + self.dbm.lock().unwrap().tracker_exists(uuid) } /// Checks the confirmation count for the [TransactionTracker]s. /// /// For unconfirmed transactions, it checks whether they have been confirmed or keep missing confirmations. /// For confirmed transactions, nothing is done until they are completed (confirmation count reaches [IRREVOCABLY_RESOLVED](constants::IRREVOCABLY_RESOLVED)) - /// Returns the set of completed trackers. - fn check_confirmations(&self, txids: &[Txid], current_height: u32) -> HashSet { - let mut completed_trackers = HashSet::new(); + /// Returns the set of completed trackers or [None] if none were completed. + fn check_confirmations(&self, txids: HashSet, current_height: u32) -> Option> { + let mut completed_trackers = Vec::new(); + let mut reorged_trackers = self.reorged_trackers.lock().unwrap(); + let dbm = self.dbm.lock().unwrap(); - for (uuid, tracker) in self.trackers.lock().unwrap().iter_mut() { - if let ConfirmationStatus::ConfirmedIn(h) = tracker.status { + for (uuid, penalty_summary) in dbm.load_penalties_summaries() { + if txids.contains(&penalty_summary.penalty_txid) { + // First confirmation was received + dbm.update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(current_height)) + .unwrap(); + // Remove that uuid from reorged trackers if it was confirmed. + reorged_trackers.remove(&uuid); + // TODO: We won't need this check when we persist the correct tracker status + // in the DB after migrations are supported. + } else if reorged_trackers.contains(&uuid) { + // Don't consider reorged trackers since they have wrong DB status. + continue; + } else if let ConfirmationStatus::ConfirmedIn(h) = penalty_summary.status { let confirmations = current_height - h; if confirmations == constants::IRREVOCABLY_RESOLVED { // Tracker is deep enough in the chain, it can be deleted - completed_trackers.insert(*uuid); + completed_trackers.push(uuid); } else { log::info!("{uuid} received a confirmation (count={confirmations})"); } - } else if txids.contains(&tracker.penalty_txid) { - // First confirmation was received - tracker.status = ConfirmationStatus::ConfirmedIn(current_height); - } else if let ConfirmationStatus::InMempoolSince(h) = tracker.status { + } else if let ConfirmationStatus::InMempoolSince(h) = penalty_summary.status { // Log all transactions that have missed confirmations log::info!( "Transaction missed a confirmation: {} (missed conf count: {})", - tracker.penalty_txid, + penalty_summary.penalty_txid, current_height - h ); } } - completed_trackers + (!completed_trackers.is_empty()).then_some(completed_trackers) } - /// Gets a map of transactions that need to be rebroadcast. A [Transaction] is flagged to be rebroadcast - /// if its missed confirmation count has reached the threshold ([CONFIRMATIONS_BEFORE_RETRY]) or if they have been - /// reorged out of the chain. If the transaction has been reorged out, the commitment transaction is also returned. + /// Handles the reorged out trackers when we start connecting to the stronger chain. /// - /// Given the [Responder] only keeps around the minimal data to track transactions, the [TransactionTracker]s - /// are queried to the [DBM]. - fn get_txs_to_rebroadcast( - &self, - height: u32, - ) -> HashMap)> { - let dbm = self.dbm.lock().unwrap(); - let mut tx_to_rebroadcast = HashMap::new(); - let mut tracker: TransactionTracker; - - for (uuid, t) in self.trackers.lock().unwrap().iter() { - if let ConfirmationStatus::InMempoolSince(h) = t.status { - if (height - h) as u8 >= CONFIRMATIONS_BEFORE_RETRY { - tracker = dbm.load_tracker(*uuid).unwrap(); - tx_to_rebroadcast.insert(*uuid, (tracker.penalty_tx, None)); - } - } else if let ConfirmationStatus::ReorgedOut = t.status { - tracker = dbm.load_tracker(*uuid).unwrap(); - tx_to_rebroadcast.insert(*uuid, (tracker.penalty_tx, Some(tracker.dispute_tx))); - } - } - - tx_to_rebroadcast - } - - /// Gets a collection of trackers that have been outdated. An outdated tracker is a [TransactionTracker] - /// from a user who's subscription has been outdated (and therefore will be removed from the tower). + /// This is called in the first block connection after a bunch of block disconnections. + /// It tries to publish the dispute and penalty transactions of reorged trackers to the blockchain. /// - /// Trackers are only returned as long as they have not been confirmed, otherwise we'll keep watching for then anyway. - fn get_outdated_trackers(&self, block_height: u32) -> HashSet { - let mut outdated_trackers = HashSet::new(); - let trackers = self.trackers.lock().unwrap(); - for uuid in self - .gatekeeper - .get_outdated_appointments(block_height) - .intersection(&trackers.keys().cloned().collect()) - { - if let ConfirmationStatus::InMempoolSince(_) = trackers[uuid].status { - outdated_trackers.insert(*uuid); - } - } - - outdated_trackers - } - - /// Rebroadcasts a list of penalty transactions that have missed too many confirmations (or that have been reorged out). - /// - /// This covers both the case where a transaction is not getting confirmations (most likely due to low fess, and needs to be bumped), - /// and the case where the transaction has been reorged out of the chain. For the former, there's no much to be done at the moment (until anchors), - /// for the latter, we need to rebroadcast the penalty (and potentially the commitment if that has also been reorged). - /// - /// Given how the confirmation status and reorgs work with a bitcoind backend, we will be rebroadcasting this during the first new connected block - /// after a reorg, but bitcoind will already be at the new tip. If the transaction is accepted, we won't do anything else until passed the new tip, - /// otherwise, we could potentially try to rebroadcast again while processing the upcoming reorged blocks (if the tx hits [CONFIRMATIONS_BEFORE_RETRY]). - /// - /// Returns a tuple with two maps, one containing the trackers that where successfully rebroadcast and another one containing the ones that were rejected. - fn rebroadcast( - &self, - txs: HashMap)>, - ) -> (HashMap, HashSet) { - let mut accepted = HashMap::new(); - let mut rejected = HashSet::new(); - - let mut trackers = self.trackers.lock().unwrap(); + /// Returns a vector of rejected trackers during rebroadcast if any were rejected, [None] otherwise. + fn handle_reorged_txs(&self, height: u32) -> Option> { + // NOTE: We are draining the reorged trackers set, meaning that we won't try sending these disputes again. + let reorged_trackers: Vec = self.reorged_trackers.lock().unwrap().drain().collect(); let mut carrier = self.carrier.lock().unwrap(); - let tx_index = self.tx_index.lock().unwrap(); + let dbm = self.dbm.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 (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. - // DISCUSS: For lightning transactions, if the dispute has been reorged the penalty cannot make it to the network. - // If we keep this general, the dispute can simply be a trigger and the penalty doesn't necessarily have to spend from it. - // We'll keel it lightning specific, at least for now. - let status = carrier.send_transaction(&dispute_tx); - if let ConfirmationStatus::Rejected(e) = status { - log::error!( - "Reorged dispute transaction rejected during rebroadcast: {} (reason: {e})", - dispute_tx.txid() + let mut rejected = Vec::new(); + // Republish all the dispute transactions of the reorged trackers. + for uuid in reorged_trackers { + let tracker = dbm.load_tracker(uuid).unwrap(); + let dispute_txid = tracker.dispute_tx.compute_txid(); + // Try to publish the dispute transaction. + let should_publish_penalty = match carrier.send_transaction(&tracker.dispute_tx) { + ConfirmationStatus::InMempoolSince(_) => { + log::info!( + "Reorged dispute tx (txid={}) is in the mempool now", + dispute_txid ); - status - } else { - // The dispute was accepted, so we can rebroadcast the penalty. - carrier.send_transaction(&penalty_tx) - } + true } - } else { - // The tracker has simply reached CONFIRMATIONS_BEFORE_RETRY missed confirmations. - log::warn!( - "Penalty transaction has missed many confirmations: {}", - penalty_tx.txid() - ); - carrier.send_transaction(&penalty_tx) + // NOTE: We aren't fully synced with the bitcoind backend so can't check if the dispute tx is in our txindex. + ConfirmationStatus::IrrevocablyResolved => { + log::info!( + "Reorged dispute tx (txid={}) is already on the strong chain", + dispute_txid + ); + true + } + ConfirmationStatus::Rejected(e) => { + log::error!( + "Reorged dispute tx (txid={}) rejected during rebroadcast (reason: {e:?})", + dispute_txid + ); + false + } + x => unreachable!( + "`Carrier::send_transaction` shouldn't return this variant: {:?}", + x + ), }; - if let ConfirmationStatus::Rejected(_) = status { - rejected.insert(uuid); + if should_publish_penalty { + // Try to rebroadcast the penalty tx. + if let ConfirmationStatus::Rejected(_) = + carrier.send_transaction(&tracker.penalty_tx) + { + rejected.push(uuid) + } else { + // The penalty might actually be confirmed (ConfirmationStatus::IrrevocablyResolved) since bitcoind + // is fully synced with the stronger chain already, but we won't know which block was it confirmed in. + // We should see the tracker appear in the blockchain in the next couple of connected blocks. + dbm.update_tracker_status(uuid, &ConfirmationStatus::InMempoolSince(height)) + .unwrap() + } } else { - // Update the tracker if it gets accepted. This will also update the height (since when we are counting the tracker - // to have been in mempool), so it resets the wait period instead of trying to rebroadcast every block. - // DISCUSS: We may want to find another approach in the future for the InMempoool transactions. - trackers.get_mut(&uuid).unwrap().status = status; - accepted.insert(uuid, status); + rejected.push(uuid) } } - (accepted, rejected) + (!rejected.is_empty()).then_some(rejected) } - // DISCUSS: Check comment regarding callbacks in watcher.rs - - /// Deletes trackers from memory. + /// Rebroadcasts a list of penalty transactions that have missed too many confirmations. /// - /// Logs a different message depending on whether the trackers have been outdated or completed. - fn delete_trackers_from_memory(&self, uuids: &HashSet, reason: DeletionReason) { - let mut trackers = self.trackers.lock().unwrap(); - let mut tx_tracker_map = self.tx_tracker_map.lock().unwrap(); - for uuid in uuids.iter() { - match reason { - DeletionReason::Completed => log::info!("Appointment completed. Penalty transaction was irrevocably confirmed: {uuid}"), - DeletionReason::Outdated => log::info!("Appointment couldn't be completed. Expiry reached but penalty didn't make it to the chain: {uuid}"), - DeletionReason::Rejected => log::info!("Appointment couldn't be completed. Either the dispute or the penalty txs where rejected during rebroadcast: {uuid}"), - } + /// This covers the case where a transaction is not getting confirmations (most likely due to low + /// fess and needs to be bumped, but there is not much we can do until anchors). + /// + /// Returns a vector of rejected trackers during rebroadcast if any were rejected, [None] otherwise. + fn rebroadcast_stale_txs(&self, height: u32) -> Option> { + let dbm = self.dbm.lock().unwrap(); + let mut carrier = self.carrier.lock().unwrap(); + let mut rejected = Vec::new(); - match trackers.remove(uuid) { - Some(tracker) => { - let trackers = tx_tracker_map.get_mut(&tracker.penalty_txid).unwrap(); - - if trackers.len() == 1 { - tx_tracker_map.remove(&tracker.penalty_txid); - - log::info!( - "No more trackers for penalty transaction: {}", - tracker.penalty_txid - ); - } else { - trackers.remove(uuid); - } - } - None => { - // This should never happen. Logging just in case so we can fix it if so - log::error!("Completed tracker not found when cleaning: {uuid}"); - } + // Retry sending trackers which have been in the mempool since more than `CONFIRMATIONS_BEFORE_RETRY` blocks. + let stale_confirmation_status = + ConfirmationStatus::InMempoolSince(height - CONFIRMATIONS_BEFORE_RETRY as u32); + // NOTE: Ideally this will only pull UUIDs which have been in mempool since `CONFIRMATIONS_BEFORE_RETRY`, but + // might also return ones which have been there for a longer period. This can only happen if the tower missed + // a couple of block connections due to a force update. + for uuid in dbm + .load_trackers_with_confirmation_status(stale_confirmation_status) + .unwrap() + { + let tracker = dbm.load_tracker(uuid).unwrap(); + log::warn!( + "Penalty transaction has missed many confirmations: {}", + tracker.penalty_tx.compute_txid() + ); + // Rebroadcast the penalty transaction. + let status = carrier.send_transaction(&tracker.penalty_tx); + if let ConfirmationStatus::Rejected(_) = status { + rejected.push(uuid); + } else { + // DISCUSS: What if the tower was down for some time and was later force updated while this penalty got on-chain? + // Sending it will yield `ConfirmationStatus::IrrevocablyResolved` which would panic here. + // We might want to replace `ConfirmationStatus::IrrevocablyResolved` variant with + // `ConfirmationStatus::ConfirmedIn(height - IRREVOCABLY_RESOLVED) + dbm.update_tracker_status(uuid, &status).unwrap(); } } - } - /// Deletes trackers from memory and the database. - /// - /// Removes all data related to the appointment from the database in cascade. - fn delete_trackers( - &self, - uuids: &HashSet, - updated_users: &HashMap, - reason: DeletionReason, - ) { - if !uuids.is_empty() { - self.delete_trackers_from_memory(uuids, reason); - self.dbm - .lock() - .unwrap() - .batch_remove_appointments(uuids, updated_users); - } + (!rejected.is_empty()).then_some(rejected) } } @@ -526,7 +413,7 @@ impl chain::Listen for Responder { /// rebroadcasting is performed for those that have missed too many. fn filtered_block_connected( &self, - header: &BlockHeader, + header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, height: u32, ) { @@ -535,73 +422,59 @@ impl chain::Listen for Responder { let txs = txdata .iter() - .map(|(_, tx)| (tx.txid(), header.block_hash())) + .map(|(_, tx)| (tx.compute_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::>(), - height, - ); - let trackers_to_delete_gk = completed_trackers - .iter() - .map(|uuid| (*uuid, self.trackers.lock().unwrap()[uuid].user_id)) - .collect(); - self.delete_trackers( - &completed_trackers, - &self - .gatekeeper - .delete_appointments_from_memory(&trackers_to_delete_gk), - DeletionReason::Completed, - ); + // Delete trackers completed at this height + if let Some(trackers) = self.check_confirmations(txs.keys().cloned().collect(), height) { + self.gatekeeper.delete_appointments(trackers, true); + } - // Also delete trackers from outdated users (from memory only, the db deletion is handled by the Gatekeeper) - self.delete_trackers_from_memory( - &self.get_outdated_trackers(height), - DeletionReason::Outdated, - ); - - // Rebroadcast those transactions that need to - let (_, rejected_trackers) = self.rebroadcast(self.get_txs_to_rebroadcast(height)); - // Delete trackers rejected during rebroadcast - let trackers_to_delete_gk = rejected_trackers - .iter() - .map(|uuid| (*uuid, self.trackers.lock().unwrap()[uuid].user_id)) - .collect(); - self.delete_trackers( - &rejected_trackers, - &self - .gatekeeper - .delete_appointments_from_memory(&trackers_to_delete_gk), - DeletionReason::Rejected, - ); - - // Remove all receipts created in this block - self.carrier.lock().unwrap().clear_receipts(); - - if self.trackers.lock().unwrap().is_empty() { - log::info!("No more pending trackers"); + let mut trackers_to_delete = Vec::new(); + // We might be connecting a new block after a disconnection (reorg). + // We will need to update those trackers that have been reorged. + if self.coming_from_reorg() { + // Handle reorged transactions. This clears `self.reorged_trackers`. + if let Some(trackers) = self.handle_reorged_txs(height) { + trackers_to_delete.extend(trackers); } } + + // Rebroadcast those transactions that need to + if let Some(trackers) = self.rebroadcast_stale_txs(height) { + trackers_to_delete.extend(trackers); + } + + if !trackers_to_delete.is_empty() { + self.gatekeeper + .delete_appointments(trackers_to_delete, false); + } + + // Remove all receipts created in this block + self.carrier.lock().unwrap().clear_receipts(); } /// Handles reorgs in the [Responder]. - fn block_disconnected(&self, header: &BlockHeader, height: u32) { + fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); + // Update the carrier and our tx_index. 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. - if tracker.status == ConfirmationStatus::ConfirmedIn(height) { - tracker.status = ConfirmationStatus::ReorgedOut; - } - } + // And store the reorged transactions to be retried later. + // TODO: Not only confirmed trackers need to be marked as reorged, but trackers that hasn't confirmed but their + // dispute did confirm in the reorged block. We can pull dispute txids of non confirmed penalties and get their + // confirmation block from our tx_index. + self.reorged_trackers.lock().unwrap().extend( + self.dbm + .lock() + .unwrap() + .load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(height)) + .unwrap(), + ); } } @@ -609,75 +482,82 @@ impl chain::Listen for Responder { mod tests { use super::*; use lightning::chain::Listen; + use teos_common::appointment::Locator; + use std::collections::HashMap; + use std::iter::FromIterator; 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_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, + create_carrier, generate_dummy_appointment, generate_dummy_appointment_with_user, + generate_uuid, get_last_n_blocks, get_random_breach, get_random_tracker, get_random_tx, + store_appointment_and_its_user, BitcoindStopper, Blockchain, MockedServerQuery, DURATION, + EXPIRY_DELTA, SLOTS, START_HEIGHT, }; use teos_common::constants::IRREVOCABLY_RESOLVED; - use teos_common::dbm::Error as DBError; use teos_common::test_utils::get_random_user_id; + impl TransactionTracker { + pub fn locator(&self) -> Locator { + Locator::new(self.dispute_tx.compute_txid()) + } + + pub fn uuid(&self) -> UUID { + UUID::new(self.locator(), self.user_id) + } + } + impl PartialEq for Responder { fn eq(&self, other: &Self) -> bool { - *self.trackers.lock().unwrap() == *other.trackers.lock().unwrap() - && *self.tx_tracker_map.lock().unwrap() == *other.tx_tracker_map.lock().unwrap() + // Same in-memory data. + *self.reorged_trackers.lock().unwrap() == *other.reorged_trackers.lock().unwrap() && + *self.tx_index.lock().unwrap() == *other.tx_index.lock().unwrap() && + // && Same DB data. + self.get_trackers() == other.get_trackers() } } impl Eq for Responder {} impl Responder { - pub(crate) fn get_trackers(&self) -> &Mutex> { - &self.trackers + pub(crate) fn get_trackers(&self) -> HashMap { + self.dbm.lock().unwrap().load_trackers(None) } pub(crate) fn get_carrier(&self) -> &Mutex { &self.carrier } - pub(crate) fn add_random_tracker( - &self, - uuid: UUID, - status: ConfirmationStatus, - ) -> TransactionTracker { + pub(crate) fn add_random_tracker(&self, status: ConfirmationStatus) -> TransactionTracker { let user_id = get_random_user_id(); let tracker = get_random_tracker(user_id, status); - self.add_dummy_tracker(uuid, &tracker); + self.add_dummy_tracker(&tracker); tracker } - pub(crate) fn add_dummy_tracker(&self, uuid: UUID, tracker: &TransactionTracker) { - // Add data to memory - self.trackers - .lock() - .unwrap() - .insert(uuid, tracker.get_summary()); - self.tx_tracker_map - .lock() - .unwrap() - .insert(tracker.penalty_tx.txid(), HashSet::from_iter([uuid])); - - // Add data to the db + pub(crate) fn add_dummy_tracker(&self, tracker: &TransactionTracker) { let (_, appointment) = generate_dummy_appointment_with_user( tracker.user_id, - Some(&tracker.dispute_tx.txid()), + Some(&tracker.dispute_tx.compute_txid()), ); - store_appointment_and_fks_to_db(&self.dbm.lock().unwrap(), uuid, &appointment); + store_appointment_and_its_user(&self.dbm.lock().unwrap(), &appointment); self.dbm .lock() .unwrap() - .store_tracker(uuid, tracker) + .store_tracker(appointment.uuid(), tracker) .unwrap(); } + + fn store_dummy_appointment_to_db(&self) -> (UserId, UUID) { + let appointment = generate_dummy_appointment(None); + let (uuid, user_id) = (appointment.uuid(), appointment.user_id); + // Store the appointment and the user to the DB. + store_appointment_and_its_user(&self.dbm.lock().unwrap(), &appointment); + (user_id, uuid) + } } async fn create_responder( @@ -725,23 +605,17 @@ mod tests { #[test] fn test_confirmation_status_from_db_data() { // These are pretty simple tests. The db can only store trackers with a confirmation status - // that's either ConfirmedIn or InMempoolSince (Rejected and Reorged are never passed to store). + // that's either ConfirmedIn or InMempoolSince (Rejected and IrrevocablyResolved are never passed to store). let h = 21; - let statuses = [true, false]; - for status in statuses { - if status { - assert_eq!( - ConfirmationStatus::from_db_data(h, status), - ConfirmationStatus::ConfirmedIn(h) - ); - } else { - assert_eq!( - ConfirmationStatus::from_db_data(h, status), - ConfirmationStatus::InMempoolSince(h) - ); - } - } + assert_eq!( + ConfirmationStatus::from_db_data(h, true), + ConfirmationStatus::ConfirmedIn(h) + ); + assert_eq!( + ConfirmationStatus::from_db_data(h, false), + ConfirmationStatus::InMempoolSince(h) + ); } #[test] @@ -759,7 +633,7 @@ mod tests { Some((h, false)) ); assert_eq!(ConfirmationStatus::Rejected(0).to_db_data(), None); - assert_eq!(ConfirmationStatus::ReorgedOut.to_db_data(), None); + assert_eq!(ConfirmationStatus::IrrevocablyResolved.to_db_data(), None); } #[tokio::test] @@ -775,11 +649,7 @@ mod tests { // If we add some trackers to the system and create a new Responder reusing the same db // (as if simulating a bootstrap from existing data), the data should be properly loaded. for i in 0..10 { - // Add the necessary FKs in the database - 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 (user_id, uuid) = responder.store_dummy_appointment_to_db(); let breach = get_random_breach(); let s = if i % 2 == 0 { ConfirmationStatus::InMempoolSince(i) @@ -801,46 +671,31 @@ mod tests { let start_height = START_HEIGHT as u32; 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 (user_id, uuid) = responder.store_dummy_appointment_to_db(); 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)); + let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); assert_eq!( - responder.trackers.lock().unwrap()[&uuid].status, + tracker.status, ConfirmationStatus::InMempoolSince(start_height) ); - assert!(responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&penalty_txid)); // Breaches won't be overwritten once passed to the Responder. If the same UUID is // passed twice, the receipt corresponding to the first breach will be handed back. let another_breach = get_random_breach(); assert_eq!( - responder.handle_breach(uuid, another_breach.clone(), user_id), + responder.handle_breach(uuid, another_breach, user_id), ConfirmationStatus::InMempoolSince(start_height) ); - - assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); + // Getting the tracker should return the old one. assert_eq!( - responder.trackers.lock().unwrap()[&uuid].status, - ConfirmationStatus::InMempoolSince(start_height) + tracker, + responder.dbm.lock().unwrap().load_tracker(uuid).unwrap() ); - assert!(!responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&another_breach.penalty_tx.txid())); } #[tokio::test] @@ -848,39 +703,28 @@ mod tests { 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 (user_id, uuid) = responder.store_dummy_appointment_to_db(); 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)); + let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); assert_eq!( - responder.trackers.lock().unwrap()[&uuid].status, + tracker.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 (user_id, uuid) = responder.store_dummy_appointment_to_db(); let breach = get_random_breach(); - let penalty_txid = breach.penalty_tx.txid(); + let penalty_txid = breach.penalty_tx.compute_txid(); // Add the tx to our txindex let target_block_hash = *responder.tx_index.lock().unwrap().blocks().get(2).unwrap(); @@ -901,16 +745,11 @@ mod tests { responder.handle_breach(uuid, breach, user_id), ConfirmationStatus::ConfirmedIn(target_height) ); - assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); + let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); assert_eq!( - responder.trackers.lock().unwrap()[&uuid].status, + tracker.status, ConfirmationStatus::ConfirmedIn(target_height) ); - assert!(responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&penalty_txid)); } #[tokio::test] @@ -923,18 +762,12 @@ mod tests { let user_id = get_random_user_id(); let uuid = generate_uuid(); let breach = get_random_breach(); - let penalty_txid = breach.penalty_tx.txid(); assert_eq!( responder.handle_breach(uuid, breach, user_id), ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_ERROR) ); - assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); - assert!(!responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&penalty_txid)); + assert!(!responder.has_tracker(uuid)); } #[tokio::test] @@ -942,11 +775,7 @@ mod tests { let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let start_height = START_HEIGHT as u32; - // Add the necessary FKs in the database - 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 (user_id, uuid) = responder.store_dummy_appointment_to_db(); let mut breach = get_random_breach(); responder.add_tracker( uuid, @@ -955,21 +784,7 @@ mod tests { ConfirmationStatus::InMempoolSince(start_height), ); - // Check that the data has been added to trackers and to the tx_tracker_map - assert_eq!( - responder.trackers.lock().unwrap().get(&uuid), - Some(&TrackerSummary { - user_id, - penalty_txid: breach.penalty_tx.txid(), - status: ConfirmationStatus::InMempoolSince(start_height) - }) - ); - assert!(responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&breach.penalty_tx.txid())); - // Check that the data is also in the database + // Check that the data has been added to the responder. assert_eq!( responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), TransactionTracker::new( @@ -980,16 +795,9 @@ mod tests { ); // Adding a confirmed tracker should result in the same but with the height being set. - let uuid = generate_uuid(); + + let (user_id, uuid) = responder.store_dummy_appointment_to_db(); breach = get_random_breach(); - - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - responder.add_tracker( uuid, breach.clone(), @@ -997,23 +805,6 @@ mod tests { ConfirmationStatus::ConfirmedIn(start_height - 1), ); - assert_eq!( - responder.trackers.lock().unwrap().get(&uuid), - Some(&TrackerSummary { - user_id, - penalty_txid: breach.penalty_tx.txid(), - status: ConfirmationStatus::ConfirmedIn(start_height - 1) - }) - ); - assert!(responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&breach.penalty_tx.txid())); - assert_eq!( - responder.tx_tracker_map.lock().unwrap()[&breach.penalty_tx.txid()].len(), - 1 - ); assert_eq!( responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), TransactionTracker::new( @@ -1023,15 +814,8 @@ mod tests { ) ); - // Adding another breach with the same penalty transaction (but different uuid) adds an additional uuid to the map entry - let uuid = generate_uuid(); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - + // Adding another breach with the same penalty transaction (but different uuid) + let (user_id, uuid) = responder.store_dummy_appointment_to_db(); responder.add_tracker( uuid, breach.clone(), @@ -1039,16 +823,6 @@ mod tests { ConfirmationStatus::ConfirmedIn(start_height), ); - assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); - assert!(responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&breach.penalty_tx.txid())); - assert_eq!( - responder.tx_tracker_map.lock().unwrap()[&breach.penalty_tx.txid()].len(), - 2 - ); assert_eq!( responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), TransactionTracker::new( @@ -1067,10 +841,7 @@ mod tests { let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Add a new tracker - 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 (user_id, uuid) = responder.store_dummy_appointment_to_db(); let breach = get_random_breach(); responder.add_tracker( uuid, @@ -1081,12 +852,8 @@ mod tests { assert!(responder.has_tracker(uuid)); - // Delete the tracker and check again (updated users are irrelevant here) - responder.delete_trackers( - &HashSet::from_iter([uuid]), - &HashMap::new(), - DeletionReason::Completed, - ); + // Delete the tracker and check again. + responder.gatekeeper.delete_appointments(vec![uuid], false); assert!(!responder.has_tracker(uuid)); } @@ -1097,12 +864,10 @@ mod tests { 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(); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - store_appointment_and_fks_to_db(&responder.dbm.lock().unwrap(), uuid, &appointment); + let (user_id, uuid) = responder.store_dummy_appointment_to_db(); // Data should not be there before adding it - assert_eq!(responder.get_tracker(uuid), None); + assert!(responder.dbm.lock().unwrap().load_tracker(uuid).is_none()); // Data should be there now let breach = get_random_breach(); @@ -1113,7 +878,7 @@ mod tests { ConfirmationStatus::InMempoolSince(start_height), ); assert_eq!( - responder.get_tracker(uuid).unwrap(), + responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), TransactionTracker::new( breach, user_id, @@ -1121,13 +886,9 @@ mod tests { ) ); - // After deleting the data it should be gone (updated users are irrelevant here) - responder.delete_trackers( - &HashSet::from_iter([uuid]), - &HashMap::new(), - DeletionReason::Outdated, - ); - assert_eq!(responder.get_tracker(uuid), None); + // After deleting the data it should be gone + responder.gatekeeper.delete_appointments(vec![uuid], false); + assert!(responder.dbm.lock().unwrap().load_tracker(uuid).is_none()); } #[tokio::test] @@ -1140,67 +901,69 @@ mod tests { let mut just_confirmed = HashSet::new(); let mut confirmed = HashSet::new(); let mut completed = HashSet::new(); - let mut txids = Vec::new(); + let mut txids = HashSet::new(); for i in 0..40 { - let user_id = get_random_user_id(); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + let (user_id, uuid) = responder.store_dummy_appointment_to_db(); let breach = get_random_breach(); - store_appointment_and_fks_to_db(&responder.dbm.lock().unwrap(), uuid, &appointment); - - if i % 4 == 0 { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(21), - ); - in_mempool.insert(uuid); - } else if i % 4 == 1 { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(i), - ); - just_confirmed.insert(uuid); - txids.push(breach.penalty_tx.txid()); - } else if i % 4 == 2 { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(42), - ); - confirmed.insert(uuid); - } else { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn( - target_height - constants::IRREVOCABLY_RESOLVED, - ), - ); - completed.insert(uuid); + match i % 4 { + 0 => { + responder.add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::InMempoolSince(21), + ); + in_mempool.insert(uuid); + } + 1 => { + responder.add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::InMempoolSince(i), + ); + just_confirmed.insert(uuid); + txids.insert(breach.penalty_tx.compute_txid()); + } + 2 => { + responder.add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn(42), + ); + confirmed.insert(uuid); + } + _ => { + responder.add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn( + target_height - constants::IRREVOCABLY_RESOLVED, + ), + ); + completed.insert(uuid); + } } } // The trackers that were completed should be returned assert_eq!( completed, - responder.check_confirmations(&txids, target_height) + HashSet::from_iter(responder.check_confirmations(txids, target_height).unwrap()) ); // The ones in mempool should still be there (at the same height) for uuid in in_mempool { assert_eq!( responder - .trackers + .dbm .lock() .unwrap() - .get(&uuid) + .load_tracker(uuid) .unwrap() .status, ConfirmationStatus::InMempoolSince(21) @@ -1211,10 +974,10 @@ mod tests { for uuid in just_confirmed { assert_eq!( responder - .trackers + .dbm .lock() .unwrap() - .get(&uuid) + .load_tracker(uuid) .unwrap() .status, ConfirmationStatus::ConfirmedIn(target_height) @@ -1225,10 +988,10 @@ mod tests { for uuid in confirmed { assert_eq!( responder - .trackers + .dbm .lock() .unwrap() - .get(&uuid) + .load_tracker(uuid) .unwrap() .status, ConfirmationStatus::ConfirmedIn(42) @@ -1237,490 +1000,165 @@ mod tests { } #[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(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); - - // Transactions are flagged to be rebroadcast when they've been in mempool for longer than CONFIRMATIONS_BEFORE_RETRY - let mut txs = HashMap::new(); - - for i in 0..CONFIRMATIONS_BEFORE_RETRY + 2 { - // Add the appointment to the db so FK rules are satisfied - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - // Create a breach and add it, setting all them as unconfirmed (at different heights) - let breach = get_random_breach(); - - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(current_height - i as u32), - ); - - if i >= CONFIRMATIONS_BEFORE_RETRY { - txs.insert(uuid, (breach.penalty_tx.clone(), None)); - } - } - - assert_eq!(responder.get_txs_to_rebroadcast(current_height), txs); - } - - #[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, _s) = init_responder(MockedServerQuery::Regular).await; - let current_height = 100; - - let user_id = get_random_user_id(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); - - // Transactions are flagged to be rebroadcast when they've been in mempool for longer than CONFIRMATIONS_BEFORE_RETRY - let mut txs = HashMap::new(); - - for i in 0..10 { - // Add the appointment to the db so FK rules are satisfied - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - // Create a breach and add it, setting half of them as reorged - let breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(current_height), - ); - - // 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 so will trigger an error in the dbm since reorged transactions are not stored in the db. - if i % 2 == 0 { - responder - .trackers - .lock() - .unwrap() - .get_mut(&uuid) - .unwrap() - .status = ConfirmationStatus::ReorgedOut; - // Here the dispute is also included - txs.insert( - uuid, - (breach.penalty_tx.clone(), Some(breach.dispute_tx.clone())), - ); - } - } - - // 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); - } - - #[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). - - // Mock data into the GK - let target_block_height = START_HEIGHT as u32; - let user_id = get_random_user_id(); - let uuids = (0..10) - .into_iter() - .map(|_| generate_uuid()) - .collect::>(); - responder - .gatekeeper - .add_outdated_user(user_id, target_block_height, Some(uuids.clone())); - - // Mock the data to the Responder. Add data to trackers (half of them unconfirmed) - let mut target_uuids = HashSet::new(); - for (i, uuid) in uuids.into_iter().enumerate() { - let tracker = if i % 2 == 0 { - target_uuids.insert(uuid); - get_random_tracker( - user_id, - ConfirmationStatus::InMempoolSince(target_block_height), - ) - } else { - get_random_tracker( - user_id, - ConfirmationStatus::ConfirmedIn(target_block_height), - ) - }; - - responder - .trackers - .lock() - .unwrap() - .insert(uuid, tracker.get_summary()); - } - - // Check the expected data is there - assert_eq!( - responder.get_outdated_trackers(target_block_height), - target_uuids - ); - } - - #[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, _s) = init_responder(MockedServerQuery::Regular).await; - let current_height = 100; - - // Add user to the database - let user_id = get_random_user_id(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); - - // Transactions are rebroadcast once they've been in mempool for CONFIRMATIONS_BEFORE_RETRY or they've been reorged out - let mut need_rebroadcast = HashSet::new(); - - for i in 0..10 { - // Generate appointment and also add it to the DB (FK checks) - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - let breach = get_random_breach(); - - let height = if i % 2 == 0 { - current_height + 1 - CONFIRMATIONS_BEFORE_RETRY as u32 - } else { - need_rebroadcast.insert(uuid); - current_height - CONFIRMATIONS_BEFORE_RETRY as u32 - }; - - responder.add_tracker( - uuid, - breach, - user_id, - ConfirmationStatus::InMempoolSince(height), - ); - - // Reorged txs need to be set manually - if i % 2 == 1 { - responder - .trackers - .lock() - .unwrap() - .get_mut(&uuid) - .unwrap() - .status = ConfirmationStatus::ReorgedOut; - } - } - - // Check all are accepted - let (accepted, rejected) = - responder.rebroadcast(responder.get_txs_to_rebroadcast(current_height)); - let accepted_uuids: HashSet = accepted.keys().cloned().collect(); - assert_eq!(accepted_uuids, need_rebroadcast); - assert!(rejected.is_empty()); - } - - #[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, _s) = init_responder(MockedServerQuery::Error( - rpc_errors::RPC_VERIFY_ERROR as i64, - )) - .await; - let current_height = 100; - - // Add user to the database - let user_id = get_random_user_id(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); - - // Transactions are rebroadcast once they've been in mempool for CONFIRMATIONS_BEFORE_RETRY or they've been reorged out - let mut need_rebroadcast = HashSet::new(); - - for i in 0..30 { - // Generate appointment and also add it to the DB (FK checks) - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - let breach = get_random_breach(); - - let height = if i % 2 == 0 { - current_height + 1 - CONFIRMATIONS_BEFORE_RETRY as u32 - } else { - need_rebroadcast.insert(uuid); - current_height - CONFIRMATIONS_BEFORE_RETRY as u32 - }; - - responder.add_tracker( - uuid, - breach, - user_id, - ConfirmationStatus::InMempoolSince(height), - ); - - // Reorged txs need to be set manually - if i % 2 == 1 { - responder - .trackers - .lock() - .unwrap() - .get_mut(&uuid) - .unwrap() - .status = ConfirmationStatus::ReorgedOut; - } - } - - // Check all are rejected - let (accepted, rejected) = - responder.rebroadcast(responder.get_txs_to_rebroadcast(current_height)); - assert_eq!(rejected, need_rebroadcast); - assert!(accepted.is_empty()); - } - - #[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(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); - - // Add some trackers both to memory and to the database - let mut to_be_deleted = HashMap::new(); + async fn test_handle_reorged_txs() { + let (responder, _s) = init_responder(MockedServerQuery::InMempoool).await; + let mut trackers = Vec::new(); for _ in 0..10 { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - let breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(21), - ); - to_be_deleted.insert(uuid, breach.penalty_tx.txid()); + let uuid = responder + .add_random_tracker(ConfirmationStatus::ConfirmedIn(42)) + .uuid(); + responder.reorged_trackers.lock().unwrap().insert(uuid); + trackers.push(uuid); } - // Delete and check data is not in memory (the reason does not matter for the test) - responder.delete_trackers_from_memory( - &to_be_deleted.keys().cloned().collect(), - DeletionReason::Completed, - ); + let height = 100; + assert!(responder.handle_reorged_txs(height).is_none()); + // The reorged trackers buffer should be empty after this. + assert!(responder.reorged_trackers.lock().unwrap().is_empty()); - for (uuid, txid) in to_be_deleted { - // Data is not in memory - assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); - assert!(!responder.tx_tracker_map.lock().unwrap().contains_key(&txid)); - - // But it can be found in the database - assert!(matches!( - responder.dbm.lock().unwrap().load_tracker(uuid), - Ok(TransactionTracker { .. }) - )); - } - } - - #[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(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); - - // Delete trackers removes data from the trackers, tx_tracker_map maps, the database. The deletion of the later is - // better check in test_filtered_block_connected. Add data to the map first. - let mut all_trackers = HashSet::new(); - let mut target_trackers = HashSet::new(); - let mut uuid_txid_map = HashMap::new(); - let mut txs_with_multiple_uuids = HashSet::new(); - let mut updated_users = HashMap::new(); - - for i in 0..10 { - // Generate appointment and also add it to the DB (FK checks) - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - let breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(42), - ); - - // Make it so some of the penalties have multiple associated trackers - if i % 3 == 0 { - let uuid2 = generate_uuid(); - responder - .tx_tracker_map - .lock() - .unwrap() - .get_mut(&breach.penalty_tx.txid()) - .unwrap() - .insert(uuid2); - txs_with_multiple_uuids.insert(breach.penalty_tx.txid()); - } - - all_trackers.insert(uuid); - uuid_txid_map.insert(uuid, breach.penalty_tx.txid()); - - // Add some trackers to be deleted - if i % 2 == 0 { - // Users will also be updated once the data is deleted. - // We can made up the numbers here just to check they are updated. - target_trackers.insert(uuid); - updated_users.insert( - appointment.user_id, - UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ), - ); - } - } - - responder.delete_trackers(&target_trackers, &updated_users, DeletionReason::Rejected); - - // Only trackers in the target_trackers map should have been removed from - // the Responder data structures. - for uuid in all_trackers { - if target_trackers.contains(&uuid) { - assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); - assert!(matches!( - responder.dbm.lock().unwrap().load_tracker(uuid), - Err(DBError::NotFound) - )); - let penalty_txid = &uuid_txid_map[&uuid]; - // If the penalty had more than one associated uuid, only one has been deleted - // (because that's how the test has been designed) - if txs_with_multiple_uuids.contains(penalty_txid) { - assert_eq!( - responder - .tx_tracker_map - .lock() - .unwrap() - .get(penalty_txid) - .unwrap() - .len(), - 1 - ); - } else { - // Otherwise the whole structure is removed, given it is now empty - assert!(!responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(penalty_txid)); - } - } else { - assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); - assert!(responder - .tx_tracker_map - .lock() - .unwrap() - .contains_key(&uuid_txid_map[&uuid])); - assert!(matches!( - responder.dbm.lock().unwrap().load_tracker(uuid), - Ok(TransactionTracker { .. }) - )); - } - } - - // The users that needed to be updated in the database have been (just checking the slot count) - for (id, info) in updated_users { + // And all the reorged trackers should have in mempool since `height` status. + for uuid in trackers { assert_eq!( responder .dbm .lock() .unwrap() - .load_user(id) + .load_tracker(uuid) .unwrap() - .available_slots, - info.available_slots - ) + .status, + ConfirmationStatus::InMempoolSince(height) + ); + } + } + + #[tokio::test] + async fn test_handle_reorged_txs_rejected() { + let (responder, _s) = init_responder(MockedServerQuery::Error( + rpc_errors::RPC_VERIFY_REJECTED as i64, + )) + .await; + let n_trackers = 10; + let mut trackers = HashSet::new(); + + for _ in 0..n_trackers { + let uuid = responder + .add_random_tracker(ConfirmationStatus::ConfirmedIn(42)) + .uuid(); + responder.reorged_trackers.lock().unwrap().insert(uuid); + trackers.insert(uuid); + } + + let height = 100; + let rejected = HashSet::from_iter(responder.handle_reorged_txs(height).unwrap()); + // All the trackers should be returned as rejected. + assert_eq!(trackers, rejected); + // The reorged trackers buffer should be empty after this. + assert!(responder.reorged_trackers.lock().unwrap().is_empty()); + + // And all the reorged trackers statuses should be untouched. + for uuid in trackers { + assert_eq!( + responder + .dbm + .lock() + .unwrap() + .load_tracker(uuid) + .unwrap() + .status, + ConfirmationStatus::ConfirmedIn(42) + ); + } + } + + #[tokio::test] + async fn test_rebroadcast_stale_txs_accepted() { + let (responder, _s) = init_responder(MockedServerQuery::InMempoool).await; + let mut statues = HashMap::new(); + let height = 100; + + for i in 0..height { + let status = if i % 4 == 0 { + ConfirmationStatus::ConfirmedIn(i) + } else { + ConfirmationStatus::InMempoolSince(i) + }; + + let uuid = responder.add_random_tracker(status).uuid(); + statues.insert(uuid, status); + } + + // There should be no rejected tx. + assert!(responder.rebroadcast_stale_txs(height).is_none()); + + for (uuid, former_status) in statues { + let status = responder + .dbm + .lock() + .unwrap() + .load_tracker(uuid) + .unwrap() + .status; + if let ConfirmationStatus::InMempoolSince(h) = former_status { + if height - h >= CONFIRMATIONS_BEFORE_RETRY as u32 { + // Transactions which stayed for more than `CONFIRMATIONS_BEFORE_RETRY` should have been rebroadcasted. + assert_eq!(status, ConfirmationStatus::InMempoolSince(height)); + } else { + // Others left untouched. + assert_eq!(status, former_status); + } + } else { + // Confirmed transactions left untouched as well. + assert_eq!(status, former_status); + } + } + } + + #[tokio::test] + async fn test_rebroadcast_stale_txs_rejected() { + let (responder, _s) = init_responder(MockedServerQuery::Error( + rpc_errors::RPC_VERIFY_ERROR as i64, + )) + .await; + let mut statues = HashMap::new(); + let height = 100; + + for i in 0..height { + let status = if i % 4 == 0 { + ConfirmationStatus::ConfirmedIn(i) + } else { + ConfirmationStatus::InMempoolSince(i) + }; + + let uuid = responder.add_random_tracker(status).uuid(); + statues.insert(uuid, status); + } + + // `rebroadcast_stale_txs` will broadcast txs which has been in mempool since `CONFIRMATIONS_BEFORE_RETRY` or more + // blocks. Since our backend rejects all the txs, all these broadcasted txs should be returned from this method (rejected). + let rejected = HashSet::from_iter(responder.rebroadcast_stale_txs(height).unwrap()); + let should_reject: HashSet<_> = statues + .iter() + .filter_map(|(&uuid, &status)| { + if let ConfirmationStatus::InMempoolSince(h) = status { + (height - h >= CONFIRMATIONS_BEFORE_RETRY as u32).then_some(uuid) + } else { + None + } + }) + .collect(); + assert_eq!(should_reject, rejected); + + for (uuid, former_status) in statues { + let status = responder + .dbm + .lock() + .unwrap() + .load_tracker(uuid) + .unwrap() + .status; + // All tracker statues shouldn't change since the submitted ones were all rejected. + assert_eq!(status, former_status); } } @@ -1732,35 +1170,40 @@ mod tests { 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 + // filtered_block_connected is used to keep track of the confirmation received (or missed) by the trackers the Responder // is keeping track of. // // If there are any trackers, the Responder will: // - Check if there is any tracker that has been completed - // - Check if there is any tracker that has been outdated // - Check if any tracker has been confirmed or add missing confirmations otherwise // - Rebroadcast all penalty transactions that need so - // - Delete completed and outdated data (including data in the GK) + // - Delete completed and invalid data (and update the data in the GK) // - Clear the Carrier issued_receipts cache + // + // We will also test that trackers for outdated users are removed by the GK. // Let's start by doing the data setup for each test (i.e. adding all the necessary data to the Responder and GK) let target_block_height = chain.get_block_count() + 1; let mut users = Vec::new(); - for _ in 2..23 { + for _ in 0..21 { let user_id = get_random_user_id(); - responder.gatekeeper.add_update_user(user_id).unwrap(); users.push(user_id); } - let mut completed_trackers = HashMap::new(); - // COMPLETED TRACKERS SETUP + let mut completed_trackers = Vec::new(); for i in 0..10 { - // Adding two trackers to each user + // Add these trackers to the first two users let user_id = users[i % 2]; - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + let dispute_tx = get_random_tx(); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + responder + .gatekeeper + .add_update_appointment(user_id, uuid, &appointment) + .unwrap(); responder .dbm .lock() @@ -1769,58 +1212,42 @@ mod tests { .unwrap(); // Trackers complete in the next block. - let breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn( - target_block_height - constants::IRREVOCABLY_RESOLVED, - ), + let breach = Breach::new(dispute_tx, get_random_tx()); + let status = ConfirmationStatus::ConfirmedIn( + target_block_height - constants::IRREVOCABLY_RESOLVED, ); - responder - .gatekeeper - .get_registered_users() - .lock() - .unwrap() - .get_mut(&user_id) - .unwrap() - .appointments - .insert(uuid, 1); - - completed_trackers.insert(uuid, (user_id, breach)); + responder.add_tracker(uuid, breach.clone(), user_id, status); + completed_trackers.push(TransactionTracker::new(breach, user_id, status)); } // OUTDATED TRACKER SETUP - let mut penalties = Vec::new(); - let mut uuids = Vec::new(); - - for user_id in users.iter().take(21).skip(11) { - let pair = [generate_uuid(), generate_uuid()].to_vec(); - - for uuid in pair.iter() { - let (_, appointment) = generate_dummy_appointment_with_user(*user_id, None); + let mut outdated_trackers = Vec::new(); + for &user_id in users.iter().take(21).skip(11) { + for _ in 0..3 { + let dispute_tx = get_random_tx(); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + responder + .gatekeeper + .add_update_appointment(user_id, uuid, &appointment) + .unwrap(); responder .dbm .lock() .unwrap() - .store_appointment(*uuid, &appointment) + .store_appointment(uuid, &appointment) .unwrap(); - let breach = get_random_breach(); - penalties.push(breach.penalty_tx.txid()); - responder.add_tracker( - *uuid, - breach, - *user_id, - ConfirmationStatus::InMempoolSince(target_block_height - 1), - ); + let breach = Breach::new(dispute_tx, get_random_tx()); + let status = ConfirmationStatus::InMempoolSince(target_block_height - 1); + responder.add_tracker(uuid, breach.clone(), user_id, status); + outdated_trackers.push(TransactionTracker::new(breach, user_id, status)); } - uuids.extend(pair.clone()); + // Outdate this user so their trackers are deleted responder .gatekeeper - .add_outdated_user(*user_id, target_block_height, Some(pair)); + .add_outdated_user(user_id, target_block_height); } // CONFIRMATIONS SETUP @@ -1830,11 +1257,18 @@ mod tests { .add_update_user(standalone_user_id) .unwrap(); - let mut transactions = Vec::new(); - let mut just_confirmed_txs = Vec::new(); + let mut missed_confirmation_trackers = Vec::new(); + let mut just_confirmed_trackers = Vec::new(); for i in 0..10 { - let (uuid, appointment) = - generate_dummy_appointment_with_user(standalone_user_id, None); + let dispute_tx = get_random_tx(); + let (uuid, appointment) = generate_dummy_appointment_with_user( + standalone_user_id, + Some(&dispute_tx.compute_txid()), + ); + responder + .gatekeeper + .add_update_appointment(standalone_user_id, uuid, &appointment) + .unwrap(); responder .dbm .lock() @@ -1842,39 +1276,55 @@ mod tests { .store_appointment(uuid, &appointment) .unwrap(); - let breach = get_random_breach(); - transactions.push(breach.clone().penalty_tx.txid()); + let breach = Breach::new(dispute_tx, get_random_tx()); + let status = ConfirmationStatus::InMempoolSince(target_block_height - 1); + responder.add_tracker(uuid, breach.clone(), standalone_user_id, status); if i % 2 == 0 { - just_confirmed_txs.push(breach.clone().penalty_tx); + just_confirmed_trackers.push(TransactionTracker::new( + breach, + standalone_user_id, + status, + )); + } else { + missed_confirmation_trackers.push(TransactionTracker::new( + breach, + standalone_user_id, + status, + )); } - responder.add_tracker( - uuid, - breach, - standalone_user_id, - ConfirmationStatus::InMempoolSince(target_block_height - 1), - ); } // REBROADCAST SETUP - let (uuid, appointment) = generate_dummy_appointment_with_user(standalone_user_id, None); + let mut trackers_to_rebroadcast = Vec::new(); + for _ in 0..5 { + let dispute_tx = get_random_tx(); + let (uuid, appointment) = generate_dummy_appointment_with_user( + standalone_user_id, + Some(&dispute_tx.compute_txid()), + ); + responder + .gatekeeper + .add_update_appointment(standalone_user_id, uuid, &appointment) + .unwrap(); + responder + .dbm + .lock() + .unwrap() + .store_appointment(uuid, &appointment) + .unwrap(); - responder - .dbm - .lock() - .unwrap() - .store_appointment(uuid, &appointment) - .unwrap(); - - let tracker_to_rebroadcast = uuid; - responder.add_tracker( - uuid, - get_random_breach(), - standalone_user_id, - ConfirmationStatus::InMempoolSince( + let breach = Breach::new(dispute_tx, get_random_tx()); + let status = ConfirmationStatus::InMempoolSince( target_block_height - CONFIRMATIONS_BEFORE_RETRY as u32, - ), - ); + ); + responder.add_tracker(uuid, breach.clone(), standalone_user_id, status); + trackers_to_rebroadcast.push(TransactionTracker::new( + breach, + standalone_user_id, + status, + )); + } // CARRIER CACHE SETUP // Add some dummy data in the cache to check that it gets cleared @@ -1883,13 +1333,22 @@ mod tests { .lock() .unwrap() .get_issued_receipts() - .insert(get_random_tx().txid(), ConfirmationStatus::ConfirmedIn(21)); + .insert( + get_random_tx().compute_txid(), + ConfirmationStatus::ConfirmedIn(21), + ); // Connecting a block should trigger all the state transitions - responder.block_connected( - &chain.generate(Some(just_confirmed_txs.clone())), - chain.get_block_count(), - ); + let block = chain.generate(Some( + just_confirmed_trackers + .iter() + .map(|t| t.penalty_tx.clone()) + .collect(), + )); + let height = chain.get_block_count(); + // We connect the gatekeeper first so it deletes the outdated users. + responder.gatekeeper.block_connected(&block, height); + responder.block_connected(&block, height); // CARRIER CHECKS assert!(responder @@ -1907,66 +1366,72 @@ mod tests { // COMPLETED TRACKERS CHECKS // Data should have been removed - for (uuid, (user_id, breach)) in completed_trackers { - assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); - assert!(!responder - .tx_tracker_map + for tracker in completed_trackers { + assert!(responder + .dbm .lock() .unwrap() - .contains_key(&breach.penalty_tx.txid())); - assert!( - !responder.gatekeeper.get_registered_users().lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid) - ); + .load_tracker(tracker.uuid()) + .is_none()); + let (_, user_locators) = responder.gatekeeper.get_user_info(tracker.user_id).unwrap(); + assert!(!user_locators.contains(&tracker.locator())); } // OUTDATED TRACKERS CHECKS - // Data should have been removed - for uuid in uuids { - assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); - } - for txid in penalties { - assert!(!responder.tx_tracker_map.lock().unwrap().contains_key(&txid)); + // Data should have been removed (tracker not found nor the user) + for tracker in outdated_trackers { + assert!(responder + .dbm + .lock() + .unwrap() + .load_tracker(tracker.uuid()) + .is_none()); + assert!(responder + .gatekeeper + .get_user_info(tracker.user_id) + .is_none()); } // CONFIRMATIONS CHECKS // The transaction confirmation count / confirmation missed should have been updated - let tx_tracker_map = responder.tx_tracker_map.lock().unwrap(); - for txid in transactions { - let uuids = tx_tracker_map.get(&txid).unwrap(); - if just_confirmed_txs - .iter() - .map(|tx| tx.txid()) - .any(|x| x == txid) - { - for uuid in uuids.iter() { - assert_eq!( - responder.trackers.lock().unwrap()[uuid].status, - ConfirmationStatus::ConfirmedIn(target_block_height) - ); - } - } else { - for uuid in uuids.iter() { - assert_eq!( - responder.trackers.lock().unwrap()[uuid].status, - ConfirmationStatus::InMempoolSince(target_block_height - 1) - ); - } - } + for tracker in just_confirmed_trackers { + assert_eq!( + responder + .dbm + .lock() + .unwrap() + .load_tracker(tracker.uuid()) + .unwrap() + .status, + ConfirmationStatus::ConfirmedIn(target_block_height) + ); + } + for tracker in missed_confirmation_trackers { + assert_eq!( + responder + .dbm + .lock() + .unwrap() + .load_tracker(tracker.uuid()) + .unwrap() + .status, + ConfirmationStatus::InMempoolSince(target_block_height - 1) + ); } // REBROADCAST CHECKS - assert_eq!( - responder - .trackers - .lock() - .unwrap() - .get(&tracker_to_rebroadcast) - .unwrap() - .status, - ConfirmationStatus::InMempoolSince(target_block_height), - ); + for tracker in trackers_to_rebroadcast { + assert_eq!( + responder + .dbm + .lock() + .unwrap() + .load_tracker(tracker.uuid()) + .unwrap() + .status, + ConfirmationStatus::InMempoolSince(target_block_height), + ); + } } #[tokio::test] @@ -1978,21 +1443,16 @@ mod tests { // Add user to the database let user_id = get_random_user_id(); - responder - .dbm - .lock() - .unwrap() - .store_user( - user_id, - &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), - ) - .unwrap(); + responder.gatekeeper.add_update_user(user_id).unwrap(); let mut reorged = Vec::new(); + let block_range = START_HEIGHT - 10..START_HEIGHT; - for i in 0..10 { - // Generate appointment and also add it to the DB (FK checks) - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + for i in block_range.clone() { + // Generate appointment and also add it to the DB + let dispute_tx = get_random_tx(); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); responder .dbm .lock() @@ -2000,49 +1460,33 @@ mod tests { .store_appointment(uuid, &appointment) .unwrap(); - let breach = get_random_breach(); - + let breach = Breach::new(dispute_tx, get_random_tx()); responder.add_tracker( uuid, - breach.clone(), + breach, user_id, - ConfirmationStatus::ConfirmedIn(i), + ConfirmationStatus::ConfirmedIn(i as u32), ); reorged.push(uuid); } // Check that trackers are flagged as reorged if the height they were included at gets disconnected - for i in (0..10).rev() { + for (i, uuid) in block_range.clone().zip(reorged.iter()).rev() { // The header doesn't really matter, just the height - responder.block_disconnected(&chain.tip().header, i); + responder.block_disconnected(&chain.tip().header, i as u32); // Check that the proper tracker gets reorged at the proper height - assert_eq!( - responder - .trackers - .lock() - .unwrap() - .get(reorged.get(i as usize).unwrap()) - .unwrap() - .status, - ConfirmationStatus::ReorgedOut - ); - + assert!(responder.reorged_trackers.lock().unwrap().contains(uuid)); // Check that the carrier block_height has been updated - assert_eq!(responder.carrier.lock().unwrap().get_height(), i); + assert_eq!(responder.carrier.lock().unwrap().get_height(), i as u32); } // Check that all reorged trackers are still reorged - for uuid in reorged { - assert_eq!( - responder - .trackers - .lock() - .unwrap() - .get(&uuid) - .unwrap() - .status, - ConfirmationStatus::ReorgedOut - ); + for uuid in reorged.iter() { + assert!(responder.reorged_trackers.lock().unwrap().contains(uuid)); } + + // But should be clear after the first block connection + responder.block_connected(&chain.generate(None), block_range.start as u32); + assert!(responder.reorged_trackers.lock().unwrap().is_empty()); } } diff --git a/teos/src/test_utils.rs b/teos/src/test_utils.rs index b7959fd..16f9675 100644 --- a/teos/src/test_utils.rs +++ b/teos/src/test_utils.rs @@ -8,6 +8,7 @@ */ use rand::Rng; +use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; use std::thread; @@ -17,22 +18,24 @@ use jsonrpc_http_server::{CloseHandle, Server, ServerBuilder}; use bitcoincore_rpc::{Auth, Client as BitcoindClient}; -use bitcoin::blockdata::block::{Block, BlockHeader}; +use bitcoin::block::Block; use bitcoin::blockdata::constants::genesis_block; -use bitcoin::blockdata::script::{Builder, Script}; +use bitcoin::blockdata::script::{Builder, ScriptBuf}; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxIn, TxOut}; use bitcoin::hash_types::BlockHash; use bitcoin::hash_types::Txid; use bitcoin::hashes::Hash; -use bitcoin::network::constants::Network; -use bitcoin::util::hash::bitcoin_merkle_root; -use bitcoin::util::uint::Uint256; +use bitcoin::merkle_tree::calculate_root; +use bitcoin::pow::Work; +use bitcoin::Amount; +use bitcoin::Network; use bitcoin::Witness; use lightning_block_sync::poll::{ ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader, }; use lightning_block_sync::{ - AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache, + AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError, + UnboundedCache, }; use teos_common::constants::IRREVOCABLY_RESOLVED; @@ -153,8 +156,11 @@ impl Blockchain { fn at_height_unvalidated(&self, height: usize) -> BlockHeaderData { assert!(!self.blocks.is_empty()); assert!(height < self.blocks.len()); + let height_bytes = height.to_be_bytes(); + let mut padded_bytes = [0u8; 32]; + padded_bytes[32 - height_bytes.len()..].copy_from_slice(&height_bytes); BlockHeaderData { - chainwork: self.blocks[0].header.work() + Uint256::from_u64(height as u64).unwrap(), + chainwork: self.blocks[0].header.work() + Work::from_be_bytes(padded_bytes), height: height as u32, header: self.blocks[height].header, } @@ -184,7 +190,7 @@ impl Blockchain { } pub fn generate(&mut self, txs: Option>) -> Block { - let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32])); + let bits = bitcoin::Target::from_be_bytes([0xff; 32]).to_compact_lossy(); let prev_block = self.blocks.last().unwrap(); let prev_blockhash = prev_block.block_hash(); @@ -199,17 +205,17 @@ impl Blockchain { } None => vec![get_random_tx()], }; - let hashes = txdata.iter().map(|obj| obj.txid().as_hash()); - let mut header = BlockHeader { - version: 0, + let hashes = txdata.iter().map(|tx| tx.compute_txid().to_raw_hash()); + let mut header = bitcoin::block::Header { + version: bitcoin::block::Version::from_consensus(0), prev_blockhash, - merkle_root: bitcoin_merkle_root(hashes).unwrap().into(), + merkle_root: calculate_root(hashes).unwrap().into(), time, bits, nonce: 0, }; - while header.validate_pow(&header.target()).is_err() { + while header.validate_pow(header.target()).is_err() { header.nonce += 1; } @@ -245,7 +251,7 @@ impl BlockSource for Blockchain { }) } - fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> { + fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult { Box::pin(async move { for (height, block) in self.blocks.iter().enumerate() { if block.header.block_hash() == *header_hash { @@ -254,8 +260,7 @@ impl BlockSource for Blockchain { return Err(BlockSourceError::persistent("block not found")); } } - - return Ok(block.clone()); + return Ok(BlockData::FullBlock(block.clone())); } } Err(BlockSourceError::transient("block not found")) @@ -289,20 +294,20 @@ pub(crate) fn get_random_tx() -> Transaction { let prev_txid_bytes = get_random_bytes(32); Transaction { - version: 2, - lock_time: 0, + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::locktime::absolute::LockTime::from_height(0).unwrap(), input: vec![TxIn { previous_output: OutPoint::new( Txid::from_slice(&prev_txid_bytes).unwrap(), rng.gen_range(0..200), ), - script_sig: Script::new(), + script_sig: ScriptBuf::new(), witness: Witness::new(), - sequence: 0, + sequence: bitcoin::Sequence(0), }], output: vec![TxOut { script_pubkey: Builder::new().push_int(1).into_script(), - value: rng.gen_range(0..21000000000), + value: Amount::from_sat(rng.gen_range(0..21_000_000_000)), }], } } @@ -323,7 +328,7 @@ pub(crate) fn generate_dummy_appointment_with_user( let mut app = generate_dummy_appointment(dispute_txid); app.user_id = user_id; - (UUID::new(app.locator(), user_id), app) + (app.uuid(), app) } pub(crate) fn get_random_breach() -> Breach { @@ -341,17 +346,15 @@ pub(crate) fn get_random_tracker( TransactionTracker::new(breach, user_id, status) } -pub(crate) fn store_appointment_and_fks_to_db( - dbm: &DBM, - uuid: UUID, - appointment: &ExtendedAppointment, -) { +pub(crate) fn store_appointment_and_its_user(dbm: &DBM, appointment: &ExtendedAppointment) { dbm.store_user( appointment.user_id, &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), ) - .unwrap(); - dbm.store_appointment(uuid, appointment).unwrap(); + // It's ok if the user is already stored. + .ok(); + dbm.store_appointment(appointment.uuid(), appointment) + .unwrap(); } pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec { @@ -369,6 +372,17 @@ pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec Vec { + last_n_blocks.iter().map(get_full_block).collect() +} + +pub(crate) fn get_full_block(block: &ValidatedBlock) -> Block { + match block.deref() { + BlockData::FullBlock(b) => b.clone(), + _ => panic!("Expected FullBlock"), + } +} + pub(crate) enum MockedServerQuery { Regular, InMempoool, @@ -407,7 +421,7 @@ pub(crate) async fn create_responder( let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, height); - Responder::new(&last_n_blocks, height, carrier, gatekeeper, dbm) + Responder::new(last_n_blocks.as_slice(), height, carrier, gatekeeper, dbm) } pub(crate) async fn create_watcher( @@ -426,7 +440,7 @@ pub(crate) async fn create_watcher( Watcher::new( gatekeeper, responder, - &last_n_blocks, + last_n_blocks.as_slice(), chain.get_block_count(), tower_sk, tower_id, diff --git a/teos/src/tls.rs b/teos/src/tls.rs index d31783e..33e3354 100644 --- a/teos/src/tls.rs +++ b/teos/src/tls.rs @@ -5,7 +5,7 @@ * https://github.com/ElementsProject/lightning/blob/master/LICENSE */ -use rcgen::{Certificate, KeyPair, RcgenError}; +use rcgen::{Certificate, Error as RcgenError, KeyPair}; use std::convert::TryFrom; use std::path::Path; @@ -30,21 +30,20 @@ impl From for GenCertificateFailure { /// Just a wrapper around a certificate and an associated keypair. #[derive(Clone, Debug)] -pub struct Identity { +struct Identity { pub key: Vec, pub certificate: Vec, } -impl TryFrom<&Identity> for Certificate { +impl TryFrom<&Identity> for (Certificate, KeyPair) { type Error = RcgenError; - fn try_from(id: &Identity) -> Result { - let keystr = String::from_utf8_lossy(&id.key); - let key = KeyPair::from_pem(&keystr)?; - let certstr = String::from_utf8_lossy(&id.certificate); - let params = rcgen::CertificateParams::from_ca_cert_pem(&certstr, key)?; - let cert = Certificate::from_params(params)?; - Ok(cert) + fn try_from(id: &Identity) -> Result<(Certificate, KeyPair), RcgenError> { + let key = KeyPair::from_pem(&String::from_utf8_lossy(&id.key))?; + let params = + rcgen::CertificateParams::from_ca_cert_pem(&String::from_utf8_lossy(&id.certificate))?; + let cert = params.self_signed(&key)?; + Ok((cert, key)) } } @@ -72,30 +71,30 @@ fn generate_or_load_identity( // Did we have to generate a new key? In that case we also need to regenerate the certificate. if !key_path.exists() || !cert_path.exists() { log::debug!("Generating a new keypair in {key_path:?}, it didn't exist",); - let keypair = KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?; + let keypair = KeyPair::generate()?; std::fs::write(&key_path, keypair.serialize_pem())?; log::debug!("Generating a new certificate for key {key_path:?} at {cert_path:?}",); // Configure the certificate we want. - let subject_alt_names = vec!["cln".to_string(), "localhost".to_string()]; - let mut params = rcgen::CertificateParams::new(subject_alt_names); - params.key_pair = Some(keypair); - params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; + let subject_alt_names = vec!["teos".to_string(), "localhost".to_string()]; + let mut params = rcgen::CertificateParams::new(subject_alt_names)?; if parent.is_none() { params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); } else { - params.is_ca = rcgen::IsCa::SelfSignedOnly; + params.is_ca = rcgen::IsCa::NoCa; } params .distinguished_name .push(rcgen::DnType::CommonName, name); - let cert = Certificate::from_params(params)?; std::fs::write( &cert_path, match parent { - None => cert.serialize_pem()?, - Some(ca) => cert.serialize_pem_with_signer(&Certificate::try_from(ca)?)?, + None => params.self_signed(&keypair)?.pem(), + Some(ca) => { + let (ca_cert, ca_key) = <(Certificate, KeyPair)>::try_from(ca)?; + params.signed_by(&keypair, &ca_cert, &ca_key)?.pem() + } }, )?; } diff --git a/teos/src/tx_index.rs b/teos/src/tx_index.rs index fd7c2b9..c0b4ee4 100644 --- a/teos/src/tx_index.rs +++ b/teos/src/tx_index.rs @@ -1,15 +1,17 @@ use std::collections::{HashMap, VecDeque}; use std::fmt; use std::hash::Hash; +use std::ops::Deref; +use bitcoin::block::Header; use bitcoin::hash_types::BlockHash; -use bitcoin::{BlockHeader, Transaction, Txid}; +use bitcoin::{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 { +pub trait Key: Hash + Eq { fn from_txid(txid: Txid) -> Self; } @@ -79,8 +81,8 @@ impl Value for Transaction { /// 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 { +#[derive(Debug, PartialEq, Eq)] +pub struct TxIndex { /// A [K]:[V] map. index: HashMap, /// Vector of block hashes covered by the index. @@ -95,7 +97,7 @@ pub struct TxIndex { impl TxIndex where - K: Key + std::cmp::Eq + Copy, + K: Key + Copy, V: Value + Clone, Self: Sized, { @@ -110,29 +112,38 @@ where }; 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"); + match block.deref() { + lightning_block_sync::BlockData::HeaderOnly(_) => { + panic!("Expected FullBlock") } - }; + lightning_block_sync::BlockData::FullBlock(block) => { + 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(); + let map = block + .txdata + .iter() + .map(|tx| { + ( + K::from_txid(tx.compute_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.update(block.header, &map); + } + } } tx_index @@ -143,11 +154,6 @@ where 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 @@ -160,7 +166,7 @@ where } /// 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) { + pub fn update(&mut self, block_header: Header, data: &HashMap) { self.blocks.push_back(block_header.block_hash()); let ks = data @@ -175,7 +181,7 @@ where if self.is_full() { // Avoid logging during bootstrap - log::info!("New block added to index: {}", block_header.block_hash()); + log::debug!("New block added to index: {}", block_header.block_hash()); self.tip += 1; self.remove_oldest_block(); } @@ -204,11 +210,11 @@ where let ks = self.tx_in_block.remove(&h).unwrap(); self.index.retain(|k, _| !ks.contains(k)); - log::info!("Oldest block removed from index: {h}"); + log::debug!("Oldest block removed from index: {h}"); } } -impl fmt::Display for TxIndex { +impl fmt::Display for TxIndex { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -223,8 +229,9 @@ mod tests { use super::*; use std::ops::Deref; - use crate::test_utils::{get_last_n_blocks, Blockchain}; + use crate::test_utils::{get_full_block, get_full_blocks, get_last_n_blocks, Blockchain}; + use bitcoin::hashes::serde_macros::serde_details::SerdeHash; use bitcoin::Block; impl TxIndex @@ -240,6 +247,10 @@ mod tests { pub fn blocks(&self) -> &VecDeque { &self.blocks } + + pub fn contains_key(&self, k: &K) -> bool { + self.index.contains_key(k) + } } #[tokio::test] @@ -247,10 +258,7 @@ mod tests { 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 blocks: Vec = get_full_blocks(&last_six_blocks); let cache: TxIndex = TxIndex::new(&last_six_blocks, height); assert_eq!(blocks.len(), cache.size); @@ -259,7 +267,7 @@ mod tests { let mut locators = Vec::new(); for tx in block.txdata.iter() { - let locator = Locator::new(tx.txid()); + let locator = Locator::new(tx.compute_txid()); assert!(cache.contains_key(&locator)); locators.push(locator); } @@ -276,19 +284,22 @@ mod tests { 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 first_block = get_full_block(last_n_blocks.get(cache_size - 1).unwrap()); + let last_block = get_full_block(last_n_blocks.first().unwrap()); + let mid_block = get_full_block(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(), + cache.get_height(&first_block.header.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(), + cache.get_height(&last_block.header.block_hash()).unwrap(), + height + ); + assert_eq!( + cache.get_height(&mid_block.header.block_hash()).unwrap(), height - cache_size / 2 ); } @@ -303,8 +314,8 @@ mod tests { height as u32, ); - let fake_hash = BlockHash::default(); - assert!(matches!(cache.get_height(&fake_hash), None)); + let fake_hash = &BlockHash::from_slice_delegated(&[0; 32]).unwrap(); + assert!(cache.get_height(fake_hash).is_none()); } #[tokio::test] @@ -316,36 +327,42 @@ mod tests { // 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(); + let first_block = last_n_blocks.last().unwrap(); // 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 + let full_block = get_full_block(&last_block); + let locator_tx_map = full_block .txdata .iter() - .map(|tx| (Locator::new(tx.txid()), tx.clone())) + .map(|tx| (Locator::new(tx.compute_txid()), tx.clone())) .collect(); - cache.update(last_block.deref().header, &locator_tx_map); + let header = full_block.header; + cache.update(header, &locator_tx_map); // Check that the new data is in the cache - assert!(cache.blocks().contains(&last_block.block_hash())); + assert!(cache.blocks().contains(&header.block_hash())); + for (locator, _) in locator_tx_map.iter() { assert!(cache.contains_key(locator)); } + + let block_hash = full_block.header.block_hash(); assert_eq!( - cache.tx_in_block[&last_block.block_hash()], + cache.tx_in_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())); + let first_full_block = get_full_block(first_block); + let tx = first_full_block.txdata[0].clone(); + assert!(!cache.contains_key(&Locator::new(tx.compute_txid()))); + + let block_hash = first_full_block.header.block_hash(); + assert!(!cache.tx_in_block.contains_key(&block_hash)); } #[tokio::test] @@ -379,7 +396,7 @@ mod tests { // 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()); + assert!(!cache.tx_in_block.contains_key(&header.block_hash())); for locator in locators.iter() { assert!(!cache.contains_key(locator)); } diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index b94d6b2..caf7766 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -1,15 +1,12 @@ //! Logic related to the Watcher, the components in charge of watching for breaches on chain. -use log; - -use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; -use std::iter::FromIterator; +use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; +use bitcoin::block::Header; use bitcoin::secp256k1::SecretKey; -use bitcoin::{BlockHeader, Transaction}; +use bitcoin::Transaction; use lightning::chain; use lightning_block_sync::poll::ValidatedBlock; @@ -19,7 +16,7 @@ use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; use teos_common::{TowerId, UserId}; use crate::dbm::DBM; -use crate::extended_appointment::{AppointmentSummary, ExtendedAppointment, UUID}; +use crate::extended_appointment::{ExtendedAppointment, UUID}; use crate::gatekeeper::{Gatekeeper, MaxSlotsReached, UserInfo}; use crate::responder::{ConfirmationStatus, Responder, TransactionTracker}; use crate::tx_index::TxIndex; @@ -82,19 +79,11 @@ pub(crate) enum AppointmentInfo { Tracker(TransactionTracker), } -/// Reason why the appointment is deleted. Used for logging purposes. -enum DeletionReason { - Outdated, - Invalid, - Accepted, -} - /// Types of new appointments stored in the [Watcher]. #[derive(Debug, PartialEq, Eq)] enum StoredAppointment { New, Update, - Collision, } /// Types of new triggered appointments handled by the [Watcher]. @@ -108,10 +97,6 @@ enum TriggeredAppointment { /// Component in charge of watching for triggers in the chain (aka channel breaches for lightning). #[derive(Debug)] pub struct Watcher { - /// A map holding a summary of every appointment ([ExtendedAppointment]) hold by the [Watcher], identified by a [UUID]. - appointments: Mutex>, - /// 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>, /// A [Responder] instance. Data will be passed to it once triggered (if valid). @@ -139,21 +124,7 @@ impl Watcher { tower_id: TowerId, dbm: Arc>, ) -> Self { - let mut appointments = HashMap::new(); - let mut locator_uuid_map: HashMap> = HashMap::new(); - for (uuid, appointment) in dbm.lock().unwrap().load_appointments(None) { - appointments.insert(uuid, appointment.get_summary()); - - if let Some(map) = locator_uuid_map.get_mut(&appointment.locator()) { - map.insert(uuid); - } else { - locator_uuid_map.insert(appointment.locator(), HashSet::from_iter(vec![uuid])); - } - } - Watcher { - appointments: Mutex::new(appointments), - locator_uuid_map: Mutex::new(locator_uuid_map), locator_cache: Mutex::new(TxIndex::new(last_n_blocks, last_known_block_height)), responder, gatekeeper, @@ -166,7 +137,7 @@ impl Watcher { /// Returns whether the [Watcher] has been created from scratch (fresh) or from backed-up data. pub fn is_fresh(&self) -> bool { - self.appointments.lock().unwrap().is_empty() + self.get_appointments_count() == 0 } /// Registers a new user within the [Watcher]. This request is passed to the [Gatekeeper], who is in @@ -186,8 +157,7 @@ impl Watcher { /// - The user has enough available slots to fit the appointment /// - The appointment hasn't been responded to yet (data cannot be found in the [Responder]) /// - /// If an appointment is accepted, an [AppointmentSummary] will be added to the the watching pool and - /// monitored by the [Watcher]. An [ExtendedAppointment] (constructed from the [Appointment]) will be persisted on disk. + /// If an appointment is accepted, an [ExtendedAppointment] (constructed from the [Appointment]) will be persisted on disk. /// In case the locator for the given appointment can be found in the cache (meaning the appointment has been /// triggered recently) the data will be passed to the [Responder] straightaway (modulo it being valid). pub(crate) fn add_appointment( @@ -214,13 +184,15 @@ impl Watcher { self.last_known_block_height.load(Ordering::Acquire), ); - let uuid = UUID::new(extended_appointment.locator(), user_id); + let uuid = extended_appointment.uuid(); if self.responder.has_tracker(uuid) { log::info!("Tracker for {uuid} already found in Responder"); return Err(AddAppointmentFailure::AlreadyTriggered); } + // TODO: This is not atomic, we update the users slots and THEN add their appointment + // this means it can happen that we update the slots but some failure happens before we insert their appointment. let available_slots = self .gatekeeper .add_update_appointment(user_id, uuid, &extended_appointment) @@ -255,54 +227,23 @@ impl Watcher { Ok((receipt, available_slots, expiry)) } - /// Stores an appointment in the [Watcher] memory and into the database (or updates it if it already exists). - /// - /// Data is stored in `locator_uuid_map` and `appointments`. + /// Stores an appointment in the database (or updates it if it already exists). fn store_appointment( &self, uuid: UUID, appointment: &ExtendedAppointment, ) -> StoredAppointment { - self.appointments - .lock() - .unwrap() - .insert(uuid, appointment.get_summary()); - let mut locator_uuid_map = self.locator_uuid_map.lock().unwrap(); - if let Entry::Vacant(e) = locator_uuid_map.entry(appointment.locator()) { - // New appointment - e.insert(HashSet::from_iter(vec![uuid])); - - self.dbm - .lock() - .unwrap() - .store_appointment(uuid, appointment) - .unwrap(); - StoredAppointment::New + let dbm = self.dbm.lock().unwrap(); + if dbm.appointment_exists(uuid) { + log::debug!( + "User {} is updating appointment {uuid}", + appointment.user_id + ); + dbm.update_appointment(uuid, appointment).unwrap(); + StoredAppointment::Update } else { - // Either an update or an appointment from another user sharing the same locator - if locator_uuid_map - .get_mut(&appointment.locator()) - .unwrap() - .insert(uuid) - { - log::debug!( - "Adding an additional appointment to locator {}: {uuid}", - appointment.locator() - ); - self.dbm - .lock() - .unwrap() - .store_appointment(uuid, appointment) - .unwrap(); - StoredAppointment::Collision - } else { - log::debug!("Update received for {uuid}, locator map not modified"); - self.dbm - .lock() - .unwrap() - .update_appointment(uuid, appointment); - StoredAppointment::Update - } + dbm.store_appointment(uuid, appointment).unwrap(); + StoredAppointment::New } } @@ -321,7 +262,7 @@ impl Watcher { "Trigger for locator {} found in cache", appointment.locator() ); - match cryptography::decrypt(appointment.encrypted_blob(), &dispute_tx.txid()) { + match cryptography::decrypt(appointment.encrypted_blob(), &dispute_tx.compute_txid()) { Ok(penalty_tx) => { // Data needs to be added the database straightaway since appointments are // FKs to trackers. If handle breach fails, data will be deleted later. @@ -329,6 +270,9 @@ impl Watcher { .lock() .unwrap() .store_appointment(uuid, appointment) + // TODO: Don't unwrap, or better, make this insertion atomic with the + // `responder.has_tracker` that might cause the unwrap in the first place. + // ref: https://github.com/talaia-labs/rust-teos/pull/190#discussion_r1218235632 .unwrap(); if let ConfirmationStatus::Rejected(reason) = self.responder.handle_breach( @@ -336,11 +280,8 @@ impl Watcher { Breach::new(dispute_tx.clone(), penalty_tx), user_id, ) { - // DISCUSS: We could either free the slots or keep it occupied as if this was misbehavior. - // Keeping it for now. log::warn!("Appointment bounced in the Responder. Reason: {reason:?}"); - - self.dbm.lock().unwrap().remove_appointment(uuid); + self.gatekeeper.delete_appointments(vec![uuid], false); TriggeredAppointment::Rejected } else { log::info!("Appointment went straight to the Responder"); @@ -389,25 +330,17 @@ impl Watcher { } let uuid = UUID::new(locator, user_id); - - if self.appointments.lock().unwrap().contains_key(&uuid) { - Ok(AppointmentInfo::Appointment( - self.dbm - .lock() - .unwrap() - .load_appointment(uuid) - .unwrap() - .inner, - )) - } else { - self.responder - .get_tracker(uuid) - .map(AppointmentInfo::Tracker) - .ok_or_else(|| { - log::info!("Cannot find {locator}"); - GetAppointmentFailure::NotFound - }) - } + let dbm = self.dbm.lock().unwrap(); + dbm.load_tracker(uuid) + .map(AppointmentInfo::Tracker) + .or_else(|| { + dbm.load_appointment(uuid) + .map(|ext_app| AppointmentInfo::Appointment(ext_app.inner)) + }) + .ok_or_else(|| { + log::info!("Cannot find {locator}"); + GetAppointmentFailure::NotFound + }) } /// Gets a map of breaches provided a map between locators and transactions. @@ -419,20 +352,14 @@ impl Watcher { &self, locator_tx_map: HashMap, ) -> HashMap { - let monitored_locators: HashSet = self - .locator_uuid_map + let breaches: HashMap = self + .dbm .lock() .unwrap() - .keys() - .cloned() + .batch_check_locators_exist(locator_tx_map.keys().collect()) + .iter() + .map(|locator| (*locator, locator_tx_map[locator].clone())) .collect(); - let new_locators = locator_tx_map.keys().cloned().collect(); - let mut breaches = HashMap::new(); - - for locator in monitored_locators.intersection(&new_locators) { - let (k, v) = locator_tx_map.get_key_value(locator).unwrap(); - breaches.insert(*k, v.clone()); - } if breaches.is_empty() { log::info!("No breaches found") @@ -443,117 +370,41 @@ impl Watcher { breaches } - /// Filters a map of breaches between those that are valid and those that are not. + /// Responds to breaches. /// - /// Valid breaches are those resulting in a properly formatted [Transaction] once decrypted. - fn filter_breaches( - &self, - breaches: HashMap, - ) -> ( - HashMap, - HashMap, - ) { - let mut valid_breaches = HashMap::new(); - let mut invalid_breaches = HashMap::new(); + /// Decrypts triggered appointments using the dispute transaction ID and publishes them. + /// If the decryption fails for some appointments or if it succeeds but they get rejected when sent to the network, + /// they are marked as an invalid breaches and returned. + /// [None] is returned if none of these breaches are invalid. + fn handle_breaches(&self, breaches: HashMap) -> Option> { + let mut invalid_breaches = Vec::new(); - // A cache of the already decrypted blobs so replicate decryption can be avoided - let mut decrypted_blobs: HashMap, Transaction> = HashMap::new(); - - let locator_uuid_map = self.locator_uuid_map.lock().unwrap(); - let dbm = self.dbm.lock().unwrap(); for (locator, dispute_tx) in breaches.into_iter() { - for uuid in locator_uuid_map.get(&locator).unwrap() { - let appointment = dbm.load_appointment(*uuid).unwrap(); - match decrypted_blobs.get(appointment.encrypted_blob()) { - Some(penalty_tx) => { - valid_breaches - .insert(*uuid, Breach::new(dispute_tx.clone(), penalty_tx.clone())); - } - None => { - match cryptography::decrypt( - appointment.encrypted_blob(), - &dispute_tx.txid(), + // WARNING(deadlock): Don't lock `self.dbm` over the loop since `Responder::handle_breach` uses it as well. + let uuids = self.dbm.lock().unwrap().load_uuids(locator); + for uuid in uuids { + let appointment = self.dbm.lock().unwrap().load_appointment(uuid).unwrap(); + match cryptography::decrypt( + appointment.encrypted_blob(), + &dispute_tx.compute_txid(), + ) { + Ok(penalty_tx) => { + if let ConfirmationStatus::Rejected(_) = self.responder.handle_breach( + uuid, + Breach::new(dispute_tx.clone(), penalty_tx), + appointment.user_id, ) { - Ok(penalty_tx) => { - decrypted_blobs.insert( - appointment.encrypted_blob().clone(), - penalty_tx.clone(), - ); - valid_breaches - .insert(*uuid, Breach::new(dispute_tx.clone(), penalty_tx)); - } - Err(e) => { - invalid_breaches.insert(*uuid, e); - } + invalid_breaches.push(uuid); } } - } - } - } - - (valid_breaches, invalid_breaches) - } - - // DISCUSS:: For outdated data this may be nicer if implemented with a callback from the GK given that: - // - The GK is queried for the data to be deleted - // - Appointment and tracker data can be deleted in cascade when a user is deleted - // If done, the GK can notify the Watcher and Responder to delete data in memory and - // take care of the database itself. - - /// Deletes appointments from memory. - /// - /// The appointments are deleted from the appointments and locator_uuid_map maps. - /// Logs a different message depending on whether the appointments have been outdated, invalid, or accepted. - fn delete_appointments_from_memory(&self, uuids: &HashSet, reason: DeletionReason) { - let mut appointments = self.appointments.lock().unwrap(); - let mut locator_uuid_map = self.locator_uuid_map.lock().unwrap(); - - for uuid in uuids { - match reason { - DeletionReason::Outdated => { - log::info!("End time reached by {uuid} without breach. Deleting appointment") - } - DeletionReason::Invalid => log::info!( - "{uuid} cannot be completed, it contains invalid data. Deleting appointment" - ), - DeletionReason::Accepted => { - log::info!("{uuid} accepted by the Responder. Deleting appointment") - } - }; - match appointments.remove(uuid) { - Some(appointment) => { - let appointments = locator_uuid_map.get_mut(&appointment.locator).unwrap(); - - if appointments.len() == 1 { - locator_uuid_map.remove(&appointment.locator); - - log::info!("No more appointments for locator: {}", appointment.locator); - } else { - appointments.remove(uuid); + Err(_) => { + invalid_breaches.push(uuid); } } - None => { - // This should never happen. Logging just in case so we can fix it if so - log::error!("Appointment not found when cleaning: {uuid}"); - } } } - } - /// Deletes appointments from memory and the database. - fn delete_appointments( - &self, - uuids: &HashSet, - updated_users: &HashMap, - reason: DeletionReason, - ) { - if !uuids.is_empty() { - self.delete_appointments_from_memory(uuids, reason); - self.dbm - .lock() - .unwrap() - .batch_remove_appointments(uuids, updated_users); - } + (!invalid_breaches.is_empty()).then_some(invalid_breaches) } /// Ges the number of users currently registered with the tower. @@ -561,9 +412,9 @@ impl Watcher { self.gatekeeper.get_registered_users_count() } - /// Gets the total number of appointments stored in the [Watcher]. + /// Gets the total number of appointments excluding trackers. pub(crate) fn get_appointments_count(&self) -> usize { - self.appointments.lock().unwrap().len() + self.dbm.lock().unwrap().get_appointments_count() } /// Gets the total number of trackers in the [Responder]. @@ -603,7 +454,7 @@ impl Watcher { } /// Gets the data held by the tower about a given user. - pub(crate) fn get_user_info(&self, user_id: UserId) -> Option { + pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec)> { self.gatekeeper.get_user_info(user_id) } @@ -626,29 +477,7 @@ impl Watcher { return Err(GetSubscriptionInfoFailure::SubscriptionExpired(expiry)); } - let subscription_info = self.gatekeeper.get_user_info(user_id).unwrap(); - let mut locators = Vec::new(); - - let appointments = self.appointments.lock().unwrap(); - let dbm = self.dbm.lock().unwrap(); - for uuid in subscription_info.appointments.keys() { - match appointments.get(uuid) { - Some(a) => locators.push(a.locator), - None => { - if self.responder.has_tracker(*uuid) { - match dbm.load_locator(*uuid) { - Ok(locator) => locators.push(locator), - Err(_) => log::error!( - "Tracker found in Responder but not in DB (uuid = {uuid})" - ), - } - } else { - log::error!("Appointment found in the Gatekeeper but not in the Watcher nor the Responder (uuid = {uuid})") - } - } - } - } - + let (subscription_info, locators) = self.gatekeeper.get_user_info(user_id).unwrap(); Ok((subscription_info, locators)) } } @@ -668,7 +497,7 @@ impl chain::Listen for Watcher { /// told by the [Gatekeeper]. fn filtered_block_connected( &self, - header: &BlockHeader, + header: &Header, txdata: &chain::transaction::TransactionData, height: u32, ) { @@ -676,7 +505,7 @@ impl chain::Listen for Watcher { let locator_tx_map = txdata .iter() - .map(|(_, tx)| (Locator::new(tx.txid()), (*tx).clone())) + .map(|(_, tx)| (Locator::new(tx.compute_txid()), (*tx).clone())) .collect(); self.locator_cache @@ -684,54 +513,9 @@ impl chain::Listen for Watcher { .unwrap() .update(*header, &locator_tx_map); - if !self.appointments.lock().unwrap().is_empty() { - // Start by removing outdated data so it is not taken into account from this point on - self.delete_appointments_from_memory( - &self.gatekeeper.get_outdated_appointments(height), - DeletionReason::Outdated, - ); - - // Filter out those breaches that do not yield a valid transaction - let (valid_breaches, invalid_breaches) = - self.filter_breaches(self.get_breaches(locator_tx_map)); - - // Send data to the Responder - let mut appointments_to_delete = HashSet::from_iter(invalid_breaches.into_keys()); - let mut delivered_appointments = HashSet::new(); - for (uuid, breach) in valid_breaches { - log::info!("Notifying Responder and deleting appointment (uuid: {uuid})"); - - if let ConfirmationStatus::Rejected(_) = self.responder.handle_breach( - uuid, - breach, - self.appointments.lock().unwrap()[&uuid].user_id, - ) { - appointments_to_delete.insert(uuid); - } else { - delivered_appointments.insert(uuid); - } - } - - // Delete data - let appointments_to_delete_gatekeeper = { - let appointments = self.appointments.lock().unwrap(); - appointments_to_delete - .iter() - .map(|uuid| (*uuid, appointments[uuid].user_id)) - .collect() - }; - self.delete_appointments_from_memory(&delivered_appointments, DeletionReason::Accepted); - self.delete_appointments( - &appointments_to_delete, - &self - .gatekeeper - .delete_appointments_from_memory(&appointments_to_delete_gatekeeper), - DeletionReason::Invalid, - ); - - if self.appointments.lock().unwrap().is_empty() { - log::info!("No more pending appointments"); - } + // Get the breaches found in this block, handle them, and delete invalid ones. + if let Some(invalid_breaches) = self.handle_breaches(self.get_breaches(locator_tx_map)) { + self.gatekeeper.delete_appointments(invalid_breaches, false); } // Update last known block @@ -742,7 +526,7 @@ impl chain::Listen for Watcher { /// Handle reorgs in the [Watcher]. /// /// Fixes the [LocatorCache] by removing the disconnected data and updates the last_known_block_height. - fn block_disconnected(&self, header: &BlockHeader, height: u32) { + fn block_disconnected(&self, header: &Header, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); self.locator_cache .lock() @@ -756,6 +540,8 @@ impl chain::Listen for Watcher { #[cfg(test)] mod tests { use super::*; + use std::collections::HashSet; + use std::iter::FromIterator; use std::ops::Deref; use std::sync::{Arc, Mutex}; @@ -764,43 +550,35 @@ 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_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, + generate_dummy_appointment_with_user, get_random_tx, BitcoindMock, BitcoindStopper, + Blockchain, MockOptions, MockedServerQuery, DURATION, EXPIRY_DELTA, SLOTS, START_HEIGHT, }; - use teos_common::cryptography::{get_random_bytes, get_random_keypair}; - use teos_common::dbm::Error as DBError; + use teos_common::cryptography::get_random_keypair; - use bitcoin::hash_types::Txid; - use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use lightning::chain::Listen; impl PartialEq for Watcher { fn eq(&self, other: &Self) -> bool { - *self.appointments.lock().unwrap() == *other.appointments.lock().unwrap() - && *self.locator_uuid_map.lock().unwrap() == *other.locator_uuid_map.lock().unwrap() - && self.last_known_block_height.load(Ordering::Relaxed) - == other.last_known_block_height.load(Ordering::Relaxed) + // Same in-memory data. + self.last_known_block_height.load(Ordering::Relaxed) == other.last_known_block_height.load(Ordering::Relaxed) && + *self.locator_cache.lock().unwrap() == *other.locator_cache.lock().unwrap() && + // && Same DB data. + self.get_all_watcher_appointments() == other.get_all_watcher_appointments() } } impl Eq for Watcher {} impl Watcher { - pub(crate) fn add_dummy_tracker_to_responder( - &self, - uuid: UUID, - tracker: &TransactionTracker, - ) { - self.responder.add_dummy_tracker(uuid, tracker) + pub(crate) fn add_dummy_tracker_to_responder(&self, tracker: &TransactionTracker) { + self.responder.add_dummy_tracker(tracker) } - pub(crate) fn add_random_tracker_to_responder(&self, uuid: UUID) -> TransactionTracker { + pub(crate) fn add_random_tracker_to_responder(&self) -> TransactionTracker { // The confirmation status can be whatever here. Using the most common. self.responder - .add_random_tracker(uuid, ConfirmationStatus::ConfirmedIn(100)) + .add_random_tracker(ConfirmationStatus::ConfirmedIn(100)) } } @@ -861,12 +639,12 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); watcher.register(user_id).unwrap(); - let appointment = generate_dummy_appointment(None).inner; - // If we add some trackers to the system and create a new Responder reusing the same db + // If we add some appointments to the system and create a new Watcher reusing the same db // (as if simulating a bootstrap from existing data), the data should be properly loaded. for _ in 0..10 { - let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); + let appointment = generate_dummy_appointment(None).inner; + let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk); watcher .add_appointment(appointment.clone(), user_sig.clone()) .unwrap(); @@ -916,7 +694,6 @@ mod tests { // - if the appointment already exists for a given user, update the data // - if the appointment is already in the Responder, reject // - if the trigger for the appointment is in the cache, trigger straightaway - // - DISCUSS: if the appointment is accepted but bounces in the Responder, do not reduce the subscription count // In any of the cases where the appointment should be added to the Watcher, the appointment will be rejected if: // - the user does not have enough slots (either to add or update) // - the subscription has expired @@ -929,10 +706,10 @@ mod tests { let user_id = UserId(user_pk); watcher.register(user_id).unwrap(); let appointment = generate_dummy_appointment(None).inner; + let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk); // Add the appointment for a new user (twice so we can check that updates work) for _ in 0..2 { - let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(appointment.clone(), user_sig.clone()) .unwrap(); @@ -945,37 +722,31 @@ mod tests { let user2_id = UserId(user2_pk); watcher.register(user2_id).unwrap(); - let user2_sig = cryptography::sign(&appointment.to_vec(), &user2_sk).unwrap(); + let user2_sig = cryptography::sign(&appointment.to_vec(), &user2_sk); let (receipt, slots, expiry) = watcher .add_appointment(appointment.clone(), user2_sig.clone()) .unwrap(); assert_appointment_added(slots, SLOTS - 1, expiry, receipt, &user2_sig, tower_id); - // There should be now two appointments in the Watcher and the same locator should have two different uuids - assert_eq!(watcher.appointments.lock().unwrap().len(), 2); - assert_eq!( - watcher.locator_uuid_map.lock().unwrap()[&appointment.locator].len(), - 2 - ); - - // Check data was added to the database - for uuid in watcher.appointments.lock().unwrap().keys() { - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(*uuid), - Ok(ExtendedAppointment { .. }) - )); - } + // There should be now two appointments in the Watcher + assert_eq!(watcher.get_appointments_count(), 2); + assert_eq!(watcher.responder.get_trackers_count(), 0); // If an appointment is already in the Responder, it should bounce - let (uuid, triggered_appointment) = generate_dummy_appointment_with_user(user_id, None); - let signature = - cryptography::sign(&triggered_appointment.inner.to_vec(), &user_sk).unwrap(); - watcher + let dispute_tx = get_random_tx(); + let (uuid, triggered_appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + let signature = cryptography::sign(&triggered_appointment.inner.to_vec(), &user_sk); + let (receipt, slots, expiry) = watcher .add_appointment(triggered_appointment.inner.clone(), signature.clone()) .unwrap(); - let breach = get_random_breach(); + assert_appointment_added(slots, SLOTS - 2, expiry, receipt, &signature, tower_id); + assert_eq!(watcher.get_appointments_count(), 3); + assert_eq!(watcher.responder.get_trackers_count(), 0); + + let breach = Breach::new(dispute_tx, get_random_tx()); watcher.responder.add_tracker( uuid, breach, @@ -988,60 +759,42 @@ mod tests { receipt, Err(AddAppointmentFailure::AlreadyTriggered) )); + assert_eq!(watcher.get_appointments_count(), 2); + assert_eq!(watcher.responder.get_trackers_count(), 1); // If the trigger is already in the cache, the appointment will go straight to the Responder let dispute_tx = tip_txs.last().unwrap(); let (uuid, appointment_in_cache) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); - let user_sig = cryptography::sign(&appointment_in_cache.inner.to_vec(), &user_sk).unwrap(); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + let user_sig = cryptography::sign(&appointment_in_cache.inner.to_vec(), &user_sk); let (receipt, slots, expiry) = watcher - .add_appointment(appointment_in_cache.inner.clone(), user_sig.clone()) + .add_appointment(appointment_in_cache.inner, user_sig.clone()) .unwrap(); - // The appointment should have been accepted, slots should have been decreased, and data should have been deleted from - // the Watcher's memory. Moreover, a new tracker should be found in the Responder + // The appointment should have been accepted, slots should have been decreased, and a new tracker should be found in the Responder assert_appointment_added(slots, SLOTS - 3, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.appointments.lock().unwrap().len(), 3); - assert!(!watcher - .locator_uuid_map - .lock() - .unwrap() - .contains_key(&appointment_in_cache.locator())); + assert_eq!(watcher.get_appointments_count(), 2); + assert_eq!(watcher.responder.get_trackers_count(), 2); + // Data should be in the database assert!(watcher.responder.has_tracker(uuid)); - // Check data was added to the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Ok(ExtendedAppointment { .. }) - )); - assert!(matches!( - watcher.dbm.lock().unwrap().load_tracker(uuid), - Ok(TransactionTracker { .. }) - )); - // If an appointment is rejected by the Responder, it is considered misbehavior and the slot count is kept // Wrong penalty let dispute_tx = &tip_txs[tip_txs.len() - 2]; let (uuid, mut invalid_appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); invalid_appointment.inner.encrypted_blob.reverse(); - let user_sig = cryptography::sign(&invalid_appointment.inner.to_vec(), &user_sk).unwrap(); + let user_sig = cryptography::sign(&invalid_appointment.inner.to_vec(), &user_sk); let (receipt, slots, expiry) = watcher - .add_appointment(invalid_appointment.inner.clone(), user_sig.clone()) + .add_appointment(invalid_appointment.inner, user_sig.clone()) .unwrap(); assert_appointment_added(slots, SLOTS - 4, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.appointments.lock().unwrap().len(), 3); - + assert_eq!(watcher.get_appointments_count(), 2); + assert_eq!(watcher.responder.get_trackers_count(), 2); // Data should not be in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); - assert!(matches!( - watcher.dbm.lock().unwrap().load_tracker(uuid), - Err(DBError::NotFound) - )); + assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); // Transaction rejected // Update the Responder with a new Carrier @@ -1052,20 +805,19 @@ mod tests { *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; - let user_sig = cryptography::sign(&invalid_appointment.to_vec(), &user_sk).unwrap(); + let (uuid, invalid_appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + let user_sig = cryptography::sign(&invalid_appointment.inner.to_vec(), &user_sk); let (receipt, slots, expiry) = watcher - .add_appointment(invalid_appointment, user_sig.clone()) + .add_appointment(invalid_appointment.inner, user_sig.clone()) .unwrap(); - assert_appointment_added(slots, SLOTS - 4, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.appointments.lock().unwrap().len(), 3); - + assert_appointment_added(slots, SLOTS - 5, expiry, receipt, &user_sig, tower_id); + assert_eq!(watcher.get_appointments_count(), 2); + assert_eq!(watcher.responder.get_trackers_count(), 2); // Data should not be in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); + assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); // FAIL cases (non-registered, subscription expired and not enough slots) @@ -1074,14 +826,11 @@ mod tests { let user3_sig = String::from_utf8((0..65).collect()).unwrap(); assert!(matches!( - watcher.add_appointment(appointment.clone(), user3_sig), + watcher.add_appointment(appointment, user3_sig), Err(AddAppointmentFailure::AuthenticationFailure) )); // Data should not be in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); // If the user has no enough slots, the appointment is rejected. We do not test all possible cases since updates are // already tested int he Gatekeeper. Testing that it is rejected if the condition is met should suffice. @@ -1094,39 +843,30 @@ mod tests { .unwrap() .available_slots = 0; - let dispute_txid = Txid::from_slice(&get_random_bytes(32)).unwrap(); - let new_appointment = generate_dummy_appointment(Some(&dispute_txid)).inner; - let new_app_sig = cryptography::sign(&new_appointment.to_vec(), &user_sk).unwrap(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + let signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk); assert!(matches!( - watcher.add_appointment(new_appointment, new_app_sig), + watcher.add_appointment(appointment.inner, signature), Err(AddAppointmentFailure::NotEnoughSlots) )); // Data should not be in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); // If the user subscription has expired, the appointment should be rejected. watcher .gatekeeper - .get_registered_users() - .lock() - .unwrap() - .get_mut(&user2_id) - .unwrap() - .subscription_expiry = START_HEIGHT as u32; + .add_outdated_user(user2_id, START_HEIGHT as u32); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user2_id, None); + let signature = cryptography::sign(&appointment.inner.to_vec(), &user2_sk); assert!(matches!( - watcher.add_appointment(appointment, user2_sig), + watcher.add_appointment(appointment.inner, signature), Err(AddAppointmentFailure::SubscriptionExpired { .. }) )); // Data should not be in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); } #[tokio::test] @@ -1138,8 +878,10 @@ mod tests { let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); watcher.register(user_id).unwrap(); + let dispute_txid = get_random_tx().compute_txid(); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); // Storing a new appointment should return New assert_eq!( @@ -1147,46 +889,22 @@ mod tests { StoredAppointment::New, ); assert_eq!( - *watcher.appointments.lock().unwrap(), - HashMap::from_iter([(uuid, appointment.get_summary())]) - ); - assert_eq!( - *watcher.locator_uuid_map.lock().unwrap(), - HashMap::from_iter([(appointment.locator(), HashSet::from_iter([uuid]))]) + watcher.get_all_watcher_appointments(), + HashMap::from_iter([(uuid, appointment)]) ); // Adding an appointment with the same UUID should be seen as an updated - // The appointment data here does not matter much, just the UUID and the locator since they are tied to each other. + // We are using a common dispute txid here to get the same uuid. + let (new_uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + assert_eq!(new_uuid, uuid); assert_eq!( watcher.store_appointment(uuid, &appointment), StoredAppointment::Update, ); assert_eq!( - *watcher.appointments.lock().unwrap(), - HashMap::from_iter([(uuid, appointment.get_summary())]) - ); - assert_eq!( - *watcher.locator_uuid_map.lock().unwrap(), - HashMap::from_iter([(appointment.locator(), HashSet::from_iter([uuid]))]) - ); - - // Adding the same appointment (same locator) with a different UUID should be seen as a collision. - // This means that a different user is sending an appointment with the same locator. - let new_uuid = generate_uuid(); - assert_eq!( - watcher.store_appointment(new_uuid, &appointment), - StoredAppointment::Collision, - ); - assert_eq!( - *watcher.appointments.lock().unwrap(), - HashMap::from_iter([ - (uuid, appointment.get_summary()), - (new_uuid, appointment.get_summary()) - ]) - ); - assert_eq!( - *watcher.locator_uuid_map.lock().unwrap(), - HashMap::from_iter([(appointment.locator(), HashSet::from_iter([uuid, new_uuid]))]) + watcher.get_all_watcher_appointments(), + HashMap::from_iter([(uuid, appointment)]) ); } @@ -1202,7 +920,7 @@ mod tests { let dispute_tx = get_random_tx(); let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); // Valid triggered appointments should be accepted by the Responder assert_eq!( @@ -1211,10 +929,7 @@ mod tests { ); // In this case the appointment is kept in the Responder and, therefore, in the database assert!(watcher.responder.has_tracker(uuid)); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Ok(ExtendedAppointment { .. }) - )); + assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); // A properly formatted but invalid transaction should be rejected by the Responder // Update the Responder with a new Carrier that will reject the transaction @@ -1225,32 +940,26 @@ mod tests { *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())); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); assert_eq!( watcher.store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx), TriggeredAppointment::Rejected, ); // In this case the appointment is not kept in the Responder nor in the database assert!(!watcher.responder.has_tracker(uuid)); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err { .. } - )); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); // Invalid triggered appointments should not be passed to the Responder // Use a dispute_tx that does not match the appointment to replicate a decryption error // (the same applies to invalid formatted transactions) - let uuid = generate_uuid(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); assert_eq!( - watcher.store_triggered_appointment(uuid, &appointment, user_id, &get_random_tx()), + watcher.store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx), TriggeredAppointment::Invalid, ); // The appointment is not kept anywhere assert!(!watcher.responder.has_tracker(uuid)); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err { .. } - )); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); } #[tokio::test] @@ -1258,9 +967,10 @@ mod tests { let mut chain = Blockchain::default().with_height(START_HEIGHT); let (watcher, _s) = init_watcher(&mut chain).await; - let appointment = generate_dummy_appointment(None).inner; + let dispute_tx = get_random_tx(); + let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner; - // If the user cannot be properly identified, the request will fail. This can be simulated by providing a wrong signature + // If the user cannot be properly identified, the request will fail. This can be simulated by providing a wrong signature let wrong_sig = String::from_utf8((0..65).collect()).unwrap(); assert!(matches!( watcher.get_appointment(appointment.locator, &wrong_sig), @@ -1274,12 +984,12 @@ mod tests { watcher .add_appointment( appointment.clone(), - cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &user_sk), ) .unwrap(); let message = format!("get appointment {}", appointment.locator); - let signature = cryptography::sign(message.as_bytes(), &user_sk).unwrap(); + let signature = cryptography::sign(message.as_bytes(), &user_sk); let info = watcher .get_appointment(appointment.locator, &signature) .unwrap(); @@ -1293,32 +1003,19 @@ mod tests { // If the appointment is in the Responder (in the form of a Tracker), data should be also returned - // Remove the data from the Watcher memory first (data is kept in the db tho) + // Remove the data from the Watcher memory first. let uuid = UUID::new(appointment.locator, user_id); - watcher.appointments.lock().unwrap().remove(&uuid); - watcher - .locator_uuid_map - .lock() - .unwrap() - .remove(&appointment.locator); // Add data to the Responder - let breach = get_random_breach(); - let tracker = TransactionTracker::new( - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(chain.get_block_count()), - ); - - watcher.responder.add_tracker( - uuid, - breach, - user_id, - ConfirmationStatus::InMempoolSince(chain.get_block_count()), - ); + let breach = Breach::new(dispute_tx, get_random_tx()); + let status = ConfirmationStatus::InMempoolSince(chain.get_block_count()); + watcher + .responder + .add_tracker(uuid, breach.clone(), user_id, status); + let tracker = TransactionTracker::new(breach, user_id, status); let tracker_message = format!("get appointment {}", appointment.locator); - let tracker_signature = cryptography::sign(tracker_message.as_bytes(), &user_sk).unwrap(); + let tracker_signature = cryptography::sign(tracker_message.as_bytes(), &user_sk); let info = watcher .get_appointment(appointment.locator, &tracker_signature) .unwrap(); @@ -1330,13 +1027,13 @@ mod tests { AppointmentInfo::Tracker(t) => assert_eq!(t, tracker), } - // If the user does exists but the requested locator does not belong to any of their associated appointments, NotFound - // should be returned. + // If the user does exists but the requested locator does not belong to any of their associated appointments, + // NotFound should be returned. let (user2_sk, user2_pk) = get_random_keypair(); let user2_id = UserId(user2_pk); watcher.register(user2_id).unwrap(); - let signature2 = cryptography::sign(message.as_bytes(), &user2_sk).unwrap(); + let signature2 = cryptography::sign(message.as_bytes(), &user2_sk); assert!(matches!( watcher.get_appointment(appointment.locator, &signature2), Err(GetAppointmentFailure::NotFound { .. }) @@ -1345,12 +1042,7 @@ mod tests { // If the user subscription has expired, the request will fail watcher .gatekeeper - .get_registered_users() - .lock() - .unwrap() - .get_mut(&user_id) - .unwrap() - .subscription_expiry = START_HEIGHT as u32; + .add_outdated_user(user_id, START_HEIGHT as u32); assert!(matches!( watcher.get_appointment(appointment.locator, &signature), @@ -1361,285 +1053,168 @@ mod tests { #[tokio::test] 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, _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(); - for tx in txs { - locator_tx_map.insert(Locator::new(tx.txid()), tx.clone()); - } + let locator_tx_map: HashMap<_, _> = (0..10) + .map(|_| get_random_tx()) + .map(|tx| (Locator::new(tx.compute_txid()), tx)) + .collect(); + + let (user_sk, user_pk) = get_random_keypair(); + let user_id = UserId(user_pk); + watcher.register(user_id).unwrap(); // Add some of them to the Watcher - for (i, locator) in locator_tx_map.keys().enumerate() { + let mut breaches = HashMap::new(); + for (i, (l, tx)) in locator_tx_map.iter().enumerate() { + // Track some of the these transactions. if i % 2 == 0 { - watcher - .locator_uuid_map - .lock() - .unwrap() - .insert(*locator, HashSet::from_iter(vec![generate_uuid()])); + let appointment = generate_dummy_appointment(Some(&tx.compute_txid())).inner; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + watcher.add_appointment(appointment, signature).unwrap(); + breaches.insert(*l, tx.clone()); } } // Check that breaches are correctly detected - let breaches = watcher.get_breaches(locator_tx_map); - let locator_uuid_map = watcher.locator_uuid_map.lock().unwrap(); - assert!( - breaches.len() == locator_uuid_map.len() - && breaches.keys().all(|k| locator_uuid_map.contains_key(k)) - ); + assert_eq!(watcher.get_breaches(locator_tx_map), breaches); } #[tokio::test] - 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(); + async fn test_handle_breaches_accepted() { + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); 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(); - for tx in txs { - locator_tx_map.insert(Locator::new(tx.txid()), tx.clone()); + let breaches: HashMap<_, _> = (0..10) + .map(|_| get_random_tx()) + .map(|tx| (Locator::new(tx.compute_txid()), tx)) + .collect(); + + let (user_sk, user_pk) = get_random_keypair(); + let user_id = UserId(user_pk); + watcher.register(user_id).unwrap(); + + // Let the watcher track these breaches. + for (_, tx) in breaches.iter() { + let appointment = generate_dummy_appointment(Some(&tx.compute_txid())).inner; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + watcher.add_appointment(appointment, signature).unwrap(); } - // Add some of them to the Watcher - let mut local_valid = Vec::new(); - let mut local_invalid = Vec::new(); - - for (i, (locator, tx)) in locator_tx_map.iter().enumerate() { - let uuid = generate_uuid(); - let tx_id = tx.txid(); - let mut dispute_txid = None; - - // Add 1/3 as valid breaches, 1/3 as invalid, leave 1/3 out - if i % 3 < 2 { - match i % 3 { - 0 => { - dispute_txid = Some(&tx_id); - local_valid.push(uuid); - } - _ => local_invalid.push(uuid), - } - - let appointment = generate_dummy_appointment(dispute_txid); - - watcher - .appointments - .lock() - .unwrap() - .insert(uuid, appointment.get_summary()); - watcher - .locator_uuid_map - .lock() - .unwrap() - .insert(*locator, HashSet::from_iter(vec![uuid])); - - // Store data in the database (the user needs to be there as well since it is a FK for appointments) - store_appointment_and_fks_to_db(&watcher.dbm.lock().unwrap(), uuid, &appointment); - } - } - - let breaches = watcher.get_breaches(locator_tx_map.clone()); - let (valid, invalid) = watcher.filter_breaches(breaches); - - // Check valid + invalid add up to 2/3 - assert_eq!(2 * locator_tx_map.len() / 3, valid.len() + invalid.len()); - - // Check valid breaches match - assert!(valid.len() == local_valid.len() && valid.keys().all(|k| local_valid.contains(k))); - - // Check invalid breaches match - assert!( - invalid.len() == local_invalid.len() - && invalid.keys().all(|k| local_invalid.contains(k)) - ); - - // All invalid breaches should be AED errors (the decryption key was invalid) - invalid - .values() - .all(|v| matches!(v, cryptography::DecryptingError::AED { .. })); + assert!(watcher.handle_breaches(breaches).is_none()) } #[tokio::test] - async fn test_delete_appointments_from_memory() { - let mut chain = Blockchain::default().with_height(START_HEIGHT); + async fn test_handle_breaches_rejected_decryption() { + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); 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(); + // Let's create some locators based on the transactions in the last block + let breaches: HashMap<_, _> = (0..10) + .map(|_| get_random_tx()) + .map(|tx| (Locator::new(tx.compute_txid()), tx)) + .collect(); - for _ in 0..10 { - let uuid = generate_uuid(); - let appointment = generate_dummy_appointment(None); - watcher - .appointments - .lock() - .unwrap() - .insert(uuid, appointment.get_summary()); - watcher - .locator_uuid_map - .lock() - .unwrap() - .insert(appointment.locator(), HashSet::from_iter([uuid])); + let (user_sk, user_pk) = get_random_keypair(); + let user_id = UserId(user_pk); + watcher.register(user_id).unwrap(); - store_appointment_and_fks_to_db(&watcher.dbm.lock().unwrap(), uuid, &appointment); - to_be_deleted.insert(uuid, appointment.locator()); - } - - // Delete and check data is not in memory (the reason does not matter for the test) - watcher.delete_appointments_from_memory( - &to_be_deleted.keys().cloned().collect(), - DeletionReason::Outdated, - ); - - for (uuid, locator) in to_be_deleted { - // Data is not in memory - assert!(!watcher.appointments.lock().unwrap().contains_key(&uuid)); - assert!(!watcher - .locator_uuid_map - .lock() - .unwrap() - .contains_key(&locator)); - - // But it can be found in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Ok(ExtendedAppointment { .. }) - )); - } - } - - #[tokio::test] - async fn test_delete_appointments() { - // 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, _s) = init_watcher(&mut chain).await; - - // Delete appointments removes data from the appointments and locator_uuid_map - // Add data to the map first - let mut all_appointments = HashSet::new(); - let mut target_appointments = HashSet::new(); - let mut uuid_locator_map = HashMap::new(); - let mut locator_with_multiple_uuids = HashSet::new(); - let mut updated_users = HashMap::new(); - - for i in 0..10 { - let uuid = generate_uuid(); - let appointment = generate_dummy_appointment(None); - watcher - .appointments - .lock() - .unwrap() - .insert(uuid, appointment.clone().get_summary()); - watcher - .locator_uuid_map - .lock() - .unwrap() - .insert(appointment.locator(), HashSet::from_iter([uuid])); - - // Add data to the database to check data deletion - store_appointment_and_fks_to_db(&watcher.dbm.lock().unwrap(), uuid, &appointment); - - // Make it so some of the locators have multiple associated uuids - if i % 3 == 0 { - // We don't need to store this properly since they will not be targeted - let uuid2 = generate_uuid(); - watcher - .locator_uuid_map - .lock() - .unwrap() - .get_mut(&appointment.locator()) - .unwrap() - .insert(uuid2); - locator_with_multiple_uuids.insert(appointment.locator()); - } - - all_appointments.insert(uuid); - uuid_locator_map.insert(uuid, appointment.locator()); - - // Add some appointments to be deleted + let mut rejected = HashSet::new(); + // Let the watcher track these breaches. + for (i, (_, tx)) in breaches.iter().enumerate() { + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&tx.compute_txid())); + let mut appointment = appointment.inner; if i % 2 == 0 { - // Users will also be updated once the data is deleted. - // We can made up the numbers here just to check they are updated. - target_appointments.insert(uuid); - updated_users.insert( - appointment.user_id, - UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ), - ); - } + // Mal-format some appointments + appointment.encrypted_blob.reverse(); + rejected.insert(uuid); + }; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + watcher.add_appointment(appointment, signature).unwrap(); } - // The deletion reason does not matter here, it only changes the logged message when deleting data - watcher.delete_appointments( - &target_appointments, - &updated_users, - DeletionReason::Accepted, + assert_eq!( + rejected, + HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) ); + } - // Only appointments in the target_appointments map should have been removed from - // the Watcher's data structures. - for uuid in all_appointments { - if target_appointments.contains(&uuid) { - assert!(!watcher.appointments.lock().unwrap().contains_key(&uuid)); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); + #[tokio::test] + async fn test_handle_breaches_rejected_by_responder_backend() { + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); + let (watcher, _s) = init_watcher(&mut chain).await; - let locator = &uuid_locator_map[&uuid]; - // If the penalty had more than one associated uuid, only one has been deleted - // (because that's how the test has been designed) - if locator_with_multiple_uuids.contains(locator) { - assert_eq!( - watcher - .locator_uuid_map - .lock() - .unwrap() - .get(locator) - .unwrap() - .len(), - 1 - ); - } else { - // Otherwise the whole structure is removed, given it is now empty - assert!(!watcher - .locator_uuid_map - .lock() - .unwrap() - .contains_key(locator)); - } - } else { - assert!(watcher.appointments.lock().unwrap().contains_key(&uuid)); - assert!(watcher - .locator_uuid_map - .lock() - .unwrap() - .contains_key(&uuid_locator_map[&uuid])); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Ok(ExtendedAppointment { .. }) - )); - } + // Replace the carrier with an erroneous one + let (carrier, _s) = create_carrier( + MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), + chain.tip().deref().height, + ); + *watcher.responder.get_carrier().lock().unwrap() = carrier; + + // Let's create some locators based on the transactions in the last block + let breaches: HashMap<_, _> = (0..10) + .map(|_| get_random_tx()) + .map(|tx| (Locator::new(tx.compute_txid()), tx)) + .collect(); + + let (user_sk, user_pk) = get_random_keypair(); + let user_id = UserId(user_pk); + watcher.register(user_id).unwrap(); + + let mut uuids = HashSet::new(); + // Let the watcher track these breaches. + for tx in breaches.values() { + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&tx.compute_txid())); + let appointment = appointment.inner; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + watcher.add_appointment(appointment, signature).unwrap(); + uuids.insert(uuid); } - // The users that needed to be updated in the database have been (just checking the slot count) - for (id, info) in updated_users { - assert_eq!( - watcher - .dbm - .lock() - .unwrap() - .load_user(id) - .unwrap() - .available_slots, - info.available_slots - ); + assert_eq!( + uuids, + HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) + ); + } + + #[tokio::test] + async fn test_handle_breaches_rejected_by_responder_malformed() { + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); + let (watcher, _s) = init_watcher(&mut chain).await; + + // Let's create some locators based on the transactions in the last block + let breaches: HashMap<_, _> = (0..10) + .map(|_| get_random_tx()) + .map(|tx| (Locator::new(tx.compute_txid()), tx)) + .collect(); + + let (user_sk, user_pk) = get_random_keypair(); + let user_id = UserId(user_pk); + watcher.register(user_id).unwrap(); + + let mut rejected_breaches = HashSet::new(); + // Let the watcher track these breaches. + for (i, (_, tx)) in breaches.iter().enumerate() { + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&tx.compute_txid())); + let mut appointment = appointment.inner; + if i % 2 == 0 { + // Mal-format some appointments, they should be returned as rejected. + appointment.encrypted_blob.reverse(); + rejected_breaches.insert(uuid); + }; + let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + watcher.add_appointment(appointment, signature).unwrap(); } + + assert_eq!( + rejected_breaches, + HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) + ); } #[tokio::test] @@ -1665,9 +1240,11 @@ mod tests { // If there are appointments to watch, the Watcher will: // - Check if any new transaction is a trigger // - Check if a trigger is valid, if so pass the data to the Responder - // - Delete invalid appointments. + // - Delete invalid appointments (decryption error or rejection by responder). // - Delete appointments that have been outdated (i.e. have expired without a trigger) - // - Delete invalid appointments also from the Gatekeeper (not outdated tough, the GK will take care of those via it's own Listen) + // - Delete invalid appointments also from the Gatekeeper + // + // We will also test that appointments for outdated users are removed by the GK. // Let's first check how data gets outdated (create two users, add an appointment to both and outdate only one) let (user_sk, user_pk) = get_random_keypair(); @@ -1677,188 +1254,111 @@ mod tests { watcher.register(user_id).unwrap(); watcher.register(user2_id).unwrap(); - let appointment = generate_dummy_appointment(None); - let uuid1 = UUID::new(appointment.locator(), user_id); - let uuid2 = UUID::new(appointment.locator(), user2_id); + let appointment = generate_dummy_appointment(None).inner; + let uuid1 = UUID::new(appointment.locator, user_id); + let uuid2 = UUID::new(appointment.locator, user2_id); - let user_sig = cryptography::sign(&appointment.inner.to_vec(), &user_sk).unwrap(); + let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk); watcher - .add_appointment(appointment.inner.clone(), user_sig) - .unwrap(); - let user2_sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - watcher - .add_appointment(appointment.inner.clone(), user2_sig) + .add_appointment(appointment.clone(), user_sig) .unwrap(); + let user2_sig = cryptography::sign(&appointment.to_vec(), &user2_sk); + watcher.add_appointment(appointment, user2_sig).unwrap(); + // Outdate the first user's registration. watcher + .gatekeeper + .add_outdated_user(user_id, chain.get_block_count()); + + // Both appointments can be found before mining a block, only the user's 2 can be found afterwards + for &uuid in &[uuid1, uuid2] { + assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); + } + + // We always need to connect the gatekeeper first so it cleans up outdated users and their data. + let block = chain.generate(None); + watcher + .gatekeeper + .block_connected(&block, chain.get_block_count()); + watcher.block_connected(&block, chain.get_block_count()); + + // uuid1 and user1 should have been deleted while uuid2 and user2 still exists. + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid1)); + assert!(!watcher .gatekeeper .get_registered_users() .lock() .unwrap() - .get_mut(&user_id) + .contains_key(&user_id)); + assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid2)); + assert!(watcher + .gatekeeper + .get_registered_users() + .lock() .unwrap() - .subscription_expiry = chain.get_block_count() - EXPIRY_DELTA + 1; - - // Both appointments can be found before mining a block, only the user's 2 can be found afterwards - for uuid in &[uuid1, uuid2] { - assert!(watcher.appointments.lock().unwrap().contains_key(uuid)); - assert!( - watcher.locator_uuid_map.lock().unwrap()[&appointment.locator()].contains(uuid) - ); - } - assert!( - watcher.gatekeeper.get_registered_users().lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid1) - ); - assert!( - watcher.gatekeeper.get_registered_users().lock().unwrap()[&user2_id] - .appointments - .contains_key(&uuid2) - ); - - watcher.block_connected(&chain.generate(None), chain.get_block_count()); - - assert!(!watcher.appointments.lock().unwrap().contains_key(&uuid1)); - assert!(!watcher.locator_uuid_map.lock().unwrap()[&appointment.locator()].contains(&uuid1)); - // Data is still in the Gatekeeper and in the database, since it'll be deleted in cascade by the - // Gatekeeper on user's deletion (given the user was outdated in the test). - assert!( - watcher.gatekeeper.get_registered_users().lock().unwrap()[&user_id] - .appointments - .contains_key(&uuid1) - ); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid1), - Ok(ExtendedAppointment { .. }) - )); - - assert!(watcher.appointments.lock().unwrap().contains_key(&uuid2)); - assert!(watcher.locator_uuid_map.lock().unwrap()[&appointment.locator()].contains(&uuid2)); - assert!( - watcher.gatekeeper.get_registered_users().lock().unwrap()[&user2_id] - .appointments - .contains_key(&uuid2) - ); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid2), - Ok(ExtendedAppointment { .. }) - )); + .contains_key(&user2_id)); // Check triggers. Add a new appointment and trigger it with valid data. let dispute_tx = get_random_tx(); - let appointment = generate_dummy_appointment(Some(&dispute_tx.txid())); - let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - let uuid = UUID::new(appointment.locator(), user2_id); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user2_id, Some(&dispute_tx.compute_txid())); + let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk); watcher.add_appointment(appointment.inner, sig).unwrap(); - assert!(watcher.appointments.lock().unwrap().contains_key(&uuid)); + assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); - watcher.block_connected( - &chain.generate(Some(vec![dispute_tx])), - chain.get_block_count(), - ); - - // Data should have been moved to the Responder and kept in the Gatekeeper, since it is still part of the system. - assert!(!watcher.appointments.lock().unwrap().contains_key(&uuid)); - assert!(watcher - .responder - .get_trackers() - .lock() - .unwrap() - .contains_key(&uuid)); - assert!( - watcher.gatekeeper.get_registered_users().lock().unwrap()[&user2_id] - .appointments - .contains_key(&uuid) - ); + let block = chain.generate(Some(vec![dispute_tx])); + watcher + .gatekeeper + .block_connected(&block, chain.get_block_count()); + watcher.block_connected(&block, chain.get_block_count()); // Data should have been kept in the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Ok(ExtendedAppointment { .. }) - )); - assert!(matches!( - watcher.dbm.lock().unwrap().load_tracker(uuid), - Ok(TransactionTracker { .. }) - )); + assert!(watcher.responder.has_tracker(uuid)); + + // Checks invalid triggers. Add a new appointment and trigger it with invalid data. + let dispute_tx = get_random_tx(); + let (uuid, mut appointment) = + generate_dummy_appointment_with_user(user2_id, Some(&dispute_tx.compute_txid())); + // Modify the encrypted blob so the data is invalid. + appointment.inner.encrypted_blob.reverse(); + let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk); + watcher.add_appointment(appointment.inner, sig).unwrap(); + + let block = chain.generate(Some(vec![dispute_tx])); + watcher + .gatekeeper + .block_connected(&block, chain.get_block_count()); + watcher.block_connected(&block, chain.get_block_count()); + + // Data should have been wiped from the database + assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); // Check triggering with a valid formatted transaction but that is rejected by the Responder. let dispute_tx = get_random_tx(); - let appointment = generate_dummy_appointment(Some(&dispute_tx.txid())); - let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - let uuid = UUID::new(appointment.locator(), user2_id); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user2_id, Some(&dispute_tx.compute_txid())); + let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk); watcher.add_appointment(appointment.inner, sig).unwrap(); // Set the carrier response - let (carrier, _as) = create_carrier( + // Both non-decryptable blobs and blobs with invalid transactions will yield an invalid trigger. + let (carrier, _s) = 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])), - chain.get_block_count(), - ); - - // Data should not be in the Responder, in the Watcher nor in the Gatekeeper - assert!(!watcher.appointments.lock().unwrap().contains_key(&uuid)); - assert!(!watcher - .responder - .get_trackers() - .lock() - .unwrap() - .contains_key(&uuid)); - assert!( - !watcher.gatekeeper.get_registered_users().lock().unwrap()[&user2_id] - .appointments - .contains_key(&uuid) - ); - // Data should also have been deleted from the database - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); - assert!(matches!( - watcher.dbm.lock().unwrap().load_tracker(uuid), - Err(DBError::NotFound) - )); - - // Checks invalid triggers. Add a new appointment and trigger it with invalid data. - let dispute_tx = get_random_tx(); - let mut appointment = generate_dummy_appointment(Some(&dispute_tx.txid())); - // Modify the encrypted blob so the data is invalid. - //Both non-decryptable blobs and blobs with invalid transactions will yield an invalid trigger - appointment.inner.encrypted_blob.reverse(); - let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - let uuid = UUID::new(appointment.locator(), user2_id); + let block = chain.generate(Some(vec![dispute_tx])); watcher - .add_appointment(appointment.inner.clone(), sig) - .unwrap(); + .gatekeeper + .block_connected(&block, chain.get_block_count()); + watcher.block_connected(&block, chain.get_block_count()); - watcher.block_connected( - &chain.generate(Some(vec![dispute_tx])), - chain.get_block_count(), - ); - - // Data has been wiped since it was invalid - assert!(!watcher.appointments.lock().unwrap().contains_key(&uuid)); - assert!(!watcher - .responder - .get_trackers() - .lock() - .unwrap() - .contains_key(&uuid)); - assert!( - !watcher.gatekeeper.get_registered_users().lock().unwrap()[&user2_id] - .appointments - .contains_key(&uuid) - ); - assert!(matches!( - watcher.dbm.lock().unwrap().load_appointment(uuid), - Err(DBError::NotFound) - )); + // Data should have been wiped from the database + assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); } #[tokio::test] diff --git a/watchtower-plugin/Cargo.toml b/watchtower-plugin/Cargo.toml index 199cb8f..d216835 100755 --- a/watchtower-plugin/Cargo.toml +++ b/watchtower-plugin/Cargo.toml @@ -3,7 +3,7 @@ name = "watchtower-plugin" version = "0.2.0" authors = ["Sergi Delgado Segura "] license = "MIT" -edition = "2018" +edition = "2021" [[bin]] name = "watchtower-client" @@ -21,16 +21,16 @@ log = "0.4.16" rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } serde = "1.0.130" serde_json = { version = "1.0", features = [ "preserve_order" ] } -tonic = { version = "^0.5", features = [ "tls", "transport" ] } +tonic = { version = "0.11", features = [ "tls", "transport" ] } tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] } # Bitcoin and Lightning -bitcoin = "0.28.0" -cln-plugin = "0.1.2" +bitcoin = "0.32.0" +cln-plugin = "0.3.0" # Local teos-common = { path = "../teos-common" } [dev-dependencies] -httpmock = "0.6" -tempdir = "0.3.7" \ No newline at end of file +mockito = "0.32.4" +tempdir = "0.3.7" diff --git a/watchtower-plugin/README.md b/watchtower-plugin/README.md index 1810653..0f9ec7e 100644 --- a/watchtower-plugin/README.md +++ b/watchtower-plugin/README.md @@ -12,6 +12,7 @@ The plugin has the following methods: - `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. +- `pingtower `: Polls the tower to check if it is online. - `listtowers`: lists all registered towers. - `getappointment `: queries a given tower about an appointment. - `getsubscriptioninfo `: gets the subscription information by querying the tower. diff --git a/watchtower-plugin/src/constants.rs b/watchtower-plugin/src/constants.rs index 1b14803..90c50d8 100644 --- a/watchtower-plugin/src/constants.rs +++ b/watchtower-plugin/src/constants.rs @@ -44,6 +44,8 @@ pub const RPC_RETRY_TOWER_DESC: &str = "Retries to send pending appointment to an unreachable tower"; pub const RPC_ABANDON_TOWER: &str = "abandontower"; pub const RPC_ABANDON_TOWER_DESC: &str = "Forgets about a tower and wipes all local data"; +pub const RPC_PING: &str = "pingtower"; +pub const RPC_PING_DESC: &str = "Polls the tower to check if it is online"; /// Collections of hook names diff --git a/watchtower-plugin/src/convert.rs b/watchtower-plugin/src/convert.rs index ec1b9d0..77ca95a 100644 --- a/watchtower-plugin/src/convert.rs +++ b/watchtower-plugin/src/convert.rs @@ -94,6 +94,8 @@ impl RegisterParams { impl TryFrom for RegisterParams { type Error = RegisterError; + // clippy-fix: We are getting more than just the first item, so this clippy check does not make sense here + #[allow(clippy::get_first)] fn try_from(value: serde_json::Value) -> Result { match value { serde_json::Value::String(s) => { @@ -193,6 +195,8 @@ pub struct GetAppointmentParams { impl TryFrom for GetAppointmentParams { type Error = GetAppointmentError; + // clippy-fix: We are getting more than just the first item, so this clippy check does not make sense here + #[allow(clippy::get_first)] fn try_from(value: serde_json::Value) -> Result { match value { serde_json::Value::Array(a) => { diff --git a/watchtower-plugin/src/dbm.rs b/watchtower-plugin/src/dbm.rs index ec03886..bf271bb 100755 --- a/watchtower-plugin/src/dbm.rs +++ b/watchtower-plugin/src/dbm.rs @@ -125,7 +125,7 @@ impl DBM { /// /// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed, /// but they are not accessible from the API either. - pub fn load_client_key(&self) -> Result { + pub fn load_client_key(&self) -> Option { let mut stmt = self .connection .prepare( @@ -137,7 +137,7 @@ impl DBM { let sk: String = row.get(0).unwrap(); Ok(SecretKey::from_str(&sk).unwrap()) }) - .map_err(|_| Error::NotFound) + .ok() } /// Stores a tower record into the database alongside the corresponding registration receipt. @@ -171,7 +171,7 @@ impl DBM { /// Tower records are composed from the tower information and the appointment data. The latter is split in: /// accepted appointments (represented by appointment receipts), pending appointments and invalid appointments. /// In the case that the tower has misbehaved, then a misbehaving proof is also attached to the record. - pub fn load_tower_record(&self, tower_id: TowerId) -> Result { + pub fn load_tower_record(&self, tower_id: TowerId) -> Option { let mut stmt = self .connection .prepare("SELECT t.net_addr, t.available_slots, r.subscription_start, r.subscription_expiry @@ -197,16 +197,16 @@ impl DBM { self.load_appointments(tower_id, AppointmentStatus::Invalid), )) }) - .map_err(|_| Error::NotFound)?; + .ok()?; - if let Ok(proof) = self.load_misbehaving_proof(tower_id) { + if let Some(proof) = self.load_misbehaving_proof(tower_id) { tower.status = TowerStatus::Misbehaving; tower.set_misbehaving_proof(proof); } else if !tower.pending_appointments.is_empty() { tower.status = TowerStatus::TemporaryUnreachable; } - Ok(tower) + Some(tower) } /// Loads the latest registration receipt for a given tower. @@ -216,7 +216,7 @@ impl DBM { &self, tower_id: TowerId, user_id: UserId, - ) -> Result { + ) -> Option { let mut stmt = self .connection .prepare( @@ -228,20 +228,17 @@ impl DBM { ) .unwrap(); - let receipt = stmt - .query_row([tower_id.to_vec()], |row| { - let slots: u32 = row.get(0).unwrap(); - let start: u32 = row.get(1).unwrap(); - let expiry: u32 = row.get(2).unwrap(); - let signature: String = row.get(3).unwrap(); + stmt.query_row([tower_id.to_vec()], |row| { + let slots: u32 = row.get(0).unwrap(); + let start: u32 = row.get(1).unwrap(); + let expiry: u32 = row.get(2).unwrap(); + let signature: String = row.get(3).unwrap(); - Ok(RegistrationReceipt::with_signature( - user_id, slots, start, expiry, signature, - )) - }) - .map_err(|_| Error::NotFound)?; - - Ok(receipt) + Ok(RegistrationReceipt::with_signature( + user_id, slots, start, expiry, signature, + )) + }) + .ok() } /// Removes a tower record from the database. @@ -333,7 +330,7 @@ impl DBM { &self, tower_id: TowerId, locator: Locator, - ) -> Result { + ) -> Option { let mut stmt = self .connection .prepare("SELECT start_block, user_signature, tower_signature FROM appointment_receipts WHERE tower_id = ?1 and locator = ?2") @@ -350,7 +347,7 @@ impl DBM { tower_sig, )) }) - .map_err(|_| Error::NotFound) + .ok() } /// Loads the appointment receipts associated to a given tower. @@ -406,7 +403,7 @@ impl DBM { } /// Loads an appointment from the database. - pub fn load_appointment(&self, locator: Locator) -> Result { + pub fn load_appointment(&self, locator: Locator) -> Option { let mut stmt = self .connection .prepare("SELECT encrypted_blob, to_self_delay FROM appointments WHERE locator = ?") @@ -418,7 +415,7 @@ impl DBM { Ok(Appointment::new(locator, encrypted_blob, to_self_delay)) }) - .map_err(|_| Error::NotFound) + .ok() } /// Stores an appointment into the database. @@ -598,7 +595,7 @@ impl DBM { } /// Loads the misbehaving proof for a given tower from the database (if found). - fn load_misbehaving_proof(&self, tower_id: TowerId) -> Result { + fn load_misbehaving_proof(&self, tower_id: TowerId) -> Option { let mut misbehaving_stmt = self .connection .prepare("SELECT locator, recovered_id FROM misbehaving_proofs WHERE tower_id = ?") @@ -633,7 +630,7 @@ impl DBM { .unwrap(); MisbehaviorProof::new(locator, receipt, recovered_id) }) - .map_err(|_| Error::NotFound) + .ok() } /// Checks whether a misbehaving proof exists for a given tower. @@ -792,10 +789,7 @@ mod tests { // If the tower does not exists, `load_tower` will fail. let tower_id = get_random_user_id(); - assert!(matches!( - dbm.load_tower_record(tower_id), - Err(Error::NotFound) - )); + assert!(dbm.load_tower_record(tower_id).is_none()); } #[test] @@ -917,10 +911,9 @@ mod tests { // 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) - )); + assert!(dbm + .load_appointment_receipt(tower_id, appointment.locator) + .is_none()); // Add the tower but not the appointment and try again let net_addr = "talaia.watch"; @@ -928,10 +921,9 @@ mod tests { dbm.store_tower_record(tower_id, net_addr, &receipt) .unwrap(); - assert!(matches!( - dbm.load_appointment_receipt(tower_id, appointment.locator), - Err(Error::NotFound) - )); + assert!(dbm + .load_appointment_receipt(tower_id, appointment.locator) + .is_none()); // Add both let tower_summary = TowerSummary::new( @@ -1045,7 +1037,7 @@ mod tests { let locator = generate_random_appointment(None).locator; let loaded_appointment = dbm.load_appointment(locator); - assert!(matches!(loaded_appointment, Err(Error::NotFound))); + assert!(loaded_appointment.is_none()); } #[test] @@ -1284,10 +1276,7 @@ mod tests { #[test] fn test_store_load_non_existing_misbehaving_proof() { let dbm = DBM::in_memory().unwrap(); - assert!(matches!( - dbm.load_misbehaving_proof(get_random_user_id()), - Err(Error::NotFound) - )); + assert!(dbm.load_misbehaving_proof(get_random_user_id()).is_none()); } #[test] @@ -1340,7 +1329,7 @@ mod tests { fn test_store_load_client_key() { let dbm = DBM::in_memory().unwrap(); - assert!(matches!(dbm.load_client_key(), Err(Error::NotFound))); + assert!(dbm.load_client_key().is_none()); for _ in 0..7 { let sk = get_random_keypair().0; dbm.store_client_key(&sk).unwrap(); diff --git a/watchtower-plugin/src/main.rs b/watchtower-plugin/src/main.rs index f7c0fbf..a344084 100755 --- a/watchtower-plugin/src/main.rs +++ b/watchtower-plugin/src/main.rs @@ -8,7 +8,8 @@ use serde_json::json; use tokio::io::{stdin, stdout}; use tokio::sync::mpsc::unbounded_channel; -use cln_plugin::options::{ConfigOption, Value}; +use cln_plugin::options::config_type::DefaultInteger; +use cln_plugin::options::ConfigOption; use cln_plugin::{anyhow, Builder, Error, Plugin}; use teos_common::appointment::{Appointment, Locator}; @@ -20,13 +21,39 @@ use teos_common::{cryptography, errors}; use watchtower_plugin::convert::{CommitmentRevocation, GetAppointmentParams, RegisterParams}; use watchtower_plugin::net::http::{ - self, post_request, process_post_response, AddAppointmentError, ApiResponse, RequestError, + self, get_request, post_request, process_post_response, AddAppointmentError, ApiResponse, + RequestError, }; use watchtower_plugin::net::ProxyInfo; use watchtower_plugin::retrier::RetryManager; use watchtower_plugin::wt_client::{RevocationData, WTClient}; use watchtower_plugin::{constants, TowerStatus}; +const DEV_WT_MAX_RETRY_INTERVAL_CONFIG: ConfigOption = + ConfigOption::new_i64_with_default( + constants::DEV_WT_MAX_RETRY_INTERVAL, + constants::DEFAULT_DEV_WT_MAX_RETRY_INTERVAL, + constants::DEV_WT_MAX_RETRY_INTERVAL_DESC, + ); + +const WT_AUTO_RETRY_DELAY_CONFIG: ConfigOption = ConfigOption::new_i64_with_default( + constants::WT_AUTO_RETRY_DELAY, + constants::DEFAULT_WT_AUTO_RETRY_DELAY, + constants::WT_AUTO_RETRY_DELAY_DESC, +); + +const WT_MAX_RETRY_TIME_CONFIG: ConfigOption = ConfigOption::new_i64_with_default( + constants::WT_MAX_RETRY_TIME, + constants::DEFAULT_WT_MAX_RETRY_TIME, + constants::WT_MAX_RETRY_TIME_DESC, +); + +const WT_PORT_CONFG: ConfigOption = ConfigOption::new_i64_with_default( + constants::WT_PORT, + constants::DEFAULT_WT_PORT, + constants::WT_PORT_DESC, +); + fn to_cln_error(e: RequestError) -> Error { let e = match e { RequestError::ConnectionError(e) => anyhow!(e), @@ -75,12 +102,12 @@ async fn register( // which is not available in the current version of `cln-plugin` (but already on master). Add it for the next release. let port = params.port.unwrap_or( - u16::try_from(plugin.option(constants::WT_PORT).unwrap().as_i64().unwrap()) + u16::try_from(plugin.option(&WT_PORT_CONFG).unwrap()) .map_err(|_| anyhow!("{} out of range", constants::WT_PORT))?, ); let tower_net_addr = { - if !host.starts_with("http://") { + if !host.starts_with("http://") && !host.starts_with("https://") { host = format!("http://{host}") } NetAddr::new(format!("{host}:{port}")) @@ -136,11 +163,13 @@ async fn get_registration_receipt( 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 {tower_id} within the known towers. Have you registered?") - })?; - - Ok(json!(response)) + if let Some(response) = state.get_registration_receipt(tower_id) { + Ok(json!(response)) + } else { + Err(anyhow!( + "Cannot find {tower_id} within the known towers. Have you registered?" + )) + } } /// Gets the subscription information directly form the tower. @@ -159,7 +188,7 @@ async fn get_subscription_info( } }?; - let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk).unwrap(); + let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk); let response: common_msgs::GetSubscriptionInfoResponse = process_post_response( post_request( @@ -204,8 +233,7 @@ async fn get_appointment( let signature = cryptography::sign( format!("get appointment {}", params.locator).as_bytes(), &user_sk, - ) - .unwrap(); + ); let response: ApiResponse = process_post_response( post_request( @@ -244,24 +272,20 @@ async fn get_appointment_receipt( 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)) + if let Some(r) = state.get_appointment_receipt(params.tower_id, params.locator) { + Ok(json!(r)) + } else if state.towers.contains_key(¶ms.tower_id) { + Err(anyhow!( + "Cannot find {} within {}. Did you send that appointment?", + params.locator, + params.tower_id + )) + } else { + Err(anyhow!( + "Cannot find {} within the known towers. Have you registered?", + params.tower_id + )) + } } /// Lists all the registered towers. @@ -283,15 +307,50 @@ async fn get_tower_info( ) -> Result { let state = plugin.state().lock().unwrap(); let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; - let tower_info = state.load_tower_info(tower_id).map_err(|_| { - anyhow!("Cannot find {tower_id} within the known towers. Have you registered?") - })?; - // Notice we need to check the status in memory since we cannot distinguish between unreachable and temporary unreachable - // by just checking the data in the database. - Ok(json!( - tower_info.with_status(state.get_tower_status(&tower_id).unwrap()) - )) + if let Some(tower_info) = state.load_tower_info(tower_id) { + // Notice we need to check the status in memory since we cannot distinguish between unreachable and temporary unreachable + // by just checking the data in the database. + Ok(json!( + tower_info.with_status(state.get_tower_status(&tower_id).unwrap()) + )) + } else { + Err(anyhow!( + "Cannot find {tower_id} within the known towers. Have you registered?", + )) + } +} + +async fn ping( + plugin: Plugin>>, + v: serde_json::Value, +) -> Result { + let (tower_net_addr, proxy) = { + // Check if the tower_id is known to the plugin + let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; + let state = plugin.state().lock().unwrap(); + ( + state + .towers + .get(&tower_id) + .ok_or(anyhow!("Unknown tower_id"))? + .net_addr + .clone(), + state.proxy.clone(), + ) + }; + let response = get_request(&tower_net_addr, Endpoint::Ping, &proxy) + .await + .map_err(to_cln_error)?; + + if response.status().is_success() { + Ok(json!("Tower is reachable")) + } else { + Err(anyhow!(format!( + "Tower cannot be reached (Error: {})", + response.status() + ))) + } } /// Triggers a manual retry of a tower, tries to send all pending appointments to it. @@ -351,7 +410,7 @@ async fn abandon_tower( ) -> 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() { + if state.towers.contains_key(&tower_id) { state.remove_tower(tower_id).unwrap(); Ok(json!(format!("{tower_id} successfully abandoned"))) } else { @@ -388,8 +447,7 @@ async fn on_commitment_revocation( let signature = cryptography::sign( &appointment.to_vec(), &plugin.state().lock().unwrap().user_sk, - ) - .unwrap(); + ); // Looks like we cannot iterate through towers given a locked state is not Send (due to the async call), // so we need to clone the bare minimum. @@ -500,26 +558,10 @@ async fn main() -> Result<(), Error> { }; let builder = Builder::new(stdin(), stdout()) - .option(ConfigOption::new( - constants::WT_PORT, - Value::Integer(constants::DEFAULT_WT_PORT), - constants::WT_PORT_DESC, - )) - .option(ConfigOption::new( - constants::WT_MAX_RETRY_TIME, - Value::Integer(constants::DEFAULT_WT_MAX_RETRY_TIME), - constants::WT_MAX_RETRY_TIME_DESC, - )) - .option(ConfigOption::new( - constants::WT_AUTO_RETRY_DELAY, - Value::Integer(constants::DEFAULT_WT_AUTO_RETRY_DELAY), - constants::WT_AUTO_RETRY_DELAY_DESC, - )) - .option(ConfigOption::new( - constants::DEV_WT_MAX_RETRY_INTERVAL, - Value::Integer(constants::DEFAULT_DEV_WT_MAX_RETRY_INTERVAL), - constants::DEV_WT_MAX_RETRY_INTERVAL_DESC, - )) + .option(WT_PORT_CONFG) + .option(WT_MAX_RETRY_TIME_CONFIG) + .option(WT_AUTO_RETRY_DELAY_CONFIG) + .option(DEV_WT_MAX_RETRY_INTERVAL_CONFIG) .rpcmethod( constants::RPC_REGISTER_TOWER, constants::RPC_REGISTER_TOWER_DESC, @@ -555,6 +597,7 @@ async fn main() -> Result<(), Error> { constants::RPC_GET_TOWER_INFO_DESC, get_tower_info, ) + .rpcmethod(constants::RPC_PING, constants::RPC_PING_DESC, ping) .rpcmethod( constants::RPC_RETRY_TOWER, constants::RPC_RETRY_TOWER_DESC, @@ -594,40 +637,21 @@ async fn main() -> Result<(), Error> { .await, )); - let max_elapsed_time = u16::try_from( - midstate - .option(constants::WT_MAX_RETRY_TIME) - .unwrap() - .as_i64() - .unwrap(), - ) - .map_err(|e| { - log::error!("{} out of range", constants::WT_MAX_RETRY_TIME); - e - })?; + let max_elapsed_time = u16::try_from(midstate.option(&WT_MAX_RETRY_TIME_CONFIG).unwrap()) + .inspect_err(|_| { + log::error!("{} out of range", constants::WT_MAX_RETRY_TIME); + })?; - let auto_retry_delay = u32::try_from( - midstate - .option(constants::WT_AUTO_RETRY_DELAY) - .unwrap() - .as_i64() - .unwrap(), - ) - .map_err(|e| { - log::error!("{} out of range", constants::WT_AUTO_RETRY_DELAY); - e - })?; + let auto_retry_delay = u32::try_from(midstate.option(&WT_AUTO_RETRY_DELAY_CONFIG).unwrap()) + .inspect_err(|_| { + log::error!("{} out of range", constants::WT_AUTO_RETRY_DELAY); + })?; let max_interval_time = u16::try_from( - midstate - .option(constants::DEV_WT_MAX_RETRY_INTERVAL) - .unwrap() - .as_i64() - .unwrap(), + midstate.option(&DEV_WT_MAX_RETRY_INTERVAL_CONFIG).unwrap(), ) - .map_err(|e| { + .inspect_err(|_| { log::error!("{} out of range", constants::DEV_WT_MAX_RETRY_INTERVAL); - e })?; let plugin = midstate.start(wt_client.clone()).await?; diff --git a/watchtower-plugin/src/net/http.rs b/watchtower-plugin/src/net/http.rs index 23448a5..1a93633 100644 --- a/watchtower-plugin/src/net/http.rs +++ b/watchtower-plugin/src/net/http.rs @@ -1,4 +1,4 @@ -use reqwest::Response; +use reqwest::{Method, Response}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use teos_common::appointment::Appointment; @@ -154,12 +154,13 @@ pub async fn send_appointment( } } -/// Generic function to post different types of requests to the tower. -pub async fn post_request( +/// A generic function to send a request to a tower. +async fn request( tower_net_addr: &NetAddr, endpoint: Endpoint, - data: S, proxy: &Option, + method: Method, + data: Option, ) -> Result { let client = if let Some(proxy) = proxy { if proxy.always_use || tower_net_addr.is_onion() { @@ -183,23 +184,42 @@ pub async fn post_request( reqwest::Client::new() }; - client - .post(format!("{}{}", tower_net_addr.net_addr(), endpoint.path())) - .json(&data) - .send() - .await - .map_err(|e| { - log::debug!("An error ocurred when sending data to the tower: {e}"); - if e.is_connect() | e.is_timeout() { - RequestError::ConnectionError( - "Cannot connect to the tower. Connection refused".to_owned(), - ) - } else { - RequestError::Unexpected( - "Unexpected error ocurred (see logs for more info)".to_owned(), - ) - } - }) + let mut request_builder = client.request( + method, + format!("{}{}", tower_net_addr.net_addr(), endpoint.path()), + ); + + if let Some(data) = data { + request_builder = request_builder.json(&data); + } + + request_builder.send().await.map_err(|e| { + log::debug!("An error ocurred when sending data to the tower: {e}"); + if e.is_connect() | e.is_timeout() { + RequestError::ConnectionError( + "Cannot connect to the tower. Connection refused".to_owned(), + ) + } else { + RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".to_owned()) + } + }) +} + +pub async fn post_request( + tower_net_addr: &NetAddr, + endpoint: Endpoint, + data: S, + proxy: &Option, +) -> Result { + request(tower_net_addr, endpoint, proxy, Method::POST, Some(data)).await +} + +pub async fn get_request( + tower_net_addr: &NetAddr, + endpoint: Endpoint, + proxy: &Option, +) -> Result { + request::<()>(tower_net_addr, endpoint, proxy, Method::GET, None).await } /// Generic function to process the response of a given post request. @@ -218,7 +238,6 @@ pub async fn process_post_response( #[cfg(test)] mod tests { use super::*; - use httpmock::prelude::*; use serde_json::json; use crate::test_utils::get_dummy_add_appointment_response; @@ -253,24 +272,25 @@ mod tests { let mut registration_receipt = get_random_registration_receipt(); registration_receipt.sign(&tower_sk); - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::Register.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(registration_receipt)); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(registration_receipt).to_string()) + .create_async() + .await; let receipt = register( TowerId(tower_pk), registration_receipt.user_id(), - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, ) .await .unwrap(); - api_mock.assert(); + api_mock.assert_async().await; assert_eq!(receipt, registration_receipt); } @@ -290,24 +310,25 @@ mod tests { #[tokio::test] async fn test_register_deserialize_error() { - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::Register.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!([])); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!([]).to_string()) + .create_async() + .await; let error = register( get_random_user_id(), get_random_user_id(), - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, ) .await .unwrap_err(); - api_mock.assert(); + api_mock.assert_async().await; assert!(matches!(error, RequestError::DeserializeError { .. })) } @@ -322,17 +343,18 @@ mod tests { let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; let (response, receipt) = add_appointment( TowerId(tower_pk), - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, &appointment, appointment_receipt.user_signature(), @@ -340,7 +362,7 @@ mod tests { .await .unwrap(); - api_mock.assert(); + api_mock.assert_async().await; assert_eq!(response, add_appointment_response.available_slots); assert_eq!(receipt, appointment_receipt); } @@ -354,17 +376,18 @@ mod tests { let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; let (response, receipt) = send_appointment( TowerId(tower_pk), - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, &appointment, appointment_receipt.user_signature(), @@ -372,7 +395,7 @@ mod tests { .await .unwrap(); - api_mock.assert(); + api_mock.assert_async().await; assert_eq!(response, add_appointment_response); assert_eq!(receipt, appointment_receipt); } @@ -386,18 +409,19 @@ mod tests { let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; let tower_id = get_random_user_id(); let error = send_appointment( tower_id, - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, &appointment, appointment_receipt.user_signature(), @@ -405,7 +429,7 @@ mod tests { .await .unwrap_err(); - api_mock.assert(); + api_mock.assert_async().await; if let AddAppointmentError::SignatureError(proof) = error { assert_eq!( MisbehaviorProof::new( @@ -441,17 +465,18 @@ mod tests { #[tokio::test] async fn test_send_appointment_deserialize_error() { - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!([])); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!([]).to_string()) + .create_async() + .await; let error = send_appointment( get_random_user_id(), - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, &generate_random_appointment(None), "user_sig", @@ -459,7 +484,7 @@ mod tests { .await .unwrap_err(); - api_mock.assert(); + api_mock.assert_async().await; if let AddAppointmentError::RequestError(e) = error { assert!(matches!(e, RequestError::DeserializeError { .. })) } else { @@ -474,17 +499,18 @@ mod tests { error_code: 1, }; - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(400) - .header("content-type", "application/json") - .json_body(json!(api_error)); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body(json!(api_error).to_string()) + .create_async() + .await; let error = send_appointment( get_random_user_id(), - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), &None, &generate_random_appointment(None), "user_sig", @@ -492,60 +518,160 @@ mod tests { .await .unwrap_err(); - api_mock.assert(); + api_mock.assert_async().await; assert!(matches!(error, AddAppointmentError::ApiError { .. })); } + #[tokio::test] + async fn test_request() { + let mut server = mockito::Server::new_async().await; + + // Test with POST + let api_mock_post = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + + let response_post = request( + &NetAddr::new(server.url()), + Endpoint::Register, + &None, + Method::POST, + Some(json!("")), + ) + .await; + + api_mock_post.assert_async().await; + assert!(matches!(response_post, Ok(Response { .. }))); + + // Test with GET + let api_mock_get = server + .mock("GET", Endpoint::Ping.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + + let response_get = request::<()>( + &NetAddr::new(server.url()), + Endpoint::Ping, + &None, + Method::GET, + None, + ) + .await; + + api_mock_get.assert_async().await; + assert!(matches!(response_get, Ok(Response { .. }))); + } + + #[tokio::test] + async fn test_request_connection_error() { + assert!(request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Register, + &None, + Method::POST, + Some(json!("")), + ) + .await + .unwrap_err() + .is_connection()); + + assert!(request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Ping, + &None, + Method::GET, + None::<&str>, + ) + .await + .unwrap_err() + .is_connection()); + } + + #[tokio::test] + async fn test_get_request() { + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("GET", Endpoint::Ping.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; + let response = get_request(&NetAddr::new(server.url()), Endpoint::Ping, &None).await; + + api_mock.assert_async().await; + + assert!(matches!(response, Ok(Response { .. }))); + } + + #[tokio::test] + async fn test_get_request_connection_error() { + assert!(get_request( + &NetAddr::new("http://unreachable_url".to_owned()), + Endpoint::Ping, + &None, + ) + .await + .unwrap_err() + .is_connection()); + } + #[tokio::test] async fn test_post_request() { - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST); - then.status(200).header("content-type", "application/json"); - }); + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::Register.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; let response = post_request( - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), + Endpoint::Register, + json!(""), + &None, + ) + .await; + + api_mock.assert_async().await; + assert!(matches!(response, Ok(Response { .. }))); + } + + #[tokio::test] + async fn test_post_request_connection_error() { + assert!(post_request( + &NetAddr::new("http://unreachable_url".to_owned()), Endpoint::Register, json!(""), &None, ) .await - .unwrap(); - - api_mock.assert(); - assert!(matches!(response, Response { .. })); - } - - #[tokio::test] - async fn test_post_request_connection_error() { - assert!(matches!( - post_request( - &NetAddr::new("http://unreachable_url".to_owned()), - Endpoint::Register, - json!(""), - &None, - ) - .await - .unwrap_err(), - RequestError::ConnectionError { .. } - )); + .unwrap_err() + .is_connection()); } #[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`. // So just testing that specific case should be enough. - let server = MockServer::start(); - let api_mock = server.mock(|when, then| { - when.method(POST); - then.status(200).header("content-type", "application/json"); - }); + + let mut server = mockito::Server::new_async().await; + let api_mock = server + .mock("POST", Endpoint::GetAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .create_async() + .await; // Any expected response work here as long as it cannot be properly deserialized let error = process_post_response::>( post_request( - &NetAddr::new(server.base_url()), + &NetAddr::new(server.url()), Endpoint::GetAppointment, json!(""), &None, @@ -555,7 +681,7 @@ mod tests { .await .unwrap_err(); - api_mock.assert(); + api_mock.assert_async().await; assert!(matches!(error, RequestError::DeserializeError { .. })); } } diff --git a/watchtower-plugin/src/retrier.rs b/watchtower-plugin/src/retrier.rs index b980a40..e89d4e5 100644 --- a/watchtower-plugin/src/retrier.rs +++ b/watchtower-plugin/src/retrier.rs @@ -103,7 +103,7 @@ impl RetryManager { } else if let Some(retrier) = self.retriers.get(&tower_id) { if retrier.is_idle() { if !data.is_none() { - log::error!("Data was send to an idle retier. This should have never happened. Please report! ({data:?})"); + log::error!("Data was send to an idle retrier. This should have never happened. Please report! ({data:?})"); continue; } log::info!( @@ -432,7 +432,7 @@ impl Retrier { // Create a new scope so we can get all the data only locking the WTClient once. let (tower_id, status, net_addr, user_id, user_sk, proxy) = { let wt_client = self.wt_client.lock().unwrap(); - if wt_client.towers.get(&self.tower_id).is_none() { + if !wt_client.towers.contains_key(&self.tower_id) { return Err(Error::permanent(RetryError::Abandoned)); } @@ -491,7 +491,7 @@ impl Retrier { &net_addr, &proxy, &appointment, - &cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(), + &cryptography::sign(&appointment.to_vec(), &user_sk), ) .await { @@ -576,13 +576,13 @@ impl Retrier { mod tests { use super::*; - use httpmock::prelude::*; use serde_json::json; use tempdir::TempDir; use tokio::sync::mpsc::unbounded_channel; use teos_common::errors; use teos_common::net::http::Endpoint; + use teos_common::protos::AddAppointmentRequest; use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; use teos_common::test_utils::{ generate_random_appointment, get_random_registration_receipt, get_random_user_id, @@ -600,6 +600,18 @@ mod tests { const MAX_INTERVAL_TIME: u16 = 1; const MAX_RUN_TIME: f64 = 0.2; + macro_rules! wait_until { + () => {}; + ($cond:expr $(,)?) => { + loop { + if $cond { + break; + } + tokio::time::sleep(Duration::from_secs_f64(0.1)).await; + } + }; + } + impl Retrier { fn empty(wt_client: Arc>, tower_id: TowerId) -> Self { Self { @@ -612,15 +624,14 @@ mod tests { } #[tokio::test] - // 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 = 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(); + + let mut server = mockito::Server::new_async().await; // Add a tower with pending appointments let (tower_sk, tower_pk) = cryptography::get_random_keypair(); @@ -629,7 +640,7 @@ mod tests { wt_client .lock() .unwrap() - .add_update_tower(tower_id, &server.base_url(), &receipt) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // Add appointment to pending @@ -641,19 +652,23 @@ mod tests { // Prepare the mock response let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); add_appointment_receipt.sign(&tower_sk); let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .delay(Duration::from_secs_f64(API_DELAY)) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |_| { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(add_appointment_response).to_string().into() + }) + .create_async() + .await; // Start the task and send the tower to the channel for retry tx.send((tower_id, RevocationData::Fresh(appointment.locator))) @@ -681,23 +696,23 @@ mod tests { .unwrap() .is_running()); - // Wait for the remaining time and re-check - tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME + HALF_API_DELAY)).await; - - let state = wt_client.lock().unwrap(); - assert_eq!( - state.get_tower_status(&tower_id).unwrap(), - TowerStatus::Reachable - ); - assert!(!state.retriers.contains_key(&tower_id)); - assert!(!state - .towers - .get(&tower_id) + wait_until!(wt_client + .lock() .unwrap() - .pending_appointments - .contains(&appointment.locator)); + .get_retrier_status(&tower_id) + .is_none()); - api_mock.assert(); + { + let state = wt_client.lock().unwrap(); + assert!(state.get_tower_status(&tower_id).unwrap().is_reachable()); + assert!(!state + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .contains(&appointment.locator)); + } + api_mock.assert_async().await; task.abort(); } @@ -759,39 +774,38 @@ mod tests { .unwrap() .is_running()); - // Wait until the task gives up and check again (this gives up due to accumulation of transient errors, so the retiers will be idle). - // Notice we'd normally wait for MAX_ELAPSED_TIME + MAX_RUN_TIME (the maximum time a Retrier can be working plus the marginal time of the last retry). - // However, we've already waited for MAX_RUN_TIME right before to check the tower was temporary unreachable, so we don't need to account for that again. - tokio::time::sleep(Duration::from_secs(MAX_ELAPSED_TIME as u64)).await; - assert!(wt_client - .lock() - .unwrap() - .get_tower_status(&tower_id) - .unwrap() - .is_unreachable()); - assert!(wt_client + // Wait until the task gives up and check again (this gives up due to accumulation of transient errors, so the retriers will be idle). + wait_until!(wt_client .lock() .unwrap() .get_retrier_status(&tower_id) .unwrap() .is_idle()); + assert!(wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap() + .is_unreachable()); + // Add a proper server and check that the auto-retry works // Prepare the mock response - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); add_appointment_receipt.sign(&tower_sk); let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .await; // Update the tower details wt_client @@ -799,7 +813,7 @@ mod tests { .unwrap() .add_update_tower( tower_id, - &server.base_url(), + &server.url(), &get_registration_receipt_from_previous(&receipt), ) .unwrap(); @@ -824,8 +838,7 @@ mod tests { .pending_appointments .contains(&appointment.locator)); assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); - - api_mock.assert(); + api_mock.assert_async().await; task.abort(); } @@ -837,7 +850,7 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // Add a tower with pending appointments let (_, tower_pk) = cryptography::get_random_keypair(); @@ -846,7 +859,7 @@ mod tests { wt_client .lock() .unwrap() - .add_update_tower(tower_id, &server.base_url(), &receipt) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // Add appointment to pending @@ -857,16 +870,21 @@ mod tests { .add_pending_appointment(tower_id, &appointment); // Prepare the mock response - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(400) - .delay(Duration::from_secs_f64(API_DELAY)) - .header("content-type", "application/json") - .json_body(json!(ApiError { + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body_from_request(|_| { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(ApiError { error: "error_msg".to_owned(), error_code: 1, - })); - }); + }) + .to_string() + .into() + }) + .create_async() + .await; // Start the task and send the tower to the channel for retry tx.send((tower_id, RevocationData::Fresh(appointment.locator))) @@ -895,16 +913,18 @@ mod tests { .is_running()); // Wait for the remaining time and re-check - tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME + HALF_API_DELAY)).await; - assert_eq!( - wt_client - .lock() - .unwrap() - .get_tower_status(&tower_id) - .unwrap(), - TowerStatus::Reachable - ); - assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); + + assert!(wt_client + .lock() + .unwrap() + .get_tower_status(&tower_id) + .unwrap() + .is_reachable()); assert!(!wt_client .lock() .unwrap() @@ -921,7 +941,7 @@ mod tests { .unwrap() .invalid_appointments .contains(&appointment.locator)); - api_mock.assert(); + api_mock.assert_async().await; task.abort(); } @@ -933,7 +953,7 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // Add a tower with pending appointments let (_, tower_pk) = cryptography::get_random_keypair(); @@ -942,7 +962,7 @@ mod tests { wt_client .lock() .unwrap() - .add_update_tower(tower_id, &server.base_url(), &receipt) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // Add appointment to pending @@ -954,20 +974,23 @@ mod tests { // Prepare the mock response let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); // Sign with a random key so it counts as misbehaving add_appointment_receipt.sign(&cryptography::get_random_keypair().0); let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .delay(Duration::from_secs_f64(API_DELAY)) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |_| { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(add_appointment_response).to_string().into() + }) + .create_async() + .await; // Start the task and send the tower to the channel for retry tx.send((tower_id, RevocationData::Fresh(appointment.locator))) @@ -995,19 +1018,21 @@ mod tests { .unwrap() .is_running()); - // Wait for the remaining time and re-check - tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY + MAX_RUN_TIME)).await; + // Wait until the tower is no longer being retried. + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); + + // The tower should have a misbehaving status. assert!(wt_client .lock() .unwrap() .get_tower_status(&tower_id) .unwrap() .is_misbehaving()); - - // Retriers are wiped every polling interval, so we'll need to wait a bit more to check it - tokio::time::sleep(Duration::from_secs(POLLING_TIME)).await; - assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); - api_mock.assert(); + api_mock.assert_async().await; task.abort(); } @@ -1019,7 +1044,7 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, )); - let server = MockServer::start(); + let server = mockito::Server::new_async().await; // Add a tower with pending appointments let (_, tower_pk) = cryptography::get_random_keypair(); @@ -1028,7 +1053,7 @@ mod tests { wt_client .lock() .unwrap() - .add_update_tower(tower_id, &server.base_url(), &receipt) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // Remove the tower (to simulate it has been abandoned) @@ -1049,7 +1074,6 @@ mod tests { .manage_retry() .await }); - assert!(!wt_client.lock().unwrap().towers.contains_key(&tower_id)); task.abort(); @@ -1062,7 +1086,7 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // Add a tower with pending appointments let (tower_sk, tower_pk) = cryptography::get_random_keypair(); @@ -1073,7 +1097,7 @@ mod tests { wt_client .lock() .unwrap() - .add_update_tower(tower_id, &server.base_url(), ®istration_receipt) + .add_update_tower(tower_id, &server.url(), ®istration_receipt) .unwrap(); // Add appointment to pending @@ -1083,32 +1107,37 @@ mod tests { .unwrap() .add_pending_appointment(tower_id, &appointment); - // Mock the add_appointment response (this is right, so after the re-registration the appointments are accepted) + // Mock the registration and add_appointment response (this is right, so after the re-registration the appointments are accepted) + let mut re_registration_receipt = + get_registration_receipt_from_previous(®istration_receipt); + re_registration_receipt.sign(&tower_sk); + let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); add_appointment_receipt.sign(&tower_sk); let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let add_appointment_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); - // Mock the re-registration - let mut re_registration_receipt = - get_registration_receipt_from_previous(®istration_receipt); - re_registration_receipt.sign(&tower_sk); - let register_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::Register.path()); - then.status(200) - .delay(Duration::from_secs_f64(API_DELAY)) - .header("content-type", "application/json") - .json_body(json!(re_registration_receipt)); - }); + let api_mock = server + .mock("POST", mockito::Matcher::Any) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |request| { + let response = if request.path() == Endpoint::Register.path().as_str() { + std::thread::sleep(Duration::from_secs_f64(API_DELAY)); + json!(re_registration_receipt).to_string() + } else if request.path() == Endpoint::AddAppointment.path().as_str() { + json!(add_appointment_response).to_string() + } else { + panic!("Wrong endpoint hit") + }; + response.into() + }) + .create_async() + .await + .expect(2); // Set the status as SubscriptionError so we simulate the retrier faced this in a previous round wt_client @@ -1143,16 +1172,20 @@ mod tests { .is_running()); // Wait for the remaining time and re-check - tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME + HALF_API_DELAY)).await; - let state = wt_client.lock().unwrap(); - assert!(!state.retriers.contains_key(&tower_id)); + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .is_none()); - let tower = state.towers.get(&tower_id).unwrap(); - assert!(tower.status.is_reachable()); - assert!(tower.pending_appointments.is_empty()); + { + let state = wt_client.lock().unwrap(); + let tower = state.towers.get(&tower_id).unwrap(); + assert!(tower.status.is_reachable()); + assert!(tower.pending_appointments.is_empty()); + } + api_mock.assert_async().await; - register_mock.assert(); - add_appointment_mock.assert(); task.abort(); } @@ -1206,9 +1239,15 @@ mod tests { MAX_ELAPSED_TIME as f64 + MAX_RUN_TIME, )) .await; - let state = wt_client.lock().unwrap(); - assert!(state.get_retrier_status(&tower_id).unwrap().is_idle()); + wait_until!(wt_client + .lock() + .unwrap() + .get_retrier_status(&tower_id) + .unwrap() + .is_idle()); + + let state = wt_client.lock().unwrap(); let tower = state.towers.get(&tower_id).unwrap(); assert!(tower.pending_appointments.contains(&appointment.locator)); assert_eq!(tower.status, TowerStatus::Unreachable); @@ -1216,12 +1255,12 @@ mod tests { // With the retrier idling all fresh data sent to it will be stored but it won't trigger a retry. // (we can check the data was stored later on) - let new_appointment = generate_random_appointment(None); + let appointment2 = generate_random_appointment(None); wt_client .lock() .unwrap() - .add_pending_appointment(tower_id, &new_appointment); - tx.send((tower_id, RevocationData::Fresh(new_appointment.locator))) + .add_pending_appointment(tower_id, &appointment2); + tx.send((tower_id, RevocationData::Fresh(appointment2.locator))) .unwrap(); { @@ -1232,22 +1271,40 @@ mod tests { assert_eq!(tower.status, TowerStatus::Unreachable); } - let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + // Create the receipts, the responses and set the mocks + let mut appointment_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); + let mut appointment2_receipt = AppointmentReceipt::new( + cryptography::sign(&appointment2.to_vec(), &wt_client.lock().unwrap().user_sk), + 42, + ); + appointment_receipt.sign(&tower_sk); + appointment2_receipt.sign(&tower_sk); // Mock a proper response - let server = MockServer::start(); - add_appointment_receipt.sign(&tower_sk); - let add_appointment_response = - get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let mut server = mockito::Server::new_async().await; + + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body_from_request(move |request| { + let body = serde_json::from_slice::(request.body().unwrap()) + .unwrap(); + + let response = if body.appointment.unwrap().locator == appointment.locator.to_vec() + { + get_dummy_add_appointment_response(appointment.locator, &appointment_receipt) + } else { + get_dummy_add_appointment_response(appointment2.locator, &appointment2_receipt) + }; + json!(response).to_string().into() + }) + .expect(2) + .create_async() + .await; // Patch the tower address wt_client @@ -1256,7 +1313,7 @@ mod tests { .towers .get_mut(&tower_id) .unwrap() - .set_net_addr(server.base_url()); + .set_net_addr(server.url()); // Check pending data is still there now, and is it not once the retrier succeeds assert_eq!( @@ -1274,24 +1331,19 @@ mod tests { // Send a retry flag to the retrier to force a retry. tx.send((tower_id, RevocationData::None)).unwrap(); + // After retrying the pending pool has been emptied, meaning that both appointments went trough tokio::time::sleep(Duration::from_secs_f64(POLLING_TIME as f64 + MAX_RUN_TIME)).await; - // FIXME: Here we should be able to check this, however, due to httpmock limitations, we cannot return a response based on the request. - // Therefore, both requests will be responded with the same data. Given pending_appointments is a HashSet, we cannot even know which request - // will be sent first (sets are initialized with a random state, which decided the order or iteration). - // https://github.com/alexliesenfeld/httpmock/issues/49 - // assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); - // assert!(wt_client - // .lock() - // .unwrap() - // .towers - // .get(&tower_id) - // .unwrap() - // .pending_appointments - // .is_empty()); + assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); + assert!(wt_client + .lock() + .unwrap() + .towers + .get(&tower_id) + .unwrap() + .pending_appointments + .is_empty()); + api_mock.assert_async().await; - // This is not much tbh, but looks like its the best we can do at the moment without experiencing random errors. - // Depending on what appointment is sent first the api will be hit either one or two times. - assert!(api_mock.hits() >= 1 && api_mock.hits() <= 2); task.abort(); } @@ -1303,14 +1355,14 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // 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) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // Add appointment to pending @@ -1322,24 +1374,25 @@ mod tests { // Prepare the mock response let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); add_appointment_receipt.sign(&tower_sk); let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .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, HashSet::from([appointment.locator])); let r = retrier.run().await; assert_eq!(r, Ok(())); - api_mock.assert(); + api_mock.assert_async().await; } #[tokio::test] @@ -1350,14 +1403,14 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); - let server = MockServer::start(); + let server = mockito::Server::new_async().await; // 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) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // If there are no pending appointments the method will simply return @@ -1373,14 +1426,14 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // 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) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); // Add appointment to pending @@ -1392,18 +1445,19 @@ mod tests { // Prepare the mock response let mut add_appointment_receipt = AppointmentReceipt::new( - cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(), + cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk), 42, ); add_appointment_receipt.sign(&cryptography::get_random_keypair().0); let add_appointment_response = get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(200) - .header("content-type", "application/json") - .json_body(json!(add_appointment_response)); - }); + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!(add_appointment_response).to_string()) + .create_async() + .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, HashSet::from([appointment.locator])); @@ -1412,7 +1466,7 @@ mod tests { r, Err(Error::Permanent(RetryError::Misbehaving { .. },)) )); - api_mock.assert(); + api_mock.assert_async().await; } #[tokio::test] @@ -1454,25 +1508,29 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // 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) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(400) - .header("content-type", "application/json") - .json_body(json!(ApiError { + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body( + json!(ApiError { error: "error_msg".to_owned(), error_code: errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR, - })); - }); + }) + .to_string(), + ) + .create_async() + .await; // Add some pending appointments and try again (with an unreachable tower). let appointment = generate_random_appointment(None); @@ -1492,7 +1550,7 @@ mod tests { .. }) )); - api_mock.assert(); + api_mock.assert_async().await; } #[tokio::test] @@ -1503,25 +1561,29 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); - let server = MockServer::start(); + let mut server = mockito::Server::new_async().await; // 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) + .add_update_tower(tower_id, &server.url(), &receipt) .unwrap(); - let api_mock = server.mock(|when, then| { - when.method(POST).path(Endpoint::AddAppointment.path()); - then.status(400) - .header("content-type", "application/json") - .json_body(json!(ApiError { + let api_mock = server + .mock("POST", Endpoint::AddAppointment.path().as_str()) + .with_status(400) + .with_header("content-type", "application/json") + .with_body( + json!(ApiError { error: "error_msg".to_owned(), error_code: 1, - })); - }); + }) + .to_string(), + ) + .create_async() + .await; // Add some pending appointments and try again (with an unreachable tower). let appointment = generate_random_appointment(None); @@ -1538,8 +1600,6 @@ mod tests { ); let r = retrier.run().await; - assert_eq!(r, Ok(())); - api_mock.assert(); assert!(wt_client .lock() .unwrap() @@ -1548,6 +1608,8 @@ mod tests { .unwrap() .invalid_appointments .contains(&appointment.locator)); + assert!(r.is_ok()); + api_mock.assert_async().await; } #[tokio::test] @@ -1558,14 +1620,13 @@ mod tests { let wt_client = Arc::new(Mutex::new( WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, )); - let server = MockServer::start(); // 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) + .add_update_tower(tower_id, "http://tower.adrress", &receipt) .unwrap(); // Remove the tower (to simulate it has been abandoned) diff --git a/watchtower-plugin/src/wt_client.rs b/watchtower-plugin/src/wt_client.rs index 315d9aa..ecf26db 100644 --- a/watchtower-plugin/src/wt_client.rs +++ b/watchtower-plugin/src/wt_client.rs @@ -95,17 +95,17 @@ impl WTClient { }); let dbm = DBM::new(&data_dir.join("watchtowers_db.sql3")).unwrap(); - let (user_sk, user_id) = match dbm.load_client_key() { - Ok(sk) => ( + + let (user_sk, user_id) = if let Some(sk) = dbm.load_client_key() { + ( sk, UserId(PublicKey::from_secret_key(&Secp256k1::new(), &sk)), - ), - Err(_) => { - log::info!("Watchtower client keys not found. Creating a fresh set"); - let (sk, pk) = cryptography::get_random_keypair(); - dbm.store_client_key(&sk).unwrap(); - (sk, UserId(pk)) - } + ) + } else { + log::info!("Watchtower client keys not found. Creating a fresh set"); + let (sk, pk) = cryptography::get_random_keypair(); + dbm.store_client_key(&sk).unwrap(); + (sk, UserId(pk)) }; let towers = dbm.load_towers(); @@ -180,15 +180,12 @@ impl WTClient { } /// Gets the latest registration receipt of a given tower. - pub fn get_registration_receipt( - &self, - tower_id: TowerId, - ) -> Result { + pub fn get_registration_receipt(&self, tower_id: TowerId) -> Option { 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 { + pub fn load_tower_info(&self, tower_id: TowerId) -> Option { self.dbm.load_tower_record(tower_id) } @@ -240,7 +237,7 @@ impl WTClient { &self, tower_id: TowerId, locator: Locator, - ) -> Result { + ) -> Option { self.dbm.load_appointment_receipt(tower_id, locator) } @@ -810,10 +807,7 @@ mod tests { // 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.load_tower_info(tower_id).is_none()); assert!(!wt_client.towers.contains_key(&tower_id)); // Try again but this time with an associated appointment to check that it also gets removed @@ -836,10 +830,7 @@ mod tests { // 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.load_tower_info(tower_id).is_none()); assert!(!wt_client.towers.contains_key(&tower_id)); assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower_id)); } @@ -890,10 +881,7 @@ mod tests { // 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.load_tower_info(tower1_id).is_none()); assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower1_id)); assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id)); diff --git a/watchtower-plugin/tests/conftest.py b/watchtower-plugin/tests/conftest.py index eb03b75..7a7be94 100644 --- a/watchtower-plugin/tests/conftest.py +++ b/watchtower-plugin/tests/conftest.py @@ -2,7 +2,7 @@ from pathlib import Path import subprocess from pyln.testing.fixtures import * # noqa: F401,F403 -from pyln.testing.utils import DEVELOPER, BITCOIND_CONFIG, TailableProc +from pyln.testing.utils import BITCOIND_CONFIG, TailableProc WT_PLUGIN = Path("~/.cargo/bin/watchtower-client").expanduser() TEOSD_CONFIG = { @@ -116,19 +116,6 @@ def pytest_runtest_makereport(item, call): setattr(item, "rep_" + rep.when, rep) -def pytest_configure(config): - config.addinivalue_line("markers", "developer: only run when developer is flagged on") - - -def pytest_runtest_setup(item): - for mark in item.iter_markers(name="developer"): - if not DEVELOPER: - if len(mark.args): - 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 diff --git a/watchtower-plugin/tests/pyproject.toml b/watchtower-plugin/tests/pyproject.toml index 2760baa..f11efae 100644 --- a/watchtower-plugin/tests/pyproject.toml +++ b/watchtower-plugin/tests/pyproject.toml @@ -12,7 +12,8 @@ black = "^22.6.0" [tool.poetry.dev-dependencies] pytest = "^7.1.2" pytest-timeout = "^2.1.0" -pyln-testing = "^0.12.1" +pyln-testing = "^24.2.1" +pyln-client = "^24.2.1" [build-system] diff --git a/watchtower-plugin/tests/test.py b/watchtower-plugin/tests/test.py index 19d633d..7b4657c 100644 --- a/watchtower-plugin/tests/test.py +++ b/watchtower-plugin/tests/test.py @@ -1,5 +1,4 @@ import pytest -from pyln.client import RpcError from conftest import WT_PLUGIN @@ -16,7 +15,6 @@ def change_endianness(x): return b[::-1].hex() -@pytest.mark.developer("Requires dev_sign_last_tx") def test_watchtower(node_factory, bitcoind, teosd): """ Test watchtower hook. @@ -27,7 +25,13 @@ def test_watchtower(node_factory, bitcoind, teosd): commitment transaction. """ - l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": WT_PLUGIN}]) + l1, l2 = node_factory.line_graph( + 2, + opts=[ + {"broken_log": r"Could not find resolution for output [0-9]?: did \*we\* cheat\?"}, + {"plugin": WT_PLUGIN}, + ], + ) # We need to register l2 with the tower tower_id = teosd.cli.gettowerinfo()["tower_id"] @@ -48,7 +52,7 @@ def test_watchtower(node_factory, bitcoind, teosd): locator = change_endianness(dispute_txid[32:]) # Make sure l2's normal penalty_tx doesn't reach the network - l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda: None) + l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda _: {"result": None, "error": None, "id": "pytest"}) l2.start() # The tower will react once the dispute gets confirmed. For now it is still watching for it @@ -60,7 +64,7 @@ def test_watchtower(node_factory, bitcoind, teosd): penalty_txid = bitcoind.rpc.getrawmempool()[0] # The channel still exists between the two peers, but it's on chain - assert l1.rpc.listpeers()["peers"][0]["channels"][0]["state"] == "ONCHAIN" + assert l1.rpc.listpeerchannels()["channels"][0]["state"] == "ONCHAIN" assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded" # Generate blocks until the penalty gets irrevocably resolved @@ -90,7 +94,6 @@ def test_unreachable_watchtower(node_factory, bitcoind, teosd): {}, { "plugin": WT_PLUGIN, - "allow_broken_log": True, "dev-watchtower-max-retry-interval": max_interval_time, }, ], @@ -123,7 +126,7 @@ def test_auto_retry_watchtower(node_factory, bitcoind, teosd): {}, { "plugin": WT_PLUGIN, - "allow_broken_log": True, + "broken_log": r"plugin-watchtower-client: Data was send to an idle retrier. This should have never happened. Please report!.*", "watchtower-max-retry-time": 1, "watchtower-auto-retry-delay": 1, }, @@ -141,7 +144,7 @@ def test_auto_retry_watchtower(node_factory, bitcoind, teosd): l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) # Wait until the tower has been flagged as unreachable - l2.daemon.wait_for_log(f"Starting to idle") + l2.daemon.wait_for_log("Starting to idle") assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] @@ -161,7 +164,6 @@ def test_manually_retry_watchtower(node_factory, bitcoind, teosd): {}, { "plugin": WT_PLUGIN, - "allow_broken_log": True, "watchtower-max-retry-time": 0, }, ], @@ -178,7 +180,7 @@ def test_manually_retry_watchtower(node_factory, bitcoind, teosd): l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) # Wait until the tower has been flagged as unreachable - l2.daemon.wait_for_log(f"Starting to idle") + l2.daemon.wait_for_log("Starting to idle") assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] @@ -193,7 +195,7 @@ def test_manually_retry_watchtower(node_factory, bitcoind, teosd): def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory): - l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN, "allow_broken_log": True}]) + l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}]) # We need to register l2 with the tower tower_id = teosd.cli.gettowerinfo()["tower_id"] @@ -210,7 +212,7 @@ def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory): def test_get_appointment(node_factory, bitcoind, teosd, directory): - l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": WT_PLUGIN}]) + l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}]) # We need to register l2 with the tower tower_id = teosd.cli.gettowerinfo()["tower_id"]